commit e0c489f62557a86332e708af7639b5f933d56313 Author: kcar Date: Fri Jul 11 13:52:19 2025 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..591b3e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.env +.DS_Store +.vscode +.idea +.pytest_cache +.coverage +.coverage.* +.coverage.*.* +.coverage.*.*.* +.coverage.*.*.*.* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95bab27 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + libreoffice \ + poppler-utils \ + libpq-dev \ + gcc \ + python3-dev \ + postgresql-client \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set up virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install Python packages +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy the application +COPY . . + +# Create necessary directories +RUN mkdir -p static templates/admin logs + +# Create entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE ${PORT_BACKEND} + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Dockerfile.macos.dev b/Dockerfile.macos.dev new file mode 100644 index 0000000..68a3d0a --- /dev/null +++ b/Dockerfile.macos.dev @@ -0,0 +1,38 @@ +FROM python:3.11-slim + +WORKDIR /app/backend + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + libreoffice \ + poppler-utils \ + libpq-dev \ + gcc \ + python3-dev \ + postgresql-client \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set up virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install Python packages +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy the application +COPY . . + +# Create necessary directories +RUN mkdir -p static templates/admin logs + +# Create entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE ${PORT_BACKEND} + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Dockerfile.macos.prod b/Dockerfile.macos.prod new file mode 100644 index 0000000..68a3d0a --- /dev/null +++ b/Dockerfile.macos.prod @@ -0,0 +1,38 @@ +FROM python:3.11-slim + +WORKDIR /app/backend + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + libreoffice \ + poppler-utils \ + libpq-dev \ + gcc \ + python3-dev \ + postgresql-client \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set up virtual environment +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install Python packages +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Copy the application +COPY . . + +# Create necessary directories +RUN mkdir -p static templates/admin logs + +# Create entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE ${PORT_BACKEND} + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..94851ef --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +The main issues are with system-level dependencies like `libreoffice` and `gcc` which aren't typically managed through conda on macOS. + +```yaml:backend/requirements.conda.yml +name: classroomcopilot-backend +channels: + - conda-forge + - nodefaults +dependencies: + - python=3.11 + # Core dependencies + - pip + - setuptools + - wheel + # Server dependencies + - fastapi + - uvicorn + - python-dotenv + - python-multipart + - python-jose + - pyjwt + - jinja2 + # Database and Auth + - sqlalchemy + - sqlalchemy-utils + - asyncpg + - redis-py + # Neo4j + - neo4j-python-driver + # Data Processing + - pandas + - scipy + - requests + - openpyxl + - scikit-learn + # Testing + - pytest + - pytest-html + # Database clients + - postgresql + - libpq + # Additional dependencies via pip + - pip: + # Supabase + - supabase + # Neo4j specific + - neontology + # Document Processing + - python-pptx + - python-docx + - pdfminer.six + - Pillow + - psutil + # Web Scraping and Processing + - emoji + - extruct + - w3lib + # Google APIs + - youtube-transcript-api + - google-api-python-client + - google-auth-oauthlib + # LangChain Ecosystem + - "langchain[llms]" + - langchain-community + - langchain-openai + - langgraph + # OpenAI + - openai + - ollama + # Microsoft Authentication + - msal +``` +I've made several important changes to make the conda environment more compatible: + +1. Removed system-level dependencies that should be installed via the system package manager: + - `libreoffice` - Install via Homebrew on macOS (`brew install libreoffice`) + - `gcc` - Install via Xcode Command Line Tools on macOS + - `poppler` - Install via Homebrew on macOS (`brew install poppler`) + +2. Changed channel configuration: + - Using only `conda-forge` with `nodefaults` to avoid package conflicts + - This provides better consistency across platforms + +3. Fixed the `langchain[llms]` specification by adding quotes to handle the square brackets properly + +For macOS users, here are the steps to set up the environment: + +1. First, install system dependencies: +```bash +# Install Xcode Command Line Tools (if not already installed) +xcode-select --install + +# Install other system dependencies via Homebrew +brew install libreoffice poppler postgresql +``` + +2. Then create the conda environment: +```bash +conda env create -f backend/requirements.conda.yml +``` + +3. Activate the environment: +```bash +conda activate classroomcopilot-backend +``` + +This setup should now work properly on macOS while still maintaining compatibility with the Docker environment for production. The system-level dependencies are handled by the appropriate system package manager (Homebrew for macOS) rather than conda, which is the recommended approach for these types of dependencies. + +Would you like me to make any additional adjustments to the environment configuration? diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..a9bb1c2 Binary files /dev/null and b/__pycache__/main.cpython-311.pyc differ diff --git a/data/init/default_institute/default_curriculum.xlsx b/data/init/default_institute/default_curriculum.xlsx new file mode 100644 index 0000000..ad428f8 Binary files /dev/null and b/data/init/default_institute/default_curriculum.xlsx differ diff --git a/data/init/default_institute/default_timetable.xlsx b/data/init/default_institute/default_timetable.xlsx new file mode 100644 index 0000000..bf9a630 Binary files /dev/null and b/data/init/default_institute/default_timetable.xlsx differ diff --git a/data/init/fpgs_data/fpgs_curriculum.xlsx b/data/init/fpgs_data/fpgs_curriculum.xlsx new file mode 100644 index 0000000..ad428f8 Binary files /dev/null and b/data/init/fpgs_data/fpgs_curriculum.xlsx differ diff --git a/data/init/fpgs_data/fpgs_timetable.xlsx b/data/init/fpgs_data/fpgs_timetable.xlsx new file mode 100644 index 0000000..57a18e3 Binary files /dev/null and b/data/init/fpgs_data/fpgs_timetable.xlsx differ diff --git a/data/init/fpgs_data/users/kca_planning.xlsx b/data/init/fpgs_data/users/kca_planning.xlsx new file mode 100644 index 0000000..4044868 Binary files /dev/null and b/data/init/fpgs_data/users/kca_planning.xlsx differ diff --git a/data/init/fpgs_data/users/kca_timetable.xlsx b/data/init/fpgs_data/users/kca_timetable.xlsx new file mode 100644 index 0000000..794f128 Binary files /dev/null and b/data/init/fpgs_data/users/kca_timetable.xlsx differ diff --git a/data/init/global_schools/Kent.csv b/data/init/global_schools/Kent.csv new file mode 100644 index 0000000..3d03ba3 --- /dev/null +++ b/data/init/global_schools/Kent.csv @@ -0,0 +1,624 @@ +URN,LA (code),LA (name),EstablishmentNumber,EstablishmentName,TypeOfEstablishment (name),EstablishmentTypeGroup (name),EstablishmentStatus (name),ReasonEstablishmentOpened (name),OpenDate,ReasonEstablishmentClosed (name),CloseDate,PhaseOfEducation (name),StatutoryLowAge,StatutoryHighAge,Boarders (name),NurseryProvision (name),OfficialSixthForm (name),Gender (name),ReligiousCharacter (name),ReligiousEthos (name),Diocese (name),AdmissionsPolicy (name),SchoolCapacity,SpecialClasses (name),CensusDate,NumberOfPupils,NumberOfBoys,NumberOfGirls,PercentageFSM,TrustSchoolFlag (name),Trusts (name),SchoolSponsorFlag (name),SchoolSponsors (name),FederationFlag (name),Federations (name),UKPRN,FEHEIdentifier,FurtherEducationType (name),OfstedLastInsp,LastChangedDate,Street,Locality,Address3,Town,County (name),Postcode,SchoolWebsite,TelephoneNum,HeadTitle (name),HeadFirstName,HeadLastName,HeadPreferredJobTitle,BSOInspectorateName (name),InspectorateReport,DateOfLastInspectionVisit,NextInspectionVisit,TeenMoth (name),TeenMothPlaces,CCF (name),SENPRU (name),EBD (name),PlacesPRU,FTProv (name),EdByOther (name),Section41Approved (name),SEN1 (name),SEN2 (name),SEN3 (name),SEN4 (name),SEN5 (name),SEN6 (name),SEN7 (name),SEN8 (name),SEN9 (name),SEN10 (name),SEN11 (name),SEN12 (name),SEN13 (name),TypeOfResourcedProvision (name),ResourcedProvisionOnRoll,ResourcedProvisionCapacity,SenUnitOnRoll,SenUnitCapacity,GOR (name),DistrictAdministrative (name),AdministrativeWard (name),ParliamentaryConstituency (name),UrbanRural (name),GSSLACode (name),Easting,Northing,MSOA (name),LSOA (name),InspectorateName (name),SENStat,SENNoStat,PropsName,OfstedRating (name),RSCRegion (name),Country (name),UPRN,SiteName,QABName (name),EstablishmentAccredited (name),QABReport,CHNumber,MSOA (code),LSOA (code),FSM,AccreditationExpiryDate +118229,886,Kent,1001,Northfleet Nursery School,Local authority nursery school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Nursery,2.0,5,No boarders,Has Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,,Not applicable,19-01-2023,91.0,57.0,34.0,0.0,Not applicable,,Not applicable,,Not under a federation,,,,Not applicable,19-07-2022,16-05-2024,140 London Road,Northfleet,,Gravesend,Kent,DA11 9JS,www.northfleetnursery.co.uk,1474533950.0,Mrs,Neerasha,Singh,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Gravesham,Rosherville,Gravesham,(England/Wales) Urban major conurbation,E10000016,563365.0,174145.0,Gravesham 001,Gravesham 001D,,,,,Outstanding,South-East England and South London,,10012024908.0,,Not applicable,Not applicable,,,E02005055,E01024279,0.0, +118254,886,Kent,2088,Crockenhill Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,187.0,83.0,104.0,29.4,Not applicable,,Not applicable,,Not under a federation,,10072997.0,,Not applicable,27-03-2019,04-12-2023,The Green,Crockenhill,,,Kent,BR8 8JG,http://www.crockenhill.kent.sch.uk,3000658300.0,Mrs,Karen,Dodd,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Sevenoaks,Crockenhill and Well Hill,Sevenoaks,(England/Wales) Urban city and town,E10000016,550590.0,167367.0,Sevenoaks 003,Sevenoaks 003A,,,,,Good,South-East England and South London,,,,Not applicable,Not applicable,,,E02005089,E01024421,55.0, +118255,886,Kent,2089,The Anthony Roper Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,315.0,No Special Classes,19-01-2023,307.0,151.0,156.0,9.8,Not supported by a trust,,Not applicable,,Not under a federation,,10069650.0,,Not applicable,27-06-2019,16-04-2024,High Street,Eynsford,,Dartford,Kent,DA4 0AA,www.anthony-roper.kent.sch.uk/,1322863680.0,Mr,Adam,Nicholls,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Eynsford,Sevenoaks,(England/Wales) Rural town and fringe,E10000016,554368.0,165949.0,Sevenoaks 005,Sevenoaks 005A,,,,,Good,South-East England and South London,,100060999084.0,,Not applicable,Not applicable,,,E02005091,E01024431,30.0, +118257,886,Kent,2094,Cobham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,219.0,No Special Classes,19-01-2023,218.0,109.0,109.0,7.3,Not applicable,,Not applicable,,Not under a federation,,10073366.0,,Not applicable,15-11-2012,07-05-2024,The Street,Cobham,,Gravesend,Kent,DA12 3BN,www.cobham.kent.sch.uk/,1474814373.0,Mrs,Jacqui,Saunders,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,"Istead Rise, Cobham & Luddesdown",Gravesham,(England/Wales) Rural village,E10000016,567152.0,168444.0,Gravesham 013,Gravesham 013D,,,,,Outstanding,South-East England and South London,,100062312570.0,,Not applicable,Not applicable,,,E02005067,E01024302,16.0, +118258,886,Kent,2095,Cecil Road Primary and Nursery School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,378.0,No Special Classes,19-01-2023,444.0,228.0,216.0,16.2,Supported by a trust,Northfleet Schools Co-Operative Trust,Not applicable,,Not under a federation,,10069334.0,,Not applicable,05-12-2019,23-04-2024,Cecil Road,Northfleet,,Gravesend,Kent,DA11 7BT,www.cecilroad.co.uk,1474534544.0,Mrs,C,Old,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Gravesham,Coldharbour & Perry Street,Gravesham,(England/Wales) Urban major conurbation,E10000016,563896.0,173124.0,Gravesham 004,Gravesham 004E,,,,,Good,South-East England and South London,,100062310705.0,,Not applicable,Not applicable,,,E02005058,E01024284,64.0, +118262,886,Kent,2109,Higham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,208.0,101.0,107.0,11.5,Not applicable,,Not applicable,,Not under a federation,,10073365.0,,Not applicable,25-01-2024,20-05-2024,School Lane,Higham,,Rochester,Kent,ME3 7JL,www.higham.kent.sch.uk,1474822535.0,Mrs,Catherine,Grattan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Higham & Shorne,Gravesham,(England/Wales) Rural village,E10000016,571349.0,172256.0,Gravesham 010,Gravesham 010C,,,,,Good,South-East England and South London,,100062390148.0,,Not applicable,Not applicable,,,E02005064,E01024267,24.0, +118264,886,Kent,2116,Lawn Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,262.0,No Special Classes,19-01-2023,230.0,117.0,113.0,52.7,Supported by a trust,Northfleet Schools Co-Operative Trust,Not applicable,,Not under a federation,,10077449.0,,Not applicable,11-01-2023,08-01-2024,High Street,Northfleet,,Gravesend,Kent,DA11 9HB,http://www.lawnprimary.co.uk,1474365303.0,Mrs,A,Wilson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Northfleet & Springhead,Gravesham,(England/Wales) Urban major conurbation,E10000016,562102.0,174368.0,Gravesham 001,Gravesham 001C,,,,,Requires improvement,South-East England and South London,,100062311102.0,,Not applicable,Not applicable,,,E02005055,E01024278,108.0, +118266,886,Kent,2120,Bean Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,190.0,88.0,102.0,22.1,Not applicable,,Not applicable,,Not under a federation,,10073364.0,,Not applicable,06-11-2019,21-05-2024,School Lane,Bean,,Dartford,Kent,DA2 8AW,https://www.bean.kent.sch.uk/,1474833225.0,Mr,Graham,Reilly,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dartford,Bean & Village Park,Dartford,(England/Wales) Rural town and fringe,E10000016,558989.0,172058.0,Dartford 012,Dartford 012B,,,,,Good,South-East England and South London,,100060855655.0,,Not applicable,Not applicable,,,E02005039,E01024134,42.0, +118271,886,Kent,2128,Capel Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,212.0,No Special Classes,19-01-2023,207.0,113.0,94.0,15.9,Not applicable,,Not applicable,,Not under a federation,,10073362.0,,Not applicable,15-01-2019,24-04-2024,Five Oak Green Road,Five Oak Green,,Tonbridge,Kent,TN12 6RP,www.capelschool.com/,1892833919.0,Mrs,Suzanne,Farr,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Capel,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,563919.0,145118.0,Tunbridge Wells 001,Tunbridge Wells 001A,,,,,Good,South-East England and South London,,10008670848.0,,Not applicable,Not applicable,,,E02005162,E01024798,33.0, +118272,886,Kent,2130,Dunton Green Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,187.0,90.0,97.0,31.6,Not applicable,,Not applicable,,Not under a federation,,10069525.0,,Not applicable,18-07-2018,08-04-2024,London Road,Dunton Green,,Sevenoaks,Kent,TN13 2UP,www.dunton-green.kent.sch.uk/,1732462221.0,Mr,Ben,Hulme,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Dunton Green and Riverhead,Sevenoaks,(England/Wales) Urban city and town,E10000016,551173.0,157352.0,Sevenoaks 008,Sevenoaks 008F,,,,,Good,South-East England and South London,United Kingdom,100062547929.0,,Not applicable,Not applicable,,,E02005094,E01035000,59.0, +118273,886,Kent,2132,Hadlow Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,187.0,107.0,80.0,38.5,Not applicable,,Not applicable,,Supported by a federation,The Bourne Partnership,10073361.0,,Not applicable,02-10-2019,13-04-2024,Hadlow,,,Tonbridge,Kent,TN11 0EH,www.hadlow.kent.sch.uk,1732850349.0,Miss,Nicole,Chapman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Bourne,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,563334.0,149843.0,Tonbridge and Malling 008,Tonbridge and Malling 008D,,,,,Good,South-East England and South London,,200000967219.0,,Not applicable,Not applicable,,,E02005156,E01024747,72.0, +118277,886,Kent,2136,Kemsing Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,184.0,99.0,85.0,14.7,Not applicable,,Not applicable,,Not under a federation,,10069522.0,,Not applicable,20-07-2022,19-01-2024,West End,Kemsing,,Sevenoaks,Kent,TN15 6PU,http://www.kemsing.kent.sch.uk,1732761236.0,Mr,Tom,Hardwick,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Kemsing,Sevenoaks,(England/Wales) Rural town and fringe,E10000016,555461.0,158873.0,Sevenoaks 009,Sevenoaks 009C,,,,,Good,South-East England and South London,,50002002944.0,,Not applicable,Not applicable,,,E02005095,E01024450,27.0, +118278,886,Kent,2137,Leigh Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,171.0,No Special Classes,19-01-2023,161.0,77.0,84.0,18.0,Not applicable,,Not applicable,,Not under a federation,,10073359.0,,Not applicable,08-02-2024,20-05-2024,The Green,Leigh,,Tonbridge,Kent,TN11 8QP,www.leighprimaryschool.com,1732832660.0,Mrs,Jenna,Halfhide,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Leigh and Chiddingstone Causeway,Tonbridge and Malling,(England/Wales) Rural village,E10000016,554871.0,146478.0,Sevenoaks 015,Sevenoaks 015B,,,,,Good,South-East England and South London,,50002001763.0,,Not applicable,Not applicable,,,E02005101,E01024451,29.0, +118279,886,Kent,2138,Otford Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,332.0,172.0,160.0,13.3,Not applicable,,Not applicable,,Not under a federation,,10069521.0,,Not applicable,18-10-2023,23-04-2024,High Street,Otford,,Sevenoaks,Kent,TN14 5PG,www.otford.kent.sch.uk,1959523145.0,Mrs,Helen,Roberts,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Otford and Shoreham,Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,552632.0,159289.0,Sevenoaks 008,Sevenoaks 008E,,,,,Good,South-East England and South London,,100062548574.0,,Not applicable,Not applicable,,,E02005094,E01024452,44.0, +118280,886,Kent,2139,Pembury School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,510.0,No Special Classes,19-01-2023,404.0,199.0,205.0,11.6,Not applicable,,Not applicable,,Not under a federation,,10073358.0,,Not applicable,26-02-2019,04-04-2024,Lower Green Road,Pembury,,Tunbridge Wells,Kent,TN2 4EB,http://www.pembury.kent.sch.uk,1892822259.0,Mrs,Hannah,Walters,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Pembury,Tunbridge Wells,(England/Wales) Rural town and fringe,E10000016,562858.0,141702.0,Tunbridge Wells 004,Tunbridge Wells 004D,,,,,Good,South-East England and South London,,100062554668.0,,Not applicable,Not applicable,,,E02005165,E01024827,47.0, +118282,886,Kent,2142,Sandhurst Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,191.0,No Special Classes,19-01-2023,154.0,74.0,80.0,25.3,Not applicable,,Not applicable,,Not under a federation,,10069520.0,,Not applicable,06-02-2019,07-05-2024,Rye Road,Sandhurst,Sandhurst Primary School,Cranbrook,Kent,TN18 5JE,www.sandhurst.kent.sch.uk,1580850288.0,Mrs,Amanda,Norman,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Hawkhurst and Sandhurst,Tunbridge Wells,(England/Wales) Rural village,E10000016,580082.0,128340.0,Tunbridge Wells 014,Tunbridge Wells 014D,,,,,Good,South-East England and South London,,10000069892.0,,Not applicable,Not applicable,,,E02005175,E01024809,39.0, +118283,886,Kent,2147,Weald Community Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,175.0,No Special Classes,19-01-2023,126.0,55.0,71.0,11.9,Not applicable,,Not applicable,,Not under a federation,,10069519.0,,Not applicable,04-03-2020,27-03-2024,Long Barn Road,Weald,,Sevenoaks,Kent,TN14 6PY,www.weald.kent.sch.uk,1732463307.0,Mr,David,Pyle,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Seal and Weald,Sevenoaks,(England/Wales) Rural village,E10000016,552697.0,150900.0,Sevenoaks 012,Sevenoaks 012B,,,,,Good,South-East England and South London,,100062548945.0,,Not applicable,Not applicable,,,E02005098,E01024459,15.0, +118284,886,Kent,2148,Shoreham Village School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,84.0,40.0,44.0,21.4,Not applicable,,Not applicable,,Not under a federation,,10073357.0,,Not applicable,26-03-2019,25-04-2024,Church Street,Shoreham,,Sevenoaks,Kent,TN14 7SN,www.shorehamvillageschool.net,1959522228.0,Mrs,Gillian,Lovatt-Young,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Otford and Shoreham,Sevenoaks,(England/Wales) Rural village,E10000016,551878.0,161599.0,Sevenoaks 008,Sevenoaks 008E,,,,,Good,South-East England and South London,,50002000337.0,,Not applicable,Not applicable,,,E02005094,E01024452,18.0, +118285,886,Kent,2155,Slade Primary School and Attached Unit for Children with Hearing Impairment,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,418.0,223.0,195.0,13.2,Not applicable,,Not applicable,,Not under a federation,,10072996.0,,Not applicable,06-03-2024,22-05-2024,The Slade,,,Tonbridge,Kent,TN9 1HR,www.slade.kent.sch.uk,1732350354.0,Mrs,Karen,Slade,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,,,,,,,,,,,,,Resourced provision,10.0,10.0,,,South East,Tonbridge and Malling,Judd,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558843.0,146765.0,Tonbridge and Malling 012,Tonbridge and Malling 012A,,,,,Good,South-East England and South London,,200000962234.0,,Not applicable,Not applicable,,,E02005160,E01024732,55.0, +118286,886,Kent,2156,Sussex Road Community Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,418.0,209.0,209.0,13.9,Not applicable,,Not applicable,,Not under a federation,,10072995.0,,Not applicable,24-11-2021,08-05-2024,Sussex Road,,,Tonbridge,Kent,TN9 2TP,http://www.sussex-road.kent.sch.uk/,1732352367.0,Mrs,Sarah,Miles,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tonbridge and Malling,Judd,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558245.0,145866.0,Tonbridge and Malling 013,Tonbridge and Malling 013A,,,,,Good,South-East England and South London,,200000961981.0,,Not applicable,Not applicable,,,E02005161,E01024757,58.0, +118288,886,Kent,2161,Boughton Monchelsea Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,206.0,102.0,104.0,19.4,Not applicable,,Not applicable,,Not under a federation,,10073356.0,,Not applicable,29-09-2023,22-04-2024,Church Hill,Boughton Monchelsea,,Maidstone,Kent,ME17 4HP,http://www.boughton-monchelsea.kent.sch.uk,1622743596.0,Mrs,Mandy,Gibbs,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,Boughton Monchelsea and Chart Sutton,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,576889.0,150785.0,Maidstone 012,Maidstone 012A,,,,,Good,South-East England and South London,,200003674284.0,,Not applicable,Not applicable,,,E02005079,E01024331,40.0, +118289,886,Kent,2163,East Farleigh Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,208.0,104.0,104.0,13.0,Not applicable,,Not applicable,,Not under a federation,,10073355.0,,Not applicable,22-06-2022,23-04-2024,Vicarage Lane,East Farleigh,,Maidstone,Kent,ME15 0LY,www.east-farleigh.kent.sch.uk,1622726364.0,Mr,Peter,Goodman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Coxheath and Hunton,Maidstone and The Weald,(England/Wales) Rural village,E10000016,573555.0,152821.0,Maidstone 016,Maidstone 016B,,,,,Good,South-East England and South London,,200003673168.0,,Not applicable,Not applicable,,,E02005083,E01024343,27.0, +118290,886,Kent,2164,East Peckham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,175.0,99.0,76.0,14.3,Not applicable,,Not applicable,,Not under a federation,,10073354.0,,Not applicable,04-07-2023,14-05-2024,130 Pound Road,East Peckham,,Tonbridge,Kent,TN12 5LH,http://www.east-peckham.kent.sch.uk/,1622871268.0,Mrs,Kate,Elliott,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"East and West Peckham, Mereworth & Wateringbury",Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,566468.0,149067.0,Tonbridge and Malling 008,Tonbridge and Malling 008B,,,,,Requires improvement,South-East England and South London,,100062628528.0,,Not applicable,Not applicable,,,E02005156,E01024744,25.0, +118291,886,Kent,2165,Headcorn Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,392.0,210.0,182.0,20.2,Not applicable,,Not applicable,,Not under a federation,,10073353.0,,Not applicable,05-05-2022,02-05-2024,Kings Road,,,Headcorn,Kent,TN27 9QT,www.headcorn.kent.sch.uk,1622891289.0,Miss,Sarah,Symonds,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Headcorn,Faversham and Mid Kent,(England/Wales) Rural town and fringe,E10000016,583351.0,144445.0,Maidstone 017,Maidstone 017B,,,,,Requires improvement,South-East England and South London,,200003698713.0,,Not applicable,Not applicable,,,E02005084,E01024365,79.0, +118292,886,Kent,2166,Hollingbourne Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,96.0,48.0,48.0,24.0,Not applicable,,Not applicable,,Not under a federation,,10073352.0,,Not applicable,01-03-2022,12-03-2024,Eyhorne Street,Hollingbourne,,Maidstone,Kent,ME17 1UA,www.hollingbourne.kent.sch.uk/,1622880270.0,Mrs,Helen,Bradley-Wyatt,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,North Downs,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,584016.0,154881.0,Maidstone 011,Maidstone 011D,,,,,Good,South-East England and South London,,200003719238.0,,Not applicable,Not applicable,,,E02005078,E01024387,23.0, +118293,886,Kent,2167,Ightham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,209.0,105.0,104.0,3.3,Not applicable,,Not applicable,,Not under a federation,,10073351.0,,Not applicable,04-03-2020,07-05-2024,Oldbury Lane,Ightham,,Sevenoaks,Kent,TN15 9DD,www.ightham.kent.sch.uk,1732882405.0,Mr,David,Sherhod,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Pilgrims with Ightham,Tonbridge and Malling,(England/Wales) Rural village,E10000016,558908.0,156539.0,Tonbridge and Malling 006,Tonbridge and Malling 006E,,,,,Outstanding,South-East England and South London,,100062551218.0,,Not applicable,Not applicable,,,E02005154,E01024755,7.0, +118294,886,Kent,2168,Lenham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,210.0,No Special Classes,19-01-2023,213.0,104.0,109.0,21.1,Not applicable,,Not applicable,,Not under a federation,,10075877.0,,Not applicable,06-12-2023,13-05-2024,Ham Lane,Lenham,,Maidstone,Kent,ME17 2LL,http://www.lenham.kent.sch.uk,1622858260.0,Mrs,Andrea,McCluskey,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Harrietsham and Lenham,Faversham and Mid Kent,(England/Wales) Rural town and fringe,E10000016,589472.0,152164.0,Maidstone 011,Maidstone 011B,,,,,Good,South-East England and South London,,200003719333.0,,Not applicable,Not applicable,,,E02005078,E01024362,45.0, +118295,886,Kent,2169,Platts Heath Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,91.0,No Special Classes,19-01-2023,62.0,32.0,30.0,29.0,Not applicable,,Not applicable,,Not under a federation,,10073350.0,,Not applicable,21-04-2022,21-05-2024,Headcorn Road,Platts Heath,,Maidstone,Kent,ME17 2NH,https://plattsheathkentsch.co.uk/,1622850316.0,Mr,Darren,Waters,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Harrietsham and Lenham,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,587729.0,150533.0,Maidstone 011,Maidstone 011B,,,,,Good,South-East England and South London,,200003703748.0,,Not applicable,Not applicable,,,E02005078,E01024362,18.0, +118297,886,Kent,2171,Brunswick House Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,421.0,No Special Classes,19-01-2023,421.0,207.0,214.0,25.2,Not applicable,,Not applicable,,Not under a federation,,10069518.0,,Not applicable,19-07-2023,21-05-2024,Leafy Lane,,,Maidstone,Kent,ME16 0QQ,http://www.brunswick-house.kent.sch.uk,1622752102.0,Mrs,Wendy,Skinner,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bridge,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575123.0,156195.0,Maidstone 006,Maidstone 006B,,,,,Good,South-East England and South London,,200003659753.0,,Not applicable,Not applicable,,,E02005073,E01024340,106.0, +118301,886,Kent,2175,North Borough Junior School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,353.0,201.0,152.0,28.6,Not applicable,,Not applicable,,Supported by a federation,St Paul's and North Borough Schools Federation,10079035.0,,Not applicable,19-10-2023,25-04-2024,"North Borough Junior School, Peel Street",,,Maidstone,Kent,ME14 2BP,www.north-borough.kent.sch.uk/,1622754708.0,Mrs,Dawn,Wakefield,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,North,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576137.0,156985.0,Maidstone 004,Maidstone 004C,,,,,Good,South-East England and South London,,200003704716.0,,Not applicable,Not applicable,,,E02005071,E01024382,101.0, +118302,886,Kent,2176,Park Way Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,315.0,No Special Classes,19-01-2023,316.0,153.0,163.0,26.3,Not applicable,,Not applicable,,Not under a federation,,10073349.0,,Not applicable,13-11-2018,12-09-2023,Park Way,,,Maidstone,Kent,ME15 7AH,http://www.park-way.kent.sch.uk,1622753651.0,Mrs,Karen,Dhanecha,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Shepway North,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,576965.0,154343.0,Maidstone 010,Maidstone 010B,,,,,Good,South-East England and South London,,200003686775.0,,Not applicable,Not applicable,,,E02005077,E01024392,83.0, +118307,886,Kent,2185,Mereworth Community Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,218.0,No Special Classes,19-01-2023,203.0,117.0,86.0,16.7,Not applicable,,Not applicable,,Not under a federation,,10069517.0,,Not applicable,07-07-2022,21-05-2024,39 the Street,Mereworth,,Maidstone,Kent,ME18 5ND,www.mereworth.kent.sch.uk,1622812569.0,Miss,Amanda,Lavelle,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"East and West Peckham, Mereworth & Wateringbury",Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,565535.0,153533.0,Tonbridge and Malling 007,Tonbridge and Malling 007A,,,,,Good,South-East England and South London,,100062386957.0,,Not applicable,Not applicable,,,E02005155,E01024746,34.0, +118308,886,Kent,2187,Offham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,205.0,100.0,105.0,5.9,Not applicable,,Not applicable,,Not under a federation,,10073347.0,,Not applicable,20-05-2015,23-01-2024,Church Road,Offham,,West Malling,Kent,ME19 5NX,http://www.offham.kent.sch.uk,1732842355.0,Mrs,Emily,John,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"East Malling, West Malling & Offham",Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,565935.0,157744.0,Tonbridge and Malling 014,Tonbridge and Malling 014E,,,,,Outstanding,South-East England and South London,,10002907519.0,,Not applicable,Not applicable,,,E02006833,E01032620,12.0, +118309,886,Kent,2188,Plaxtol Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,109.0,No Special Classes,19-01-2023,98.0,49.0,49.0,10.2,Not applicable,,Not applicable,,Not under a federation,,10073346.0,,Not applicable,21-03-2023,21-05-2024,School Lane,Plaxtol,,Sevenoaks,Kent,TN15 0QD,http://www.plaxtol.kent.sch.uk/,1732810200.0,Mrs,Claire,Rowley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Bourne,Tonbridge and Malling,(England/Wales) Rural village,E10000016,560235.0,153454.0,Tonbridge and Malling 006,Tonbridge and Malling 006A,,,,,Good,South-East England and South London,,200000959103.0,,Not applicable,Not applicable,,,E02005154,E01024723,10.0, +118310,886,Kent,2189,Ryarsh Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,210.0,107.0,103.0,8.1,Not applicable,,Not applicable,,Not under a federation,,10073345.0,,Not applicable,26-04-2012,03-06-2024,Birling Road,Ryarsh,,West Malling,Kent,ME19 5LS,www.ryarsh.kent.sch.uk/,1732870600.0,Mr,Daniel,Childs,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"Birling, Leybourne & Ryarsh",Tonbridge and Malling,(England/Wales) Rural village,E10000016,567182.0,159943.0,Tonbridge and Malling 014,Tonbridge and Malling 014F,,,,,Outstanding,South-East England and South London,,200000959182.0,,Not applicable,Not applicable,,,E02006833,E01032829,17.0, +118311,886,Kent,2190,Shipbourne School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,59.0,No Special Classes,19-01-2023,60.0,28.0,32.0,18.3,Not applicable,,Not applicable,,Supported by a federation,The Bourne Partnership,10073344.0,,Not applicable,28-03-2019,25-04-2024,Stumble Hill,Shipbourne,,Tonbridge,Kent,TN11 9PB,http://www.shipbourne.kent.sch.uk,1732810344.0,Mrs,Terri,Daters,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Bourne,Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,559185.0,151948.0,Tonbridge and Malling 006,Tonbridge and Malling 006A,,,,,Good,South-East England and South London,,200000966854.0,,Not applicable,Not applicable,,,E02005154,E01024723,11.0, +118313,886,Kent,2192,Staplehurst School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,525.0,No Special Classes,19-01-2023,406.0,194.0,212.0,19.5,Not applicable,,Not applicable,,Not under a federation,,10073343.0,,Not applicable,26-01-2022,17-04-2024,Gybbon Rise,Staplehurst,,Tonbridge,Kent,TN12 0LZ,www.staplehurstschool.co.uk/,1580891765.0,Miss,Lucy,Davenport,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Staplehurst,Maidstone and The Weald,(England/Wales) Rural town and fringe,E10000016,578439.0,143382.0,Maidstone 019,Maidstone 019D,,,,,Good,South-East England and South London,,200003679143.0,,Not applicable,Not applicable,,,E02005086,E01024409,79.0, +118314,886,Kent,2193,Sutton Valence Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,205.0,100.0,105.0,20.5,Not applicable,,Not applicable,,Not under a federation,,10073342.0,,Not applicable,29-03-2023,09-05-2024,North Street,Sutton Valence,,Maidstone,Kent,ME17 3HT,http://www.sutton-valence.kent.sch.uk,1622842188.0,Mrs,Rebecca,Latham-Parsons,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Sutton Valence and Langley,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,581028.0,149379.0,Maidstone 017,Maidstone 017D,,,,,Good,South-East England and South London,,200003720509.0,,Not applicable,Not applicable,,,E02005084,E01024411,42.0, +118336,886,Kent,2226,Eastling Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,101.0,39.0,62.0,20.8,Not applicable,,Not applicable,,Not under a federation,,10073341.0,,Not applicable,20-10-2021,04-06-2024,Kettle Hill Road,Eastling,,Faversham,Kent,ME13 0BA,http://www.eastling.kent.sch.uk,1795890252.0,Mrs,Melanie,Dale,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,East Downs,Faversham and Mid Kent,(England/Wales) Rural hamlet and isolated dwellings,E10000016,596524.0,156325.0,Swale 016,Swale 016B,,,,,Good,South-East England and South London,,200002532821.0,,Not applicable,Not applicable,,,E02005130,E01024566,21.0, +118337,886,Kent,2227,Ethelbert Road Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,209.0,108.0,101.0,5.7,Not applicable,,Not applicable,,Not under a federation,,10073340.0,,Not applicable,01-10-2014,29-05-2024,Ethelbert Road,,,Faversham,Kent,ME13 8SQ,www.ethelbert-road.kent.sch.uk,1795533124.0,Mrs,Michele,Kirkbride,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Watling,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,601072.0,160785.0,Swale 014,Swale 014E,,,,,Outstanding,South-East England and South London,,10035063205.0,,Not applicable,Not applicable,,,E02005128,E01024626,12.0, +118338,886,Kent,2228,Davington Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,417.0,208.0,209.0,21.8,Not applicable,,Not applicable,,Not under a federation,,10069515.0,,Not applicable,22-06-2023,18-03-2024,Priory Row,Davington,,Faversham,Kent,ME13 7EQ,www.davington.kent.sch.uk/,1795532401.0,Mr,Chilton,Saint,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Priory,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,601089.0,161986.0,Swale 015,Swale 015D,,,,,Good,South-East England and South London,,100062379818.0,,Not applicable,Not applicable,,,E02005129,E01024563,91.0, +118341,886,Kent,2231,Lower Halstow Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,188.0,94.0,94.0,13.3,Not applicable,,Not applicable,,Supported by a federation,The Federation of Lower Halstow Primary and Newington CofE Primary School,10073339.0,,Not applicable,14-03-2019,04-06-2024,School Lane,Lower Halstow,,Sittingbourne,Kent,ME9 7ES,www.lower-halstow.kent.sch.uk,1795842344.0,Mrs,Tara,Deevoy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,"Bobbing, Iwade and Lower Halstow",Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,585951.0,166826.0,Swale 008,Swale 008E,,,,,Good,South-East England and South London,,200002532824.0,,Not applicable,Not applicable,,,E02005122,E01024575,25.0, +118346,886,Kent,2239,Rodmersham School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,70.0,No Special Classes,19-01-2023,117.0,59.0,58.0,3.4,Not applicable,,Not applicable,,Not under a federation,,10073338.0,,Not applicable,22-09-2011,24-04-2024,Rodmersham Green,,,Sittingbourne,Kent,ME9 0PS,www.rodmersham.kent.sch.uk/,1795423776.0,Mrs,Nicola,McMullon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,West Downs,Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,591601.0,161273.0,Swale 013,Swale 013C,,,,,Outstanding,South-East England and South London,,100062397234.0,,Not applicable,Not applicable,,,E02005127,E01024628,4.0, +118348,886,Kent,2245,Rose Street Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,438.0,225.0,213.0,59.6,Not applicable,,Not applicable,,Supported by a federation,Sheerness West Federation,10076228.0,,Not applicable,30-11-2022,14-05-2024,Rose Street,,,Sheerness,Kent,ME12 1AW,www.swfed.co.uk,1795663012.0,Mrs,Samantha,Mackay,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheerness,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591895.0,174664.0,Swale 002,Swale 002A,,,,,Requires improvement,South-East England and South London,,100062377402.0,,Not applicable,Not applicable,,,E02005116,E01024613,249.0, +118354,886,Kent,2254,Canterbury Road Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,208.0,103.0,105.0,32.7,Not applicable,,Not applicable,,Not under a federation,,10073337.0,,Not applicable,06-03-2024,22-05-2024,School Road,,,Sittingbourne,Kent,ME10 4SE,http://www.canterbury-road.kent.sch.uk,1795423818.0,Mr,Timothy,Pye,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Roman,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591591.0,163352.0,Swale 010,Swale 010D,,,,,Good,South-East England and South London,,200002531732.0,,Not applicable,Not applicable,,,E02005124,E01024599,68.0, +118356,886,Kent,2258,Blean Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,469.0,Not applicable,19-01-2023,431.0,215.0,216.0,6.7,Not applicable,,Not applicable,,Not under a federation,,10073336.0,,Not applicable,09-03-2022,07-05-2024,Whitstable Road,Blean,,Canterbury,Kent,CT2 9ED,www.bleanprimary.org.uk/,1227471254.0,Mr,Ian,Rowden,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Blean Forest,Canterbury,(England/Wales) Urban city and town,E10000016,612837.0,159932.0,Canterbury 012,Canterbury 012H,,,,,Outstanding,South-East England and South London,,200000683406.0,,Not applicable,Not applicable,,,E02005021,E01035309,29.0, +118359,886,Kent,2263,Herne Bay Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,2.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,304.0,154.0,150.0,35.7,Not applicable,,Not applicable,,Not under a federation,,10074072.0,,Not applicable,04-12-2019,17-01-2024,Stanley Road,,,Herne Bay,Kent,CT6 5SH,www.herne-bay.kent.sch.uk/,1227372245.0,Ms,Nicky,Brown,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Heron,North Thanet,(England/Wales) Urban city and town,E10000016,617893.0,167868.0,Canterbury 003,Canterbury 003D,,,,,Good,South-East England and South London,,200000682558.0,,Not applicable,Not applicable,,,E02005012,E01024083,99.0, +118361,886,Kent,2265,Hoath Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,103.0,45.0,58.0,16.5,Not applicable,,Not applicable,,Supported by a federation,The Federation of Chislet CE and Hoath Primary Schools,10073335.0,,Not applicable,18-01-2022,12-01-2024,School Lane,Hoath,,Canterbury,Kent,CT3 4LA,www.chislethoathfederation.co.uk,1227860249.0,Mr,Tim,Whitehouse,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Reculver,North Thanet,(England/Wales) Rural hamlet and isolated dwellings,E10000016,620491.0,164365.0,Canterbury 010,Canterbury 010C,,,,,Good,South-East England and South London,,10033152360.0,,Not applicable,Not applicable,,,E02005019,E01024086,17.0, +118363,886,Kent,2268,Westmeads Community Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,156.0,84.0,72.0,23.1,Not applicable,,Not applicable,,Not under a federation,,10074071.0,,Not applicable,18-05-2022,03-06-2024,Cromwell Road,,,Whitstable,Kent,CT5 1NA,www.westmeads.kent.sch.uk/,1227272995.0,Ms,Kirsty,White,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Gorrell,Canterbury,(England/Wales) Urban city and town,E10000016,611013.0,166711.0,Canterbury 008,Canterbury 008D,,,,,Requires improvement,South-East England and South London,,100062300538.0,,Not applicable,Not applicable,,,E02005017,E01024072,36.0, +118364,886,Kent,2269,Whitstable Junior School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,255.0,No Special Classes,19-01-2023,232.0,126.0,106.0,28.9,Supported by a trust,The Coastal Alliance Co-operative Trust,Not applicable,,Not under a federation,,10077448.0,,Not applicable,18-06-2019,02-04-2024,Oxford Street,,,Whitstable,Kent,CT5 1DB,www.whitstable-junior.kent.sch.uk/,1227272385.0,Ms,Sarah,Kent,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Gorrell,Canterbury,(England/Wales) Urban city and town,E10000016,610711.0,166216.0,Canterbury 008,Canterbury 008C,,,,,Good,South-East England and South London,,100062300650.0,,Not applicable,Not applicable,,,E02005017,E01024070,67.0, +118365,886,Kent,2270,Aldington Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,191.0,112.0,79.0,11.0,Supported by a trust,CARE Foundation Trust,Not applicable,,Not under a federation,,10073112.0,,Not applicable,07-02-2024,22-05-2024,Roman Road,Aldington,,Ashford,Kent,TN25 7EE,http://www.aldington.kent.sch.uk/,1233720247.0,Mr,Ben,Dawson.,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Saxon Shore,Folkestone and Hythe,(England/Wales) Rural village,E10000016,606435.0,136475.0,Ashford 010,Ashford 010A,,,,,Good,South-East England and South London,,100062562493.0,,Not applicable,Not applicable,,,E02005005,E01024013,21.0, +118369,886,Kent,2275,Victoria Road Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,206.0,No Special Classes,19-01-2023,211.0,113.0,98.0,25.6,Not applicable,,Not applicable,,Not under a federation,,10069513.0,,Not applicable,15-01-2019,25-04-2024,Victoria Road,,,Ashford,Kent,TN23 7HQ,http://www.victoriaroad.co.uk/,1233620044.0,Mrs,Kelly,Collens,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Victoria,Ashford,(England/Wales) Urban city and town,E10000016,600804.0,142343.0,Ashford 005,Ashford 005F,,,,,Good,South-East England and South London,,100062048541.0,,Not applicable,Not applicable,,,E02005000,E01034985,54.0, +118370,886,Kent,2276,Willesborough Infant School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,390.0,No Special Classes,19-01-2023,357.0,177.0,180.0,15.7,Not supported by a trust,,Not applicable,,Supported by a federation,The Willesborough Schools,10080431.0,,Not applicable,15-09-2022,08-05-2024,Church Road,Willesborough,,Ashford,Kent,TN24 0JZ,www.willesborough-infant.kent.sch.uk/,1233624165.0,Mr,Tom,Head,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,,,,,South East,Ashford,Highfield,Ashford,(England/Wales) Urban city and town,E10000016,603174.0,141786.0,Ashford 006,Ashford 006B,,,,,Good,South-East England and South London,,100062560396.0,,Not applicable,Not applicable,,,E02005001,E01023995,56.0, +118371,886,Kent,5226,Willesborough Junior School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,510.0,No Special Classes,19-01-2023,511.0,249.0,262.0,18.8,Not supported by a trust,,Not applicable,,Supported by a federation,The Willesborough Schools,10069649.0,,Not applicable,22-03-2023,12-09-2023,Highfield Road,Willesborough,,Ashford,Kent,TN24 0JU,www.willesborough-js.kent.sch.uk/,1233620405.0,Mr,Tom,Head,Interim Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Highfield,Ashford,(England/Wales) Urban city and town,E10000016,603107.0,141715.0,Ashford 006,Ashford 006B,,,,,Good,South-East England and South London,,200002904249.0,,Not applicable,Not applicable,,,E02005001,E01023995,96.0, +118372,886,Kent,2278,Bethersden Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,140.0,No Special Classes,19-01-2023,131.0,70.0,61.0,25.4,Not applicable,,Not applicable,,Not under a federation,,10073334.0,,Not applicable,08-06-2023,21-05-2024,School Road,Bethersden,,Ashford,Kent,TN26 3AH,www.bethersden.kent.sch.uk/,1233820479.0,Ms,Rebecca,Heaton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Weald Central,Ashford,(England/Wales) Rural village,E10000016,592556.0,140108.0,Ashford 012,Ashford 012B,,,,,Good,South-East England and South London,,100062563194.0,,Not applicable,Not applicable,,,E02005007,E01024031,33.0, +118373,886,Kent,2279,Brook Community Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,71.0,40.0,31.0,12.7,Supported by a trust,CARE Foundation Trust,Not applicable,,Not under a federation,,10073111.0,,Not applicable,11-05-2023,22-04-2024,Spelders Hill,Brook,,Ashford,Kent,TN25 5PB,www.brook-ashford.kent.sch.uk,1233812614.0,Mrs,Ellen,Ranson-McCabe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Bircholt,Folkestone and Hythe,(England/Wales) Rural village,E10000016,606030.0,143546.0,Ashford 010,Ashford 010C,,,,,Good,South-East England and South London,,100062561999.0,,Not applicable,Not applicable,,,E02005005,E01024015,9.0, +118374,886,Kent,2280,Challock Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,210.0,100.0,110.0,7.6,Supported by a trust,CARE Foundation Trust,Not applicable,,Not under a federation,,10073110.0,,Not applicable,12-07-2023,13-05-2024,Church Lane,Challock,,Ashford,Kent,TN25 4BU,www.challockprimaryschool.co.uk,1233740286.0,Mrs,Susan,Sweet,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Downs West,Ashford,(England/Wales) Rural village,E10000016,600941.0,150288.0,Ashford 002,Ashford 002C,,,,,Outstanding,South-East England and South London,,100062561620.0,,Not applicable,Not applicable,,,E02004997,E01023989,16.0, +118375,886,Kent,2282,Great Chart Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,420.0,219.0,201.0,17.6,Not applicable,,Not applicable,,Not under a federation,,10073333.0,,Not applicable,07-06-2023,03-06-2024,Hoxton Close,Singleton,,Ashford,Kent,TN23 5LB,www.great-chart.kent.sch.uk/,1233620040.0,Mrs,Wendy,Pang,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Singleton East,Ashford,(England/Wales) Urban city and town,E10000016,598848.0,141528.0,Ashford 007,Ashford 007E,,,,,Outstanding,South-East England and South London,,100062617920.0,,Not applicable,Not applicable,,,E02005002,E01024017,74.0, +118377,886,Kent,2285,Mersham Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,181.0,93.0,88.0,14.4,Supported by a trust,CARE Foundation Trust,Not applicable,,Not under a federation,,10073109.0,,Not applicable,23-02-2022,12-04-2024,Church Road,Mersham,,Ashford,Kent,TN25 6NU,www.mersham.kent.sch.uk/,1233720449.0,Mrs,Cheryl,Chalkley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,"Mersham, Sevington South with Finberry",Ashford,(England/Wales) Rural village,E10000016,605060.0,139253.0,Ashford 010,Ashford 010F,,,,,Good,South-East England and South London,,100062562340.0,,Not applicable,Not applicable,,,E02005005,E01034988,26.0, +118381,886,Kent,2289,Smeeth Community Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,140.0,No Special Classes,19-01-2023,122.0,66.0,56.0,13.1,Supported by a trust,CARE Foundation Trust,Not applicable,,Not under a federation,,10073108.0,,Not applicable,04-07-2023,03-11-2023,Caroland Close,Smeeth,,Ashford,Kent,TN25 6RX,www.smeeth.kent.sch.uk,1303813128.0,Ms,Jennifer,Payne,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Bircholt,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,607714.0,140096.0,Ashford 010,Ashford 010A,,,,,Good,South-East England and South London,,100062562341.0,,Not applicable,Not applicable,,,E02005005,E01024013,16.0, +118385,886,Kent,2298,Hawkinge Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,396.0,189.0,207.0,17.9,Not supported by a trust,,Not applicable,,Not under a federation,,10073107.0,,Not applicable,12-06-2019,03-05-2024,Canterbury Road,Hawkinge,,Folkestone,Kent,CT18 7BN,www.hawkingeprimaryschool.co.uk/,1303892224.0,Miss,Aly,Ward,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs East,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,621648.0,139892.0,Folkestone and Hythe 002,Folkestone and Hythe 002B,,,,,Outstanding,South-East England and South London,,50034845.0,,Not applicable,Not applicable,,,E02005103,E01024541,71.0, +118387,886,Kent,2300,Sellindge Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,142.0,61.0,81.0,13.4,Not applicable,,Not applicable,,Not under a federation,,10073331.0,,Not applicable,03-02-2023,22-03-2024,Main Road,Sellindge,,Ashford,Kent,TN25 6JY,www.sellindge-ashford.co.uk/,1303812073.0,Miss,Joanne,Wren,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs West,Folkestone and Hythe,(England/Wales) Rural village,E10000016,610305.0,138188.0,Folkestone and Hythe 009,Folkestone and Hythe 009D,,,,,Good,South-East England and South London,,50010667.0,,Not applicable,Not applicable,,,E02005110,E01024546,19.0, +118393,886,Kent,2312,River Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,427.0,Not applicable,19-01-2023,413.0,210.0,203.0,18.4,Not applicable,,Not applicable,,Supported by a federation,Lydden and River Primary Schools Federation,10072994.0,,Not applicable,29-11-2013,24-01-2024,Lewisham Road,River,,Dover,Kent,CT17 0PP,www.river.kent.sch.uk/,1304822516.0,Mr,Neil,Brinicombe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,12.0,12.0,,,South East,Dover,Dover Downs & River,Dover,(England/Wales) Urban city and town,E10000016,629160.0,143403.0,Dover 010,Dover 010C,,,,,Outstanding,South-East England and South London,,100062290036.0,,Not applicable,Not applicable,,,E02005050,E01024233,76.0, +118398,886,Kent,2318,Langdon Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,86.0,51.0,35.0,22.1,Not applicable,,Not applicable,,Not under a federation,,10073330.0,,Not applicable,28-01-2020,07-05-2024,East Langdon,,The Street,Dover,Kent,CT15 5JQ,www.langdonprimaryschool.co.uk,1304852600.0,Mrs,Lynn,Paylor Sutton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Guston, Kingsdown & St Margaret's-at-Cliffe",Dover,(England/Wales) Rural village,E10000016,633397.0,146333.0,Dover 012,Dover 012B,,,,,Good,South-East England and South London,,100062287609.0,,Not applicable,Not applicable,,,E02005052,E01024238,19.0, +118399,886,Kent,2320,Eythorne Elvington Community Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,140.0,No Special Classes,19-01-2023,107.0,47.0,60.0,52.3,Not applicable,,Not applicable,,Supported by a federation,The Federation of Shepherdswell Church of England and Eythorne Elvington,10069511.0,,Not applicable,14-12-2022,17-05-2024,Adelaide Road,Eythorne,,Dover,Kent,CT15 4AN,www.eythorne-elvington.kent.sch.uk,1304830376.0,Mr,N,Garvey,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Aylesham, Eythorne & Shepherdswell",Dover,(England/Wales) Rural town and fringe,E10000016,628011.0,149789.0,Dover 006,Dover 006D,,,,,Outstanding,South-East England and South London,,10034882456.0,,Not applicable,Not applicable,,,E02005046,E01024204,56.0, +118400,886,Kent,2321,Lydden Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,84.0,No Special Classes,19-01-2023,78.0,37.0,41.0,11.7,Not applicable,,Not applicable,,Supported by a federation,Lydden and River Primary Schools Federation,10073329.0,,Not applicable,05-02-2019,26-03-2024,Stonehall,Lydden,,Dover,Kent,CT15 7LA,http://www.lydden.kent.sch.uk,1304822887.0,Mr,Neil,Brinicombe,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Dover Downs & River,Dover,(England/Wales) Rural village,E10000016,626743.0,145568.0,Dover 010,Dover 010G,,,,,Good,South-East England and South London,,100062288383.0,,Not applicable,Not applicable,,,E02005050,E01033210,9.0, +118401,886,Kent,2322,Preston Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,133.0,No Special Classes,19-01-2023,117.0,58.0,59.0,7.7,Not applicable,,Not applicable,,Supported by a federation,The Federation of Preston and Wingham,10073328.0,,Not applicable,01-02-2024,20-05-2024,Mill Lane,Preston,,Canterbury,Kent,CT3 1HB,www.prestonprimary.org.uk,1227722235.0,Mrs,Helen,Clements,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Little Stour & Ashstone,South Thanet,(England/Wales) Rural village,E10000016,625140.0,160901.0,Dover 001,Dover 001A,,,,,Good,South-East England and South London,,100062298104.0,,Not applicable,Not applicable,,,E02005041,E01024206,9.0, +118403,886,Kent,2326,Wingham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,191.0,97.0,94.0,15.7,Not applicable,,Not applicable,,Supported by a federation,The Federation of Preston and Wingham,10073327.0,,Not applicable,17-11-2021,18-04-2024,School Lane,Wingham,,Canterbury,Kent,CT3 1BD,www.winghamprimary.org.uk/,1227720277.0,Mrs,Helen,Clements,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Little Stour & Ashstone,South Thanet,(England/Wales) Rural town and fringe,E10000016,624160.0,157221.0,Dover 001,Dover 001D,,,,,Good,South-East England and South London,,100062298256.0,,Not applicable,Not applicable,,,E02005041,E01024209,30.0, +118405,886,Kent,2328,St Mildred's Primary Infant School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,269.0,135.0,134.0,21.9,Supported by a trust,Thanet Endeavour Learning Trust,Not applicable,,Supported by a federation,The Federation of Bromstone Primary School and St.Mildred's Primary Infant School,10074070.0,,Not applicable,24-11-2021,30-04-2024,St Mildred's Avenue,,,Broadstairs,Kent,CT10 2BX,http://www.st-mildreds.kent.sch.uk,1843862035.0,Headteacher,James,Williams,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Viking,South Thanet,(England/Wales) Urban city and town,E10000016,639125.0,167790.0,Thanet 010,Thanet 010C,,,,,Outstanding,South-East England and South London,,200002882176.0,,Not applicable,Not applicable,,,E02005141,E01024706,59.0, +118406,886,Kent,2329,Callis Grange Nursery and Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,296.0,No Special Classes,19-01-2023,288.0,159.0,129.0,21.2,Not applicable,,Not applicable,,Not under a federation,,10074069.0,,Not applicable,21-04-2022,15-05-2024,Beacon Road,,,Broadstairs,Kent,CT10 3DG,http://www.callis-grange.kent.sch.uk,1843862531.0,Mrs,Vikki,Bowman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Beacon Road,South Thanet,(England/Wales) Urban city and town,E10000016,638675.0,169146.0,Thanet 006,Thanet 006A,,,,,Good,South-East England and South London,,100062281748.0,,Not applicable,Not applicable,,,E02005137,E01024633,61.0, +118411,886,Kent,2337,St Crispin's Community Primary Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,254.0,128.0,126.0,28.3,Not applicable,,Not applicable,,Not under a federation,,10074068.0,,Not applicable,11-09-2019,22-04-2024,St Crispin's Road,,,Westgate-on-Sea,Kent,CT8 8EB,http://www.st-crispinsinfants.org.uk,1843832040.0,Mrs,Louise,Davidson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Westgate-on-Sea,North Thanet,(England/Wales) Urban city and town,E10000016,632291.0,169367.0,Thanet 007,Thanet 007E,,,,,Good,South-East England and South London,,200001851326.0,,Not applicable,Not applicable,,,E02005138,E01024716,72.0, +118414,886,Kent,2340,Ellington Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,134.0,71.0,63.0,47.8,Not applicable,,Not applicable,,Not under a federation,,10074067.0,,Not applicable,19-07-2022,23-04-2024,High Street,St Lawrence,,Ramsgate,Kent,CT11 0QH,www.ellington-infant.kent.sch.uk,1843591638.0,Mr,Adnan,Ahmet,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Central Harbour,South Thanet,(England/Wales) Urban city and town,E10000016,637175.0,165319.0,Thanet 015,Thanet 015A,,,,,Good,South-East England and South London,,200003079680.0,,Not applicable,Not applicable,,,E02005146,E01024645,64.0, +118416,886,Kent,2345,Priory Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,155.0,76.0,79.0,38.1,Not applicable,,Not applicable,,Not under a federation,,10074066.0,,Not applicable,21-06-2023,05-06-2024,Cannon Road,,,Ramsgate,Kent,CT11 9XT,www.priory.kent.sch.uk,1843593105.0,Ms,Tracey,Sandy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Central Harbour,South Thanet,(England/Wales) Urban city and town,E10000016,637770.0,165232.0,Thanet 016,Thanet 016A,,,,,Good,South-East England and South London,,100062283791.0,,Not applicable,Not applicable,,,E02005147,E01024646,59.0, +118436,886,Kent,2431,Shears Green Junior School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,480.0,No Special Classes,19-01-2023,483.0,232.0,251.0,28.0,Supported by a trust,Northfleet Schools Co-Operative Trust,Not applicable,,Not under a federation,,10073106.0,,Not applicable,19-01-2023,03-05-2024,White Avenue,Northfleet,,Gravesend,Kent,DA11 7JB,www.shearsgreenjuniorschool.co.uk,1474567359.0,Mr,Matthew,Paterson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Coldharbour & Perry Street,Gravesham,(England/Wales) Urban major conurbation,E10000016,563816.0,172261.0,Gravesham 009,Gravesham 009A,,,,,Good,South-East England and South London,,100062310707.0,,Not applicable,Not applicable,,,E02005063,E01024264,135.0, +118438,886,Kent,2434,West Minster Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,570.0,Has Special Classes,19-01-2023,508.0,253.0,255.0,56.6,Not applicable,,Not applicable,,Supported by a federation,Sheerness West Federation,10076225.0,,Not applicable,01-12-2021,14-05-2024,St George's Avenue,,,Sheerness,Kent,ME12 1ET,www.swfed.co.uk,1795662178.0,Ms,Hazel,Brewer,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheerness,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591524.0,173868.0,Swale 002,Swale 002C,,,,,Good,South-East England and South London,,100062626557.0,,Not applicable,Not applicable,,,E02005116,E01024615,269.0, +118449,886,Kent,2454,Aycliffe Community Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,135.0,No Special Classes,19-01-2023,93.0,54.0,39.0,61.3,Not applicable,,Not applicable,,Not under a federation,,10073325.0,,Not applicable,01-12-2022,19-04-2024,St David's Avenue,,,Dover,Kent,CT17 9HJ,www.aycliffecpschool.co.uk,1304202651.0,Mrs,Jacky,Cox,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Town & Castle,Dover,(England/Wales) Urban city and town,E10000016,630152.0,139911.0,Dover 013,Dover 013E,,,,,Good,South-East England and South London,,200001851339.0,,Not applicable,Not applicable,,,E02005053,E01024249,57.0, +118453,886,Kent,2459,Riverhead Infants' School,Community school,Local authority maintained schools,Open,Not applicable,26-02-1997,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,268.0,140.0,128.0,4.5,Not applicable,,Not applicable,,Not under a federation,,10080749.0,,Not applicable,22-03-2023,24-01-2024,Worships Hill,,,Riverhead,Kent,TN13 2AS,http://www.riverhead.kent.sch.uk,1732452475.0,Mr,Andrew,King,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Dunton Green and Riverhead,Sevenoaks,(England/Wales) Urban city and town,E10000016,551308.0,156016.0,Sevenoaks 011,Sevenoaks 011C,,,,,Good,South-East England and South London,,50002018912.0,,Not applicable,Not applicable,,,E02005097,E01024424,12.0, +118456,886,Kent,2465,Claremont Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,436.0,No Special Classes,19-01-2023,438.0,239.0,199.0,2.7,Not applicable,,Not applicable,,Not under a federation,,10069510.0,,Not applicable,12-01-2023,24-05-2024,Banner Farm Road,,,Tunbridge Wells,Kent,TN2 5EB,www.claremont.kent.sch.uk,1892531395.0,Mrs,Candi,Roberts,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Pantiles and St Mark's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558955.0,138862.0,Tunbridge Wells 012,Tunbridge Wells 012E,,,,,Good,South-East England and South London,,100062554871.0,,Not applicable,Not applicable,,,E02005173,E01024820,12.0, +118459,886,Kent,2471,Whitfield Aspen School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,591.0,Has Special Classes,19-01-2023,585.0,329.0,256.0,36.1,Not applicable,,Not applicable,,Not under a federation,,10072993.0,,Not applicable,12-09-2019,02-05-2024,Mayfield Road,Whitfield,,Dover,Kent,CT16 3LJ,www.whitfieldaspenschool.co.uk/,1304821526.0,Mr,Jason,Cook,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,PMLD - Profound and Multiple Learning Difficulty,,,,,,,,,,,,,Resourced provision,168.0,168.0,,,South East,Dover,Whitfield,Dover,(England/Wales) Urban city and town,E10000016,630324.0,145151.0,Dover 010,Dover 010F,,,,,Good,South-East England and South London,,100062289581.0,,Not applicable,Not applicable,,,E02005050,E01024256,211.0, +118461,886,Kent,2474,St Paul's Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,261.0,120.0,141.0,30.3,Not applicable,,Not applicable,,Supported by a federation,St Paul's and North Borough Schools Federation,10074064.0,,Not applicable,15-01-2020,24-04-2024,Hillary Road,,,Maidstone,Kent,ME14 2BS,www.stpaulsmaidstone.co.uk,1622753322.0,Mrs,Sarah,Aldridge,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,North,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576222.0,157046.0,Maidstone 004,Maidstone 004D,,,,,Good,South-East England and South London,,200003707812.0,,Not applicable,Not applicable,,,E02005071,E01024383,79.0, +118465,886,Kent,2482,Langton Green Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,414.0,209.0,205.0,5.1,Not applicable,,Not applicable,,Not under a federation,,10073324.0,,Not applicable,20-06-2012,21-05-2024,Lampington Row,Langton Green,,Tunbridge Wells,Kent,TN3 0JG,www.langton-green-school.org,1892862648.0,Mr,Alex,Cornelius,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Speldhurst and Bidborough,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,554343.0,139529.0,Tunbridge Wells 006,Tunbridge Wells 006D,,,,,Outstanding,South-East England and South London,,100062566427.0,,Not applicable,Not applicable,,,E02005167,E01024853,21.0, +118468,886,Kent,2490,Bishops Down Primary and Nursery School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,233.0,121.0,112.0,11.7,Not applicable,,Not applicable,,Not under a federation,,10077232.0,,Not applicable,02-11-2023,16-04-2024,Rydal Drive,,,Tunbridge Wells,Kent,TN4 9SU,www.bishopsdownprimary.org/,1892520114.0,Mrs,Laura,Johnson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,7.0,8.0,,,South East,Tunbridge Wells,Culverden,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,556995.0,140187.0,Tunbridge Wells 007,Tunbridge Wells 007B,,,,,Requires improvement,South-East England and South London,,100062586163.0,,Not applicable,Not applicable,,,E02005168,E01024800,25.0, +118479,886,Kent,2509,Singlewell Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,390.0,No Special Classes,19-01-2023,392.0,178.0,214.0,18.4,Not applicable,,Not applicable,,Not under a federation,,10069509.0,,Not applicable,25-01-2023,03-05-2024,Mackenzie Way,,,Gravesend,Kent,DA12 5TY,www.singlewell.kent.sch.uk,1474569859.0,Mrs,Michelle,Brown,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Singlewell,Gravesham,(England/Wales) Urban major conurbation,E10000016,565793.0,170786.0,Gravesham 011,Gravesham 011B,,,,,Good,South-East England and South London,,100062312820.0,,Not applicable,Not applicable,,,E02005065,E01024304,72.0, +118480,886,Kent,2510,Cheriton Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,389.0,203.0,186.0,24.9,Not supported by a trust,,Not applicable,,Not under a federation,,10073105.0,,Not applicable,30-10-2019,24-04-2024,Church Road,,,Folkestone,Kent,CT20 3EP,http://www.cheritonprimary.org.uk/,1303276112.0,Ms,Sophia,Dover,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,619394.0,136651.0,Folkestone and Hythe 005,Folkestone and Hythe 005C,,,,,Good,South-East England and South London,,50025272.0,,Not applicable,Not applicable,,,E02005106,E01024494,97.0, +118484,886,Kent,2514,Brookfield Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,180.0,85.0,95.0,18.9,Not applicable,,Not applicable,,Supported by a federation,Flourish,10074062.0,,Not applicable,20-04-2023,23-04-2024,Swallow Road,Larkfield,,Aylesford,Kent,ME20 6PY,https://flourish-federation.secure-primarysite.net/,1732840955.0,Miss,C,Smith,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Larkfield,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,570115.0,158700.0,Tonbridge and Malling 003,Tonbridge and Malling 003F,,,,,Good,South-East England and South London,,10002912773.0,,Not applicable,Not applicable,,,E02005151,E01024765,34.0, +118487,886,Kent,2519,Vigo Village School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,142.0,78.0,64.0,15.5,Not applicable,,Not applicable,,Not under a federation,,10073323.0,,Not applicable,06-11-2019,16-05-2024,Erskine Road,Vigo Village,Meopham,Gravesend,Kent,DA13 0RL,http://www.vigo.kent.sch.uk,1732823144.0,Mr,Roger,Barber,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Meopham South & Vigo,Gravesham,(England/Wales) Rural town and fringe,E10000016,564387.0,161691.0,Gravesham 013,Gravesham 013C,,,,,Good,South-East England and South London,,100062312944.0,,Not applicable,Not applicable,,,E02005067,E01024275,22.0, +118488,886,Kent,2520,Madginford Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,654.0,No Special Classes,19-01-2023,642.0,353.0,289.0,10.1,Not applicable,,Not applicable,,Not under a federation,,10073322.0,,Not applicable,20-04-2023,12-09-2023,Egremont Road,Bearsted,,Maidstone,Kent,ME15 8LH,www.madginfordprimaryschool.co.uk,1622734539.0,Mrs,Amanda,Woolcombe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bearsted,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,579076.0,154708.0,Maidstone 007,Maidstone 007B,,,,,Good,South-East England and South London,,200003691655.0,,Not applicable,Not applicable,,,E02005074,E01024327,65.0, +118490,886,Kent,2524,Palmarsh Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,140.0,No Special Classes,19-01-2023,191.0,82.0,109.0,30.9,Not applicable,,Not applicable,,Not under a federation,,10073321.0,,Not applicable,02-10-2019,04-06-2024,St George's Place,,,Hythe,Kent,CT21 6NE,http://www.palmarsh.kent.sch.uk,1303260212.0,Mr,Jamie,Leach,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Hythe Rural,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,613796.0,133711.0,Folkestone and Hythe 010,Folkestone and Hythe 010D,,,,,Good,South-East England and South London,,50013151.0,,Not applicable,Not applicable,,,E02005111,E01024529,59.0, +118491,886,Kent,2525,Painters Ash Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,406.0,203.0,203.0,22.4,Supported by a trust,Northfleet Schools Co-Operative Trust,Not applicable,,Not under a federation,,10073104.0,,Not applicable,01-03-2023,23-05-2024,Masefield Road,Northfleet,,Gravesend,Kent,DA11 8EL,www.paintersashprimary.co.uk,1474568991.0,,Georgina,Salter,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Painters Ash,Gravesham,(England/Wales) Urban major conurbation,E10000016,562691.0,172101.0,Gravesham 006,Gravesham 006E,,,,,Good,South-East England and South London,,100062310909.0,,Not applicable,Not applicable,,,E02005060,E01024288,91.0, +118493,886,Kent,2530,Tunbury Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,587.0,292.0,295.0,7.8,Not applicable,,Not applicable,,Not under a federation,,10073320.0,,Not applicable,08-06-2023,14-05-2024,Tunbury Avenue,Walderslade,,Chatham,Kent,ME5 9HY,www.tunbury.kent.sch.uk,1634863085.0,Mrs,Ruth,Austin,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Walderslade,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,575737.0,162426.0,Tonbridge and Malling 001,Tonbridge and Malling 001D,,,,,Good,South-East England and South London,,100062393390.0,,Not applicable,Not applicable,,,E02005149,E01024722,46.0, +118495,886,Kent,2532,St Margaret's-at-Cliffe Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,189.0,96.0,93.0,9.0,Not applicable,,Not applicable,,Not under a federation,,10069508.0,,Not applicable,03-07-2015,03-06-2024,Sea Street,St Margaret's-At-Cliffe,,Dover,Kent,CT15 6SS,http://www.stmargaretsprimary.co.uk,1304852639.0,Ms,Helen,Comfort,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Guston, Kingsdown & St Margaret's-at-Cliffe",Dover,(England/Wales) Rural town and fringe,E10000016,636125.0,144710.0,Dover 009,Dover 009B,,,,,Outstanding,South-East England and South London,,100062288026.0,,Not applicable,Not applicable,,,E02005049,E01024236,17.0, +118501,886,Kent,2539,Stocks Green Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,218.0,No Special Classes,19-01-2023,216.0,115.0,101.0,5.1,Not applicable,,Not applicable,,Not under a federation,,10069507.0,,Not applicable,07-03-2024,22-05-2024,Leigh Road,Hildenborough,,Tonbridge,Kent,TN11 9AE,www.stocksgreenprimary.co.uk,1732832758.0,Mr,P,Hipkiss,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Hildenborough,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,557065.0,148064.0,Tonbridge and Malling 010,Tonbridge and Malling 010B,,,,,Good,South-East England and South London,,100062628569.0,,Not applicable,Not applicable,,,E02005158,E01024752,11.0, +118505,886,Kent,2545,Sandgate Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,418.0,206.0,212.0,9.8,Not applicable,,Not applicable,,Not under a federation,,10073318.0,,Not applicable,16-09-2021,12-09-2023,Coolinge Lane,,,Folkestone,Kent,CT20 3QU,www.sandgateprimaryschool.co.uk/,1303257280.0,Mr,Matthew,Green,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Sandgate & West Folkestone,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,620849.0,135894.0,Folkestone and Hythe 006,Folkestone and Hythe 006H,,,,,Good,South-East England and South London,,50027156.0,,Not applicable,Not applicable,,,E02005107,E01024521,41.0, +118511,886,Kent,2552,Sandling Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,421.0,226.0,195.0,4.8,Not applicable,,Not applicable,,Not under a federation,,10073317.0,,Not applicable,05-02-2020,07-05-2024,Ashburnham Road,Penenden Heath,,Maidstone,Kent,ME14 2JG,http://www.sandling.kent.sch.uk/,1622763297.0,Miss,Claire,Coombes,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,North,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576624.0,157776.0,Maidstone 002,Maidstone 002D,,,,,Good,South-East England and South London,,10014308132.0,,Not applicable,Not applicable,,,E02005069,E01024385,20.0, +118515,886,Kent,2559,Capel-le-Ferne Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,177.0,99.0,78.0,22.6,Not applicable,,Not applicable,,Not under a federation,,10071996.0,,Not applicable,30-03-2022,21-05-2024,Capel Street,Capel-le-Ferne,,Folkestone,Kent,CT18 7HB,www.capelleferneprimary.co.uk,1303251353.0,Mr,Anthony,Richards,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dover,Alkham & Capel-le-Ferne,Dover,(England/Wales) Rural hamlet and isolated dwellings,E10000016,625026.0,139092.0,Dover 014,Dover 014A,,,,,Good,South-East England and South London,,100062290850.0,,Not applicable,Not applicable,,,E02005054,E01024198,40.0, +118516,886,Kent,2562,Lunsford Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,208.0,104.0,104.0,20.7,Not applicable,,Not applicable,,Not under a federation,,10073316.0,,Not applicable,14-06-2023,01-04-2024,Swallow Road,Larkfield,,Aylesford,Kent,ME20 6PY,http://www.lunsford.kent.sch.uk,1732843352.0,Mr,Gary,Anscombe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Larkfield,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,570068.0,158786.0,Tonbridge and Malling 003,Tonbridge and Malling 003F,,,,,Good,South-East England and South London,,200000966558.0,,Not applicable,Not applicable,,,E02005151,E01024765,43.0, +118523,886,Kent,2574,Downs View Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,260.0,138.0,122.0,16.2,Not applicable,,Not applicable,,Not under a federation,,10080430.0,,Not applicable,18-10-2023,21-05-2024,Ball Lane,Kennington,,Ashford,Kent,TN25 4PJ,www.downs-view.kent.sch.uk/,1233632339.0,Mrs,Tracy Kent,Mrs Sarah Collins,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Kennington,Ashford,(England/Wales) Urban city and town,E10000016,602119.0,145227.0,Ashford 003,Ashford 003B,,,,,Good,South-East England and South London,,100062561615.0,,Not applicable,Not applicable,,,E02004998,E01023999,42.0, +118524,886,Kent,2578,Kingswood Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,140.0,No Special Classes,19-01-2023,129.0,70.0,59.0,10.1,Not applicable,,Not applicable,,Not under a federation,,10073315.0,,Not applicable,19-07-2022,20-11-2023,Cayser Drive,Kingswood,,Maidstone,Kent,ME17 3QF,www.kingswoodkentsch.co.uk,1622842674.0,Mrs,Lynsey,Sanchez Daviu,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Leeds,Faversham and Mid Kent,(England/Wales) Rural town and fringe,E10000016,584054.0,150831.0,Maidstone 015,Maidstone 015C,,,,,Good,South-East England and South London,,10022897422.0,,Not applicable,Not applicable,,,E02005082,E01024375,13.0, +118526,886,Kent,2586,Senacre Wood Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,212.0,109.0,103.0,32.1,Not applicable,,Not applicable,,Not under a federation,,10073314.0,,Not applicable,04-12-2019,27-02-2024,Graveney Road,Senacre,,Maidstone,Kent,ME15 8QQ,www.senacre-wood.kent.sch.uk/,3000658430.0,Mrs,Emily,Sweeney,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Shepway South,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,578633.0,153076.0,Maidstone 013,Maidstone 013F,,,,,Good,South-East England and South London,,200003717450.0,,Not applicable,Not applicable,,,E02005080,E01024399,68.0, +118534,886,Kent,2603,"Bromstone Primary School, Broadstairs",Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Has Special Classes,19-01-2023,406.0,221.0,185.0,38.9,Supported by a trust,Thanet Endeavour Learning Trust,Not applicable,,Supported by a federation,The Federation of Bromstone Primary School and St.Mildred's Primary Infant School,10073313.0,,Not applicable,27-03-2019,21-05-2024,Rumfields Road,,,Broadstairs,Kent,CT10 2PW,www.bromstoneschool.com/,1843867010.0,Mr,James,Williams,Executive Head,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,VI - Visual Impairment,HI - Hearing Impairment,"SLCN - Speech, language and Communication",,,,,,,,,,,Resourced provision,35.0,35.0,,,South East,Thanet,St Peters,South Thanet,(England/Wales) Urban city and town,E10000016,637928.0,167475.0,Thanet 011,Thanet 011D,,,,,Good,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005142,E01024690,158.0, +118536,886,Kent,2607,Parkside Community Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,168.0,84.0,84.0,75.6,Supported by a trust,Thanet Endeavour Learning Trust,Not applicable,,Not under a federation,,10073312.0,,Not applicable,26-04-2023,30-04-2024,Tennyson Avenue,,,Canterbury,Kent,CT1 1EP,www.parksidecommunityprimaryschool.co.uk,1227464956.0,Mr,James,Williams,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Northgate,Canterbury,(England/Wales) Urban city and town,E10000016,616671.0,158965.0,Canterbury 014,Canterbury 014C,,,,,Good,South-East England and South London,,100062279371.0,,Not applicable,Not applicable,,,E02005023,E01024090,127.0, +118541,886,Kent,2615,High Firs Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,206.0,98.0,108.0,13.6,Not applicable,,Not applicable,,Not under a federation,,10073311.0,,Not applicable,12-10-2023,23-04-2024,Court Crescent,,,Swanley,Kent,BR8 8NR,www.high-firs.kent.sch.uk/,1322669721.0,Mr,Andrew,Kilbride,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Swanley Christchurch and Swanley Village,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,551435.0,168115.0,Sevenoaks 003,Sevenoaks 003C,,,,,Good,South-East England and South London,,50002001954.0,,Not applicable,Not applicable,,,E02005089,E01024474,28.0, +118548,886,Kent,2627,Sandwich Junior School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,240.0,No Special Classes,19-01-2023,210.0,104.0,106.0,22.4,Not applicable,,Not applicable,,Not under a federation,,10079030.0,,Not applicable,24-03-2022,12-09-2023,St Bart's Road,,,Sandwich,Kent,CT13 0AS,www.sandwich-junior.kent.sch.uk/,1304612227.0,Mr,Martin,Dyson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Sandwich,South Thanet,(England/Wales) Rural town and fringe,E10000016,632790.0,157449.0,Dover 002,Dover 002C,,,,,Outstanding,South-East England and South London,,200001851331.0,,Not applicable,Not applicable,,,E02005042,E01024243,47.0, +118551,886,Kent,2632,Sevenoaks Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,587.0,312.0,275.0,15.2,Not applicable,,Not applicable,,Not under a federation,,10073310.0,,Not applicable,19-04-2023,15-04-2024,Bradbourne Park Road,,,Sevenoaks,Kent,TN13 3LB,www.sevenoaks.kent.sch.uk,1732453952.0,Mrs,Cassandra,Malone,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Town and St John's,Sevenoaks,(England/Wales) Urban city and town,E10000016,552671.0,156229.0,Sevenoaks 010,Sevenoaks 010F,,,,,Good,South-East England and South London,,100062548252.0,,Not applicable,Not applicable,,,E02005096,E01024468,89.0, +118558,886,Kent,2643,Swalecliffe Community Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,660.0,No Special Classes,19-01-2023,581.0,320.0,261.0,18.2,Supported by a trust,The Coastal Alliance Co-operative Trust,Not applicable,,Not under a federation,,10073102.0,,Not applicable,14-09-2023,24-01-2024,Bridgefield Road,Swalecliffe,,Whitstable,Kent,CT5 2PH,http://www.swalecliffeprimary.org,1227272101.0,Mrs,Sarah,Watson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Swalecliffe,Canterbury,(England/Wales) Urban city and town,E10000016,613028.0,166878.0,Canterbury 005,Canterbury 005E,,,,,Good,South-East England and South London,,100062301012.0,,Not applicable,Not applicable,,,E02005014,E01024115,106.0, +118563,886,Kent,2648,Aylesham Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,391.0,191.0,200.0,39.1,Not applicable,,Not applicable,,Not under a federation,,10073309.0,,Not applicable,08-06-2023,16-04-2024,Attlee Avenue,Aylesham,,Canterbury,Kent,CT3 3BS,www.aylesham.kent.sch.uk,1304840392.0,Mr,Darran,Callaghan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Aylesham, Eythorne & Shepherdswell",Dover,(England/Wales) Rural town and fringe,E10000016,623337.0,152432.0,Dover 006,Dover 006E,,,,,Good,South-East England and South London,,10034883661.0,,Not applicable,Not applicable,,,E02005046,E01035314,153.0, +118566,886,Kent,2651,Broadwater Down Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,148.0,69.0,79.0,34.5,Not applicable,,Not applicable,,Not under a federation,,10073308.0,,Not applicable,09-03-2023,17-04-2024,Broadwater Lane,,,Tunbridge Wells,Kent,TN2 5RP,www.broadwater-down.kent.sch.uk,1892527588.0,Mr,Alex,Cornelius,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Broadwater,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,557682.0,138106.0,Tunbridge Wells 010,Tunbridge Wells 010A,,,,,Good,South-East England and South London,,100062555286.0,,Not applicable,Not applicable,,,E02005171,E01024795,51.0, +118568,886,Kent,2653,West Borough Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,510.0,No Special Classes,19-01-2023,503.0,257.0,246.0,22.2,Not applicable,,Not applicable,,Not under a federation,,10076223.0,,Not applicable,19-10-2022,20-02-2024,Greenway,,,Maidstone,Kent,ME16 8TL,http://www.west-borough.kent.sch.uk/,1622726391.0,Mrs,Lisa,Edinburgh,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Heath,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,573885.0,155150.0,Maidstone 008,Maidstone 008C,,,,,Good,South-East England and South London,,10014309300.0,,Not applicable,Not applicable,,,E02005075,E01024367,102.0, +118575,886,Kent,2662,Long Mead Community Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,128.0,50.0,78.0,54.7,Not supported by a trust,,Not applicable,,Not under a federation,,10073101.0,,Not applicable,18-09-2019,31-03-2024,Waveney Road,,,Tonbridge,Kent,TN10 3JU,www.long-mead.kent.sch.uk/,1732350601.0,Mrs,Karen,Follows,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Trench,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558754.0,148444.0,Tonbridge and Malling 009,Tonbridge and Malling 009C,,,,,Good,South-East England and South London,,200000961885.0,,Not applicable,Not applicable,,,E02005157,E01024775,64.0, +118585,886,Kent,2674,King's Farm Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,492.0,No Special Classes,19-01-2023,437.0,237.0,200.0,53.2,Not applicable,,Not applicable,,Supported by a federation,Ifield School & King's Farm Primary School Federation,10069504.0,,Not applicable,28-02-2024,22-05-2024,Cedar Avenue,,,Gravesend,Kent,DA12 5JT,www.kings-farm.kent.sch.uk,1474566979.0,Mr,Chris,Jackson,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,13.0,15.0,,,South East,Gravesham,Singlewell,Gravesham,(England/Wales) Urban major conurbation,E10000016,565269.0,171511.0,Gravesham 011,Gravesham 011D,,,,,Good,South-East England and South London,,100062312756.0,,Not applicable,Not applicable,,,E02005065,E01024306,216.0, +118590,886,Kent,3010,St Pauls' Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,107.0,52.0,55.0,10.3,Not applicable,,Not applicable,,Supported by a federation,The Compass Federation,10070335.0,,Not applicable,28-01-2020,01-05-2024,School Lane,,,Swanley Village,Kent,BR8 7PJ,www.st-pauls-swanley.kent.sch.uk,1322664324.0,Mr,Benjamin,Hulme,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Swanley Christchurch and Swanley Village,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,552800.0,169829.0,Sevenoaks 001,Sevenoaks 001D,,,,,Good,South-East England and South London,,100062076900.0,,Not applicable,Not applicable,,,E02005087,E01024472,11.0, +118592,886,Kent,3015,Fawkham Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,102.0,50.0,52.0,5.9,Not applicable,,Not applicable,,Not under a federation,,10078483.0,,Not applicable,22-11-2023,15-04-2024,Valley Road,Fawkham,,LONGFIELD,Kent,DA3 8NA,http://fawkhamschool.com,1474702312.0,Miss,Mandy,Bridges,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Fawkham and West Kingsdown,Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,558819.0,166693.0,Sevenoaks 007,Sevenoaks 007C,,,,,Good,South-East England and South London,,10035185497.0,,Not applicable,Not applicable,,,E02005093,E01024437,6.0, +118597,886,Kent,3022,Benenden Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,211.0,96.0,115.0,11.4,Not applicable,,Not applicable,,Supported by a federation,The 10:10 Primary Federation,10074253.0,,Not applicable,24-02-2022,20-05-2024,Rolvenden Road,,,Cranbrook,,TN17 4EH,http://www.benenden-cep.kent.sch.uk,1580240565.0,Mrs,Lindsay,Roberts,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tunbridge Wells,Benenden and Cranbrook,Maidstone and The Weald,(England/Wales) Rural village,E10000016,580826.0,132744.0,Tunbridge Wells 014,Tunbridge Wells 014A,,,,,Good,South-East England and South London,United Kingdom,10000069843.0,,Not applicable,Not applicable,,,E02005175,E01024789,24.0, +118598,886,Kent,3023,Bidborough Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,207.0,109.0,98.0,6.3,Not applicable,,Not applicable,,Not under a federation,,10070333.0,,Not applicable,10-11-2022,20-05-2024,Spring Lane,Bidborough,,Tunbridge Wells,Kent,TN3 0UE,http://www.bidborough.kent.sch.uk,1892529333.0,Mrs,Julie,Burton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Speldhurst and Bidborough,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,556545.0,143127.0,Tunbridge Wells 006,Tunbridge Wells 006E,,,,,Good,South-East England and South London,,100062566429.0,,Not applicable,Not applicable,,,E02005167,E01024854,13.0, +118600,886,Kent,3027,Cranbrook Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,199.0,93.0,106.0,32.2,Not applicable,,Not applicable,,Not under a federation,,10070332.0,,Not applicable,22-06-2022,21-05-2024,Carriers Road,,,Cranbrook,Kent,TN17 3JZ,http://www.cranbrook-cep.kent.sch.uk,1580713249.0,Miss,Francesca,Shaw,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Benenden and Cranbrook,Maidstone and The Weald,(England/Wales) Rural town and fringe,E10000016,577605.0,136487.0,Tunbridge Wells 013,Tunbridge Wells 013C,,,,,Good,South-East England and South London,,100062552228.0,,Not applicable,Not applicable,,,E02005174,E01024790,64.0, +118601,886,Kent,3029,Goudhurst and Kilndown Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,211.0,107.0,104.0,15.6,Not applicable,,Not applicable,,Supported by a federation,The 10:10 Primary Federation,10078482.0,,Not applicable,20-03-2014,20-05-2024,Beaman Close,Cranbrook Road,Goudhurst,Cranbrook,Kent,TN17 1DZ,www.goudhurst-kilndown.kent.sch.uk,1580211365.0,Mrs,Lindsay,Roberts,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Goudhurst and Lamberhurst,Tunbridge Wells,(England/Wales) Rural village,E10000016,572817.0,137814.0,Tunbridge Wells 011,Tunbridge Wells 011C,,,,,Outstanding,South-East England and South London,,10008665566.0,,Not applicable,Not applicable,,,E02005172,E01024804,33.0, +118602,886,Kent,3032,Hawkhurst Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,183.0,100.0,83.0,42.1,Not applicable,,Not applicable,,Not under a federation,,10070331.0,,Not applicable,25-01-2023,15-04-2024,Fowlers Park,Rye Road,Hawkhurst,Cranbrook,Kent,TN18 4JJ,www.hawkhurst.kent.sch.uk,1580753254.0,Mrs,Jodi,Hacker,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Hawkhurst and Sandhurst,Tunbridge Wells,(England/Wales) Rural town and fringe,E10000016,576506.0,130526.0,Tunbridge Wells 014,Tunbridge Wells 014B,,,,,Good,South-East England and South London,,10000066322.0,,Not applicable,Not applicable,,,E02005175,E01024807,77.0, +118603,886,Kent,3033,Hildenborough Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,190.0,92.0,98.0,8.4,Not applicable,,Not applicable,,Not under a federation,,10069124.0,,Not applicable,01-03-2023,23-01-2024,Riding Lane,Hildenborough,,Tonbridge,Kent,TN11 9HY,http://www.hildenborough.kent.sch.uk,1732833394.0,Miss,Ruth,Ardrey,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Hildenborough,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,556585.0,148896.0,Tonbridge and Malling 010,Tonbridge and Malling 010D,,,,,Good,South-East England and South London,,200000966230.0,,Not applicable,Not applicable,,,E02005158,E01024754,16.0, +118604,886,Kent,3034,Lamberhurst St Mary's CofE (Voluntary Controlled) Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,206.0,95.0,111.0,8.7,Not applicable,,Not applicable,,Not under a federation,,10069123.0,,Not applicable,08-03-2023,01-05-2024,Pearse Place,Lamberhurst,,Tunbridge Wells,Kent,TN3 8EJ,www.lamberhurst.kent.sch.uk,1892890281.0,Mrs,Caroline,Bromley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Goudhurst and Lamberhurst,Tunbridge Wells,(England/Wales) Rural village,E10000016,567565.0,136008.0,Tunbridge Wells 011,Tunbridge Wells 011D,,,,,Good,South-East England and South London,,10000067311.0,,Not applicable,Not applicable,,,E02005172,E01024805,18.0, +118606,886,Kent,3037,"St John's Church of England Primary School, Sevenoaks",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,202.0,90.0,112.0,10.9,Not applicable,,Not applicable,,Not under a federation,,10078481.0,,Not applicable,26-04-2023,19-04-2024,Bayham Road,,,Sevenoaks,Kent,TN13 3XD,www.stjohnssevenoaks.co.uk,1732453944.0,Mrs,Therese,Pullan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Eastern,Sevenoaks,(England/Wales) Urban city and town,E10000016,553550.0,155884.0,Sevenoaks 010,Sevenoaks 010C,,,,,Good,South-East England and South London,,10035184681.0,,Not applicable,Not applicable,,,E02005096,E01024461,22.0, +118607,886,Kent,3042,Speldhurst Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,213.0,106.0,107.0,8.5,Not applicable,,Not applicable,,Not under a federation,,10069121.0,,Not applicable,07-02-2014,19-03-2024,Langton Road,Speldhurst,,Tunbridge Wells,Kent,TN3 0NP,http://www.speldhurst.kent.sch.uk,1892863044.0,Mrs,Stephanie,Hayward,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tunbridge Wells,Speldhurst and Bidborough,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,555310.0,141309.0,Tunbridge Wells 006,Tunbridge Wells 006D,,,,,Outstanding,South-East England and South London,,100062566310.0,,Not applicable,Not applicable,,,E02005167,E01024853,18.0, +118608,886,Kent,3043,Sundridge and Brasted Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,60.0,32.0,28.0,38.3,Not applicable,,Not applicable,,Not under a federation,,10078480.0,,Not applicable,12-10-2023,16-04-2024,Church Road,Sundridge,,Sevenoaks,Kent,TN14 6EA,http://www.sundridge.kent.sch.uk,1959562694.0,Mr,Tom,Hardwick,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Brasted, Chevening and Sundridge",Sevenoaks,(England/Wales) Rural village,E10000016,548465.0,154940.0,Sevenoaks 013,Sevenoaks 013A,,,,,Good,South-East England and South London,,100062548784.0,,Not applicable,Not applicable,,,E02005099,E01024417,23.0, +118611,886,Kent,3050,St John's Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,630.0,No Special Classes,19-01-2023,631.0,314.0,317.0,11.6,Not applicable,,Not applicable,,Not under a federation,,10069120.0,,Not applicable,22-03-2023,16-05-2024,Cunningham Road,,St John's Cep,Tunbridge Wells,Kent,TN4 9EW,www.st-johns-school.org.uk/,1892678980.0,Mr,Niall,Dossad,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558728.0,141298.0,Tunbridge Wells 003,Tunbridge Wells 003A,,,,,Good,South-East England and South London,,100062586186.0,,Not applicable,Not applicable,,,E02005164,E01024836,73.0, +118613,886,Kent,3052,St Mark's Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,420.0,No Special Classes,19-01-2023,360.0,193.0,167.0,23.1,Not applicable,,Not applicable,,Not under a federation,,10069119.0,,Not applicable,29-06-2022,21-05-2024,Ramslye Road,,,Tunbridge Wells,Kent,TN4 8LN,http://www.st-marks.kent.sch.uk,1892525402.0,Mr,Simon,Bird,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Broadwater,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,556979.0,138070.0,Tunbridge Wells 010,Tunbridge Wells 010B,,,,,Good,South-East England and South London,,100062585738.0,,Not applicable,Not applicable,,,E02005171,E01024796,83.0, +118614,886,Kent,3053,St Peter's Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,213.0,116.0,97.0,13.1,Not applicable,,Not applicable,,Not under a federation,,10078479.0,,Not applicable,20-03-2014,27-09-2023,Hawkenbury Road,,,Tunbridge Wells,,TN2 5BW,http://www.st-peters.kent.sch.uk,1892525727.0,Mrs,Joanna,Langton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Park,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,559229.0,139204.0,Tunbridge Wells 009,Tunbridge Wells 009C,,,,,Outstanding,South-East England and South London,United Kingdom,10090053470.0,,Not applicable,Not applicable,,,E02005170,E01024824,28.0, +118615,886,Kent,3054,Crockham Hill Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,140.0,No Special Classes,19-01-2023,137.0,70.0,67.0,0.7,Not applicable,,Not applicable,,Not under a federation,,10069118.0,,Not applicable,26-04-2023,23-05-2024,Crockham Hill,,,Edenbridge,Kent,TN8 6RP,http://www.crockhamhill.kent.sch.uk,1732866374.0,Mrs,Lisa,Higgs,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Sevenoaks,Westerham and Crockham Hill,Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,544353.0,150691.0,Sevenoaks 013,Sevenoaks 013D,,,,,Good,South-East England and South London,,10035185257.0,,Not applicable,Not applicable,,,E02005099,E01024484,1.0, +118616,886,Kent,3055,Churchill Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,420.0,No Special Classes,19-01-2023,201.0,94.0,107.0,28.4,Not applicable,,Not applicable,,Not under a federation,,10078478.0,,Not applicable,05-12-2019,12-09-2023,Rysted Lane,,,Westerham,Kent,TN16 1EZ,www.churchill.kent.sch.uk,1959562197.0,Mrs,Kathy,Jax,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Westerham and Crockham Hill,Sevenoaks,(England/Wales) Rural town and fringe,E10000016,544398.0,154520.0,Sevenoaks 013,Sevenoaks 013D,,,,,Good,South-East England and South London,,10035185216.0,,Not applicable,Not applicable,,,E02005099,E01024484,57.0, +118617,886,Kent,3057,St Peter's Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,194.0,102.0,92.0,14.4,Not applicable,,Not applicable,,Not under a federation,,10069117.0,,Not applicable,20-03-2019,03-06-2024,Mount Pleasant,,,Aylesford,Kent,ME20 7BE,www.stpetersaylesford.kent.sch.uk,1622717335.0,Mr,Jim,Holditch,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford North & North Downs,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,573064.0,159050.0,Tonbridge and Malling 001,Tonbridge and Malling 001A,,,,,Good,South-East England and South London,,100062628349.0,,Not applicable,Not applicable,,,E02005149,E01024719,28.0, +118619,886,Kent,3061,Bredhurst Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,140.0,No Special Classes,19-01-2023,130.0,70.0,60.0,5.4,Not applicable,,Not applicable,,Not under a federation,,10069116.0,,Not applicable,24-01-2024,20-05-2024,The Street,Bredhurst,,Gillingham,Kent,ME7 3JY,www.bredhurst.kent.sch.uk/,1634231271.0,Mrs,Michelle,Cox,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Boxley,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,579632.0,162276.0,Maidstone 001,Maidstone 001C,,,,,Good,South-East England and South London,,200003720400.0,,Not applicable,Not applicable,,,E02005068,E01024335,7.0, +118620,886,Kent,3062,Burham Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,196.0,No Special Classes,19-01-2023,144.0,74.0,70.0,8.3,Not applicable,,Not applicable,,Not under a federation,,10069115.0,,Not applicable,05-12-2018,14-05-2024,Bell Lane,Burham,,Rochester,Kent,ME1 3SY,http://www.burham.kent.sch.uk,1634861691.0,Mrs,Holly,Goddon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford North & North Downs,Chatham and Aylesford,(England/Wales) Rural village,E10000016,572942.0,161845.0,Tonbridge and Malling 001,Tonbridge and Malling 001F,,,,,Good,South-East England and South London,,200000961192.0,,Not applicable,Not applicable,,,E02005149,E01024728,12.0, +118622,886,Kent,3067,Harrietsham Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,390.0,No Special Classes,19-01-2023,309.0,148.0,161.0,19.2,Not applicable,,Not applicable,,Not under a federation,,10070328.0,,Not applicable,21-02-2024,20-05-2024,West Street,Harrietsham,,Maidstone,Kent,ME17 1JZ,www.harrietsham.kent.sch.uk,1622859261.0,Mrs,Jackie,Chambers,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Harrietsham and Lenham,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,586070.0,152776.0,Maidstone 011,Maidstone 011F,,,,,Good,South-East England and South London,,10022893860.0,,Not applicable,Not applicable,,,E02005078,E01034994,59.0, +118623,886,Kent,3069,Leeds and Broomfield Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,84.0,40.0,44.0,23.8,Not applicable,,Not applicable,,Not under a federation,,10069114.0,,Not applicable,19-10-2021,17-01-2024,Lower Street,Leeds,,Maidstone,Kent,ME17 1RL,www.leedsandbroomfieldkentsch.co.uk,1622861398.0,Mrs,Fiona,Steer,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Leeds,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,582467.0,153353.0,Maidstone 015,Maidstone 015C,,,,,Good,South-East England and South London,,200003698499.0,,Not applicable,Not applicable,,,E02005082,E01024375,20.0, +118625,886,Kent,3072,"Maidstone, St Michael's Church of England Junior School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,178.0,No Special Classes,19-01-2023,178.0,89.0,89.0,18.5,Not applicable,,Not applicable,,Supported by a federation,St Michael's Church of England Federated Infant and Junior School,10078477.0,,Not applicable,22-11-2023,23-04-2024,Douglas Road,,,Maidstone,Kent,ME16 8ER,www.st-michaels-junior.kent.sch.uk,1622751502.0,Mrs,Lisa,Saunders,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Fant,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575266.0,155120.0,Maidstone 006,Maidstone 006G,,,,,Good,South-East England and South London,,200003668897.0,,Not applicable,Not applicable,,,E02005073,E01033091,33.0, +118626,886,Kent,3073,St Michael's Church of England Infant School Maidstone,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,120.0,No Special Classes,19-01-2023,121.0,63.0,58.0,17.4,Not applicable,,Not applicable,,Supported by a federation,St Michael's Church of England Federated Infant and Junior School,10079671.0,,Not applicable,29-01-2014,17-04-2024,Douglas Road,,,Maidstone,Kent,ME16 8ER,http://www.st-michaels-infant.kent.sch.uk,1622751398.0,Mrs,Lisa,Saunders,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Fant,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575266.0,155120.0,Maidstone 006,Maidstone 006G,,,,,Outstanding,South-East England and South London,,200003668897.0,,Not applicable,Not applicable,,,E02005073,E01033091,21.0, +118629,886,Kent,3081,Thurnham Church of England Infant School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,270.0,No Special Classes,19-01-2023,270.0,140.0,130.0,1.9,Not applicable,,Not applicable,,Not under a federation,,10079670.0,,Not applicable,22-02-2023,25-04-2024,The Landway,Bearsted,,Maidstone,Kent,ME14 4BL,www.thurnham-infant.kent.sch.uk,1622737685.0,Mr,Tony,Pring,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bearsted,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,579425.0,155670.0,Maidstone 005,Maidstone 005A,,,,,Good,South-East England and South London,,200003691727.0,,Not applicable,Not applicable,,,E02005072,E01024330,5.0, +118630,886,Kent,3082,Trottiscliffe Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,84.0,No Special Classes,19-01-2023,81.0,39.0,42.0,2.5,Not applicable,,Not applicable,,Not under a federation,,10069113.0,,Not applicable,15-09-2022,21-05-2024,Church Lane,Trottiscliffe,,West Malling,Kent,ME19 5EB,http://www.trottiscliffe.kent.sch.uk/,1732822803.0,Miss,Lucy,Henderson,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Pilgrims with Ightham,Tonbridge and Malling,(England/Wales) Rural village,E10000016,564298.0,160200.0,Tonbridge and Malling 014,Tonbridge and Malling 014F,,,,,Good,South-East England and South London,,200000963789.0,,Not applicable,Not applicable,,,E02006833,E01032829,2.0, +118631,886,Kent,3083,Ulcombe Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,63.0,31.0,32.0,50.8,Not applicable,,Not applicable,,Not under a federation,,10069112.0,,Not applicable,27-11-2019,03-06-2024,The Street,Ulcombe,,Maidstone,Kent,ME17 1DU,https://ulcombekentsch.co.uk/,1622842903.0,Ms,Emma,Masters,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,"SEMH - Social, Emotional and Mental Health",MLD - Moderate Learning Difficulty,,,,,,,,,,Resourced provision,6.0,15.0,,,South East,Maidstone,Headcorn,Faversham and Mid Kent,(England/Wales) Rural hamlet and isolated dwellings,E10000016,584797.0,148979.0,Maidstone 017,Maidstone 017C,,,,,Good,South-East England and South London,,10014312333.0,,Not applicable,Not applicable,,,E02005084,E01024366,32.0, +118632,886,Kent,3084,Wateringbury Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,175.0,74.0,101.0,16.6,Not applicable,,Not applicable,,Not under a federation,,10078476.0,,Not applicable,08-03-2023,30-11-2023,147 Bow Road,Wateringbury,,Maidstone,Kent,ME18 5EA,www.wateringbury.kent.sch.uk/,1622812199.0,Miss,Debbie,Johnson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"East and West Peckham, Mereworth & Wateringbury",Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,569089.0,152864.0,Tonbridge and Malling 007,Tonbridge and Malling 007D,,,,,Requires improvement,South-East England and South London,,10014312449.0,,Not applicable,Not applicable,,,E02005155,E01024781,29.0, +118634,886,Kent,3088,"Wouldham, All Saints Church of England Voluntary Controlled Primary School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Non-selective,420.0,No Special Classes,19-01-2023,401.0,196.0,205.0,13.7,Not applicable,,Not applicable,,Not under a federation,,10075829.0,,Not applicable,26-04-2023,10-10-2023,1 Worrall Drive,Wouldham,,Rochester,,ME1 3GE,http://www.wouldham.kent.sch.uk,1634861434.0,Mrs,Victoria,Baldwin,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford North & North Downs,Chatham and Aylesford,(England/Wales) Rural hamlet and isolated dwellings,E10000016,571413.0,163394.0,Tonbridge and Malling 001,Tonbridge and Malling 001H,,,,,Good,South-East England and South London,United Kingdom,10092972751.0,,Not applicable,Not applicable,,,E02005149,E01035003,55.0, +118635,886,Kent,3089,St George's Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,203.0,103.0,100.0,18.7,Not applicable,,Not applicable,,Not under a federation,,10078871.0,,Not applicable,14-12-2022,03-06-2024,Old London Road,Wrotham,,Sevenoaks,Kent,TN15 7DL,http://www.st-georges-wrotham.kent.sch.uk/,1732882401.0,Mrs,Elizabeth,Rye,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tonbridge and Malling,Pilgrims with Ightham,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,560941.0,159482.0,Tonbridge and Malling 006,Tonbridge and Malling 006F,,,,,Good,South-East England and South London,,200000964122.0,,Not applicable,Not applicable,,,E02005154,E01024786,38.0, +118636,886,Kent,3090,"St Margaret's, Collier Street Church of England Voluntary Controlled School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,120.0,No Special Classes,19-01-2023,123.0,58.0,65.0,4.1,Not applicable,,Not applicable,,Not under a federation,,10069111.0,,Not applicable,04-05-2022,07-05-2024,Collier Street,Marden,,Tonbridge,Kent,TN12 9RR,www.collier-street.kent.sch.uk/,1892730264.0,Mr,Paul,Ryan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Marden and Yalding,Maidstone and The Weald,(England/Wales) Rural hamlet and isolated dwellings,E10000016,571630.0,146064.0,Maidstone 018,Maidstone 018C,,,,,Good,South-East England and South London,,200003662059.0,,Not applicable,Not applicable,,,E02005085,E01024380,5.0, +118637,886,Kent,3091,Laddingford St Mary's Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,101.0,No Special Classes,19-01-2023,81.0,37.0,44.0,25.0,Not applicable,,Not applicable,,Not under a federation,,10069110.0,,Not applicable,08-03-2023,30-04-2024,Darman Lane,Laddingford,,Maidstone,Kent,ME18 6BL,http://www.laddingford.kent.sch.uk,1622871270.0,Mrs,Lucy,Clark,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,Marden and Yalding,Maidstone and The Weald,(England/Wales) Rural hamlet and isolated dwellings,E10000016,568865.0,147811.0,Maidstone 018,Maidstone 018B,,,,,Good,South-East England and South London,,200003718886.0,,Not applicable,Not applicable,,,E02005085,E01024379,20.0, +118638,886,Kent,3092,"Yalding, St Peter and St Paul Church of England Voluntary Controlled Primary School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,168.0,No Special Classes,19-01-2023,173.0,87.0,86.0,10.4,Not applicable,,Not applicable,,Not under a federation,,10078475.0,,Not applicable,30-01-2019,05-06-2024,Vicarage Road,Yalding,,Maidstone,Kent,ME18 6DP,http://www.yalding.kent.sch.uk,1622814298.0,Mrs,Sarah,Friend,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Marden and Yalding,Maidstone and The Weald,(England/Wales) Rural town and fringe,E10000016,570095.0,150173.0,Maidstone 014,Maidstone 014D,,,,,Good,South-East England and South London,,200003718898.0,,Not applicable,Not applicable,,,E02005081,E01024378,18.0, +118646,886,Kent,3108,Ospringe Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,213.0,113.0,100.0,30.5,Not applicable,,Not applicable,,Not under a federation,,10069108.0,,Not applicable,07-06-2023,16-04-2024,Water Lane,Ospringe,,Faversham,Kent,ME13 8TX,www.ospringeprimary.co.uk/,1795532004.0,Mrs,Amanda,Ralph,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Watling,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,600216.0,160694.0,Swale 014,Swale 014F,,,,,Good,South-East England and South London,,100062380290.0,,Not applicable,Not applicable,,,E02005128,E01024627,65.0, +118647,886,Kent,3109,Hernhill Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,212.0,105.0,107.0,10.4,Not applicable,,Not applicable,,Not under a federation,,10078474.0,,Not applicable,07-03-2024,22-05-2024,Fostall,Hernhill,,Faversham,Kent,ME13 9JG,www.hernhill.kent.sch.uk,1227751322.0,Mrs,Sarah,Alexander,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Boughton and Courtenay,Faversham and Mid Kent,(England/Wales) Rural hamlet and isolated dwellings,E10000016,606692.0,161491.0,Swale 017,Swale 017B,,,,,Outstanding,South-East England and South London,,10023196116.0,,Not applicable,Not applicable,,,E02005131,E01024556,22.0, +118649,886,Kent,3111,Newington Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,236.0,No Special Classes,19-01-2023,252.0,139.0,113.0,33.3,Not applicable,,Not applicable,,Supported by a federation,The Federation of Lower Halstow Primary and Newington CofE Primary School,10069107.0,,Not applicable,15-05-2019,19-04-2024,School Lane,Newington,,Sittingbourne,Kent,ME9 7LB,www.newington.kent.sch.uk,1795842300.0,Mrs,Tara,Deevoy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,"Hartlip, Newington and Upchurch",Sittingbourne and Sheppey,(England/Wales) Rural town and fringe,E10000016,585949.0,165334.0,Swale 008,Swale 008B,,,,,Good,South-East England and South London,,100062626974.0,,Not applicable,Not applicable,,,E02005122,E01024571,84.0, +118651,886,Kent,3117,Teynham Parochial Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,202.0,105.0,97.0,31.2,Not applicable,,Not applicable,,Not under a federation,,10078473.0,,Not applicable,29-03-2023,23-05-2024,Station Road,Teynham,,Sittingbourne,Kent,ME9 9BQ,www.teynham.kent.sch.uk,1795521217.0,Mrs,Elizabeth,Pearson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Teynham and Lynsted,Sittingbourne and Sheppey,(England/Wales) Rural town and fringe,E10000016,595417.0,162735.0,Swale 016,Swale 016D,,,,,Requires improvement,South-East England and South London,,100062627014.0,,Not applicable,Not applicable,,,E02005130,E01024623,63.0, +118653,886,Kent,3120,Barham Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,210.0,102.0,108.0,12.4,Not applicable,,Not applicable,,Not under a federation,,10069106.0,,Not applicable,25-01-2023,15-04-2024,Valley Road,Barham,Barham C of E Primary School,CANTERBURY,Kent,CT4 6NX,www.barham.kent.sch.uk/,1227831312.0,Mrs,Alison Higgins,Jo Duhig,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Nailbourne,Canterbury,(England/Wales) Rural village,E10000016,620612.0,150003.0,Canterbury 018,Canterbury 018B,,,,,Good,South-East England and South London,,100062299377.0,,Not applicable,Not applicable,,,E02005027,E01024043,26.0, +118654,886,Kent,3122,Bridge and Patrixbourne Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,407.0,199.0,208.0,3.4,Not applicable,,Not applicable,,Not under a federation,,10078472.0,,Not applicable,04-10-2023,18-04-2024,Conyngham Lane,Bridge,,Canterbury,Kent,CT4 5JX,www.bridge.kent.sch.uk,1227830276.0,Mr,James,Tibbles,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Nailbourne,Canterbury,(England/Wales) Rural town and fringe,E10000016,618239.0,154550.0,Canterbury 018,Canterbury 018C,,,,,Good,South-East England and South London,,100062298950.0,,Not applicable,Not applicable,,,E02005027,E01024088,14.0, +118655,886,Kent,3123,Chislet Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,95.0,50.0,45.0,12.6,Not applicable,,Not applicable,,Supported by a federation,The Federation of Chislet CE and Hoath Primary Schools,10078870.0,,Not applicable,24-11-2022,18-04-2024,Church Lane,Chislet,,Canterbury,Kent,CT3 4DU,www.chislethoathfederation.co.uk,1227860295.0,Mr,Tim,Whitehouse,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Canterbury,Reculver,North Thanet,(England/Wales) Rural village,E10000016,622377.0,164226.0,Canterbury 010,Canterbury 010D,,,,,Good,South-East England and South London,,10033152283.0,,Not applicable,Not applicable,,,E02005019,E01024087,12.0, +118657,886,Kent,3126,Littlebourne Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,99.0,47.0,52.0,26.3,Not applicable,,Not applicable,,Not under a federation,,10069105.0,,Not applicable,23-05-2019,30-05-2024,Church Road,Littlebourne,,Canterbury,Kent,CT3 1XS,http://www.littlebourne.kent.sch.uk,1227721671.0,Mrs,Samantha,Killick,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Little Stour & Adisham,Canterbury,(England/Wales) Rural town and fringe,E10000016,620956.0,157780.0,Canterbury 010,Canterbury 010A,,,,,Good,South-East England and South London,,200000681653.0,,Not applicable,Not applicable,,,E02005019,E01024084,26.0, +118659,886,Kent,3129,St Alphege Church of England Infant School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,232.0,No Special Classes,19-01-2023,206.0,105.0,101.0,16.3,Not applicable,,Not applicable,,Not under a federation,,10079668.0,,Not applicable,03-02-2023,03-06-2024,Oxford Street,,,Whitstable,Kent,CT5 1DA,http://www.st-alphege.kent.sch.uk,1227272977.0,Mrs,Liz,Thomas-Friend,Executive Head,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Gorrell,Canterbury,(England/Wales) Urban city and town,E10000016,610681.0,166250.0,Canterbury 008,Canterbury 008C,,,,,Good,South-East England and South London,,100062300537.0,,Not applicable,Not applicable,,,E02005017,E01024070,30.0, +118660,886,Kent,3130,Wickhambreaux Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,115.0,No Special Classes,19-01-2023,118.0,55.0,63.0,11.0,Not applicable,,Not applicable,,Not under a federation,,10065385.0,,Not applicable,26-02-2015,20-05-2024,The Street,Wickhambreaux,,Canterbury,Kent,CT3 1RN,www.wickhambreaux-school.ik.org/,1227721300.0,Mrs,Lisa,Crosbie,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Little Stour & Adisham,Canterbury,(England/Wales) Rural hamlet and isolated dwellings,E10000016,622169.0,158684.0,Canterbury 010,Canterbury 010B,,,,,Outstanding,South-East England and South London,,100062298259.0,,Not applicable,Not applicable,,,E02005019,E01024085,13.0, +118663,886,Kent,3136,Brabourne Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,104.0,49.0,55.0,11.5,Not applicable,,Not applicable,,Not under a federation,,10078471.0,,Not applicable,19-06-2018,06-06-2024,School Lane,Brabourne,,Ashford,Kent,TN25 5LQ,www.brabourne.kent.sch.uk,1303813276.0,Mr,Andrew,Stapley,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Bircholt,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,609183.0,141706.0,Ashford 010,Ashford 010C,,,,,Good,South-East England and South London,,100062561998.0,,Not applicable,Not applicable,,,E02005005,E01024015,12.0, +118664,886,Kent,3137,Brookland Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,87.0,43.0,44.0,16.1,Not applicable,,Not applicable,,Not under a federation,,10078470.0,,Not applicable,03-02-2023,14-05-2024,High Street,Brookland,,Romney Marsh,Kent,TN29 9QR,http://www.brookland.kent.sch.uk,1797344317.0,Mr,Martin,Hacker,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Walland & Denge Marsh,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,599003.0,125887.0,Folkestone and Hythe 011,Folkestone and Hythe 011D,,,,,Good,South-East England and South London,,50000252.0,,Not applicable,Not applicable,,,E02005112,E01024548,14.0, +118665,886,Kent,3138,"Chilham, St Mary's Church of England Primary School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,79.0,36.0,43.0,24.1,Not applicable,,Not applicable,,Not under a federation,,10069103.0,,Not applicable,02-02-2022,08-05-2024,School Hill,Chilham,,Canterbury,Kent,CT4 8DE,www.chilham.kent.sch.uk,1227730442.0,Mrs,Delia,Cooper,Interim Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Downs North,Ashford,(England/Wales) Rural village,E10000016,606875.0,153533.0,Ashford 001,Ashford 001B,,,,,Good,South-East England and South London,,10012840696.0,,Not applicable,Not applicable,,,E02004996,E01023987,19.0, +118666,886,Kent,3139,High Halden Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,102.0,45.0,57.0,17.6,Not applicable,,Not applicable,,Supported by a federation,Flourish Together Federation,10069102.0,,Not applicable,24-02-2022,19-04-2024,Church Hill,High Halden,,Ashford,Kent,TN26 3JB,http://www.high-halden.kent.sch.uk/,1233850285.0,Mrs,Kelly,Burlton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Weald Central,Ashford,(England/Wales) Rural village,E10000016,590169.0,137144.0,Ashford 011,Ashford 011C,,,,,Good,South-East England and South London,,100062617661.0,,Not applicable,Not applicable,,,E02005006,E01024032,18.0, +118672,886,Kent,3145,Woodchurch Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,182.0,No Special Classes,19-01-2023,157.0,88.0,69.0,26.1,Not applicable,,Not applicable,,Supported by a federation,Flourish Together Federation,10069101.0,,Not applicable,17-05-2023,07-05-2024,Bethersden Road,Woodchurch,,Ashford,Kent,TN26 3QJ,http://www.woodchurch.kent.sch.uk,1233860232.0,Mrs,Kelly,Burlton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Weald South,Ashford,(England/Wales) Rural village,E10000016,594165.0,134993.0,Ashford 012,Ashford 012C,,,,,Good,South-East England and South London,,10012848908.0,,Not applicable,Not applicable,,,E02005007,E01024038,41.0, +118673,886,Kent,3146,Bodsham Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,103.0,No Special Classes,19-01-2023,82.0,43.0,39.0,6.1,Not applicable,,Not applicable,,Supported by a federation,The Federation of Bodsham Church of England Primary School and Saltwood Church of England Primary School,10069100.0,,Not applicable,26-05-2022,23-04-2024,School Hill,Bodsham,,Ashford,Kent,TN25 5JQ,www.bodsham.kent.sch.uk,1233750374.0,Mr,Paul,Newton,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Folkestone and Hythe,North Downs West,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,611020.0,145724.0,Folkestone and Hythe 001,Folkestone and Hythe 001C,,,,,Good,South-East England and South London,,50010714.0,,Not applicable,Not applicable,,,E02005102,E01024545,5.0, +118675,886,Kent,3149,"Folkestone, St Martin's Church of England Primary School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,202.0,90.0,112.0,18.3,Not applicable,,Not applicable,,Supported by a federation,The Federation of St Martin's and Seabrook CEP Schools,10069099.0,,Not applicable,24-04-2015,17-04-2024,Horn Street,,,Folkestone,Kent,CT20 3JJ,http://www.stmartinsfolkestone.com,1303238888.0,Mrs,Elizabeth,Carter,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,618951.0,136429.0,Folkestone and Hythe 005,Folkestone and Hythe 005C,,,,,Outstanding,South-East England and South London,,50023003.0,,Not applicable,Not applicable,,,E02005106,E01024494,37.0, +118676,886,Kent,3150,"Folkestone, St Peter's Church of England Primary School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,104.0,47.0,57.0,43.3,Not applicable,,Not applicable,,Not under a federation,,10078469.0,,Not applicable,26-06-2019,13-05-2024,The Durlocks,,,Folkestone,Kent,CT19 6AL,www.stpetersfolkestone.com/,1303255400.0,Mrs,Toni,Browne,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Folkestone Harbour,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,623352.0,136165.0,Folkestone and Hythe 014,Folkestone and Hythe 014A,,,,,Good,South-East England and South London,,,,Not applicable,Not applicable,,,E02006879,E01024504,45.0, +118678,886,Kent,3153,Seabrook Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,104.0,45.0,59.0,13.5,Not applicable,,Not applicable,,Supported by a federation,The Federation of St Martin's and Seabrook CEP Schools,10078468.0,,Not applicable,08-11-2023,17-04-2024,Seabrook Road,,,Hythe,Kent,CT21 5RL,www.seabrookprimaryschool.co.uk/,1303238429.0,Mrs,Elizabeth,Carter,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Hythe,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,618674.0,134958.0,Folkestone and Hythe 005,Folkestone and Hythe 005F,,,,,Good,South-East England and South London,,50022207.0,,Not applicable,Not applicable,,,E02005106,E01024528,14.0, +118679,886,Kent,3154,Lyminge Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,196.0,97.0,99.0,8.7,Not applicable,,Not applicable,,Not under a federation,,10069098.0,,Not applicable,09-11-2023,04-06-2024,Church Road,Lyminge,,Folkestone,Kent,CT18 8JA,www.lymingeprimaryschool.co.uk/,1303862367.0,Mr,Matt,Day,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs West,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,616220.0,141085.0,Folkestone and Hythe 001,Folkestone and Hythe 001D,,,,,Good,South-East England and South London,,50021153.0,,Not applicable,Not applicable,,,E02005102,E01024547,17.0, +118680,886,Kent,3155,Lympne Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,203.0,104.0,99.0,6.4,Not applicable,,Not applicable,,Not under a federation,,10070327.0,,Not applicable,14-10-2021,24-05-2024,Octavian Drive,Lympne,,Hythe,Kent,CT21 4JG,www.lympne.kent.sch.uk,1303268041.0,Mrs,Melanie,Nash,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Hythe Rural,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,612226.0,135150.0,Folkestone and Hythe 009,Folkestone and Hythe 009C,,,,,Good,South-East England and South London,,50012208.0,,Not applicable,Not applicable,,,E02005110,E01024536,13.0, +118681,886,Kent,3158,Stelling Minnis Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,83.0,48.0,35.0,18.1,Not applicable,,Not applicable,,Not under a federation,,10078467.0,,Not applicable,09-06-2022,03-06-2024,Bossingham Road,Stelling Minnis,,Canterbury,Kent,CT4 6DU,www.stelling-minnis.kent.sch.uk,1227709218.0,Mrs,Julie,Simmons,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs West,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,614898.0,148564.0,Folkestone and Hythe 001,Folkestone and Hythe 001A,,,,,Good,South-East England and South London,,50014408.0,,Not applicable,Not applicable,,,E02005102,E01024490,15.0, +118682,886,Kent,3159,Stowting Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,103.0,52.0,51.0,7.8,Not applicable,,Not applicable,,Not under a federation,,10069097.0,,Not applicable,13-11-2019,20-03-2024,Stowting,Stowting Hill,,Ashford,Kent,TN25 6BE,http://www.stowting.kent.sch.uk,1303862375.0,Mrs,Sarah,Uden,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs West,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,612365.0,142429.0,Folkestone and Hythe 001,Folkestone and Hythe 001C,,,,,Good,South-East England and South London,,50012509.0,,Not applicable,Not applicable,,,E02005102,E01024545,8.0, +118683,886,Kent,3160,Selsted Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,99.0,54.0,45.0,9.1,Not applicable,,Not applicable,,Not under a federation,,10069096.0,,Not applicable,02-11-2022,15-05-2024,Selsted,"Wootton Lane, Selsted",Wootton Lane,Dover,Kent,CT15 7HH,www.selstedschool.org/,1303844286.0,Mrs,Angela,Woodgate,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs East,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,622244.0,144587.0,Folkestone and Hythe 001,Folkestone and Hythe 001B,,,,,Good,South-East England and South London,,50044106.0,,Not applicable,Not applicable,,,E02005102,E01024544,9.0, +118685,886,Kent,3167,Eastry Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,218.0,No Special Classes,19-01-2023,165.0,87.0,78.0,26.8,Not applicable,,Not applicable,,Not under a federation,,10069095.0,,Not applicable,14-06-2023,08-05-2024,Cooks Lea,Eastry,,Sandwich,Kent,CT13 0LR,www.eastry.kent.sch.uk/,1304611360.0,Mrs,Sarah,Moss,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dover,Eastry Rural,Dover,(England/Wales) Rural town and fringe,E10000016,630665.0,154869.0,Dover 002,Dover 002A,,,,,Good,South-East England and South London,,100062284627.0,,Not applicable,Not applicable,,,E02005042,E01024202,44.0, +118686,886,Kent,3168,Goodnestone Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,70.0,No Special Classes,19-01-2023,53.0,21.0,32.0,9.4,Not applicable,,Not applicable,,Not under a federation,,10069094.0,,Not applicable,17-01-2019,14-05-2024,The Street,Goodnestone,,Canterbury,Kent,CT3 1PQ,www.goodnestone.kent.sch.uk/,1304840329.0,Mrs,Victoria,Solly,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Little Stour & Ashstone,South Thanet,(England/Wales) Rural village,E10000016,625516.0,154670.0,Dover 001,Dover 001B,,,,,Good,South-East England and South London,,100062298258.0,,Not applicable,Not applicable,,,E02005041,E01024207,5.0, +118687,886,Kent,3169,Guston Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Non-selective,180.0,No Special Classes,19-01-2023,151.0,81.0,70.0,9.3,Not applicable,,Not applicable,,Not under a federation,,10075828.0,,Not applicable,21-10-2021,16-04-2024,Burgoyne Heights,Guston,,Dover,Kent,CT15 5LR,www.guston.kent.sch.uk/,1304206847.0,Mrs,Deby,Day,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Guston, Kingsdown & St Margaret's-at-Cliffe",Dover,(England/Wales) Rural village,E10000016,632307.0,143088.0,Dover 012,Dover 012B,,,,,Good,South-East England and South London,,100062620447.0,,Not applicable,Not applicable,,,E02005052,E01024238,14.0, +118688,886,Kent,3171,Nonington Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,86.0,No Special Classes,19-01-2023,38.0,19.0,19.0,57.9,Not applicable,,Not applicable,,Not under a federation,,10078869.0,,Not applicable,21-04-2022,14-05-2024,Church Street,Nonington,,Dover,Kent,CT15 4LB,www.noningtonprimary.co.uk/,1304840348.0,Mrs,Victoria,Solly,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SEMH - Social, Emotional and Mental Health",,,,,,,,,,,,,Resourced provision,,8.0,,,South East,Dover,"Aylesham, Eythorne & Shepherdswell",Dover,(England/Wales) Rural hamlet and isolated dwellings,E10000016,625273.0,152175.0,Dover 006,Dover 006A,,,,,Requires improvement,South-East England and South London,,100062287271.0,,Not applicable,Not applicable,,,E02005046,E01024190,22.0, +118691,886,Kent,3175,Shepherdswell Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,202.0,104.0,98.0,11.4,Not applicable,,Not applicable,,Supported by a federation,The Federation of Shepherdswell Church of England and Eythorne Elvington,10069093.0,,Not applicable,20-10-2021,02-05-2024,Coldred Road,Shepherdswell,,DOVER,Kent,CT15 7LH,www.shepherdswell.kent.sch.uk/,1304830312.0,Mr,Mark,Lamb,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Aylesham, Eythorne & Shepherdswell",Dover,(England/Wales) Rural town and fringe,E10000016,626077.0,147755.0,Dover 006,Dover 006D,,,,,Good,South-East England and South London,United Kingdom,100062288370.0,,Not applicable,Not applicable,,,E02005046,E01024204,23.0, +118693,886,Kent,3178,Birchington Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,570.0,No Special Classes,19-01-2023,475.0,235.0,240.0,25.3,Not applicable,,Not applicable,,Not under a federation,,10078464.0,,Not applicable,25-09-2019,05-06-2024,Park Lane,,,Birchington,Kent,CT7 0AS,www.birchington-primary.com/,1843841046.0,Ms,Kathleen,Barham,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Birchington South,North Thanet,(England/Wales) Urban city and town,E10000016,630372.0,168670.0,Thanet 007,Thanet 007A,,,,,Good,South-East England and South London,,100062303819.0,,Not applicable,Not applicable,,,E02005138,E01024641,120.0, +118694,886,Kent,3179,"Margate, Holy Trinity and St John's Church of England Primary School",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,407.0,208.0,199.0,55.3,Not applicable,,Not applicable,,Not under a federation,,10069092.0,,Not applicable,29-03-2023,21-05-2024,St John's Road,,,Margate,Kent,CT9 1LU,www.holytrinitymargate.co.uk/,1843223237.0,Mr,Rob,Garratt,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,16.0,16.0,,,South East,Thanet,Margate Central,North Thanet,(England/Wales) Urban city and town,E10000016,635618.0,170614.0,Thanet 001,Thanet 001G,,,,,Good,South-East England and South London,,200003082079.0,,Not applicable,Not applicable,,,E02005132,E01035317,225.0, +118695,886,Kent,3181,St Saviour's Church of England Junior School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,384.0,Not applicable,19-01-2023,375.0,196.0,179.0,30.9,Not applicable,,Not applicable,,Not under a federation,,10078463.0,,Not applicable,18-10-2023,13-12-2023,Elm Grove,,St. Saviour's C.E Junior School Elm Grove,Westgate on Sea,Kent,CT8 8LD,www.stsavioursjunior.com,1843831707.0,Mr,Nick,Bonell,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Westgate-on-Sea,North Thanet,(England/Wales) Urban city and town,E10000016,632054.0,169794.0,Thanet 007,Thanet 007C,,,,,Good,South-East England and South London,,100062304387.0,,Not applicable,Not applicable,,,E02005138,E01024714,116.0, +118696,886,Kent,3182,Minster Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,389.0,212.0,177.0,22.1,Not applicable,,Not applicable,,Not under a federation,,10069091.0,,Not applicable,18-01-2023,23-04-2024,Molineux Road,Minster-in-Thanet,,Ramsgate,Kent,CT12 4PS,http://www.minster-ramsgate.kent.sch.uk/,1843821384.0,Mr,Paul,McCarthy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Thanet Villages,North Thanet,(England/Wales) Rural town and fringe,E10000016,630821.0,164492.0,Thanet 014,Thanet 014B,,,,,Good,South-East England and South London,,200001487980.0,,Not applicable,Not applicable,,,E02005145,E01024702,86.0, +118697,886,Kent,3183,Monkton Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,106.0,49.0,57.0,23.6,Not applicable,,Not applicable,,Not under a federation,,10069090.0,,Not applicable,30-01-2024,20-05-2024,Monkton Street,Monkton,,Ramsgate,Kent,CT12 4JQ,http://www.monkton.kent.sch.uk,1843821394.0,Mr,Paul,McCarthy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Thanet Villages,North Thanet,(England/Wales) Rural village,E10000016,628701.0,165070.0,Thanet 014,Thanet 014C,,,,,Good,South-East England and South London,,10022964433.0,,Not applicable,Not applicable,,,E02005145,E01024703,25.0, +118698,886,Kent,3186,St Nicholas At Wade Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,195.0,86.0,109.0,15.9,Not applicable,,Not applicable,,Not under a federation,,10078462.0,,Not applicable,02-10-2019,21-05-2024,Down Barton Road,St Nicholas-At-Wade,,Birchington,Kent,CT7 0PY,www.st-nicholas-birchington.kent.sch.uk/,1843847253.0,Mrs,Taralee,Kennedy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Thanet Villages,North Thanet,(England/Wales) Rural village,E10000016,626348.0,166666.0,Thanet 014,Thanet 014C,,,,,Good,South-East England and South London,,100062303823.0,,Not applicable,Not applicable,,,E02005145,E01024703,31.0, +118701,886,Kent,3198,Frittenden Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,98.0,44.0,54.0,23.5,Not applicable,,Not applicable,,Not under a federation,,10069089.0,,Not applicable,24-11-2022,26-04-2024,Frittenden Primary School,,Frittenden,Cranbrook,Kent,TN17 2DD,http://www.frittenden.kent.sch.uk,1580852250.0,Ms,nichola,costello,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Frittenden and Sissinghurst,Maidstone and The Weald,(England/Wales) Rural village,E10000016,581389.0,140963.0,Tunbridge Wells 013,Tunbridge Wells 013E,,,,,Requires improvement,South-East England and South London,,10008667029.0,,Not applicable,Not applicable,,,E02005174,E01024803,23.0, +118702,886,Kent,3199,Egerton Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,188.0,103.0,85.0,20.7,Not applicable,,Not applicable,,Not under a federation,,10069088.0,,Not applicable,12-10-2023,16-04-2024,Stisted Way,Egerton,,Ashford,Kent,TN27 9DR,http://www.egerton.kent.sch.uk,1233756274.0,Mrs,Julia,Walker,Interim Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Weald North,Ashford,(England/Wales) Rural village,E10000016,590598.0,147325.0,Ashford 002,Ashford 002F,,,,,Good,South-East England and South London,,100062563941.0,,Not applicable,Not applicable,,,E02004997,E01024035,39.0, +118704,886,Kent,3201,St Lawrence Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,84.0,No Special Classes,19-01-2023,76.0,35.0,41.0,3.9,Not applicable,,Not applicable,,Not under a federation,,10078461.0,,Not applicable,28-09-2022,05-02-2024,Church Road,Stone Street,,Sevenoaks,Kent,TN15 0LN,http://www.st-lawrence-sevenoaks.kent.sch.uk,1732761393.0,Mr,Daniel,Eaton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Seal and Weald,Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,557394.0,154844.0,Sevenoaks 012,Sevenoaks 012A,,,,,Good,South-East England and South London,,10035185576.0,,Not applicable,Not applicable,,,E02005098,E01024458,3.0, +118705,886,Kent,3282,Boughton-under-Blean and Dunkirk Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Methodist,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,203.0,100.0,103.0,24.1,Not applicable,,Not applicable,,Not under a federation,,10078141.0,,Not applicable,11-07-2019,03-06-2024,School Lane,Boughton-under-Blean,,Faversham,Kent,ME13 9AW,www.bad.kent.sch.uk,1227751431.0,Mr,Simon,Way,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Boughton and Courtenay,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,605832.0,159483.0,Swale 017,Swale 017C,,,,,Good,South-East England and South London,,200002540885.0,,Not applicable,Not applicable,,,E02005131,E01024557,49.0, +118706,886,Kent,3284,Lady Joanna Thornhill Endowed Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,418.0,198.0,220.0,11.2,Not applicable,,Not applicable,,Not under a federation,,10078140.0,,Not applicable,05-02-2015,18-04-2024,Bridge Street,Wye,,Ashford,Kent,TN25 5EA,www.ladyj.kent.sch.uk/,1233812781.0,Mrs,Rachael,Foster,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Wye with Hinxhill,Ashford,(England/Wales) Rural town and fringe,E10000016,605082.0,146710.0,Ashford 001,Ashford 001D,,,,,Outstanding,South-East England and South London,,10012868707.0,,Not applicable,Not applicable,,,E02004996,E01024040,47.0, +118707,886,Kent,3289,St Peter's Methodist Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Methodist,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,209.0,108.0,101.0,21.5,Not applicable,,Not applicable,,Not under a federation,,10078139.0,,Not applicable,12-12-2018,01-05-2024,St Peter's Grove,,,Canterbury,Kent,CT1 2DH,http://www.st-peters-canterbury.kent.sch.uk,1227464392.0,Mrs,Kristina,Dyer,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Westgate,Canterbury,(England/Wales) Urban city and town,E10000016,614679.0,157910.0,Canterbury 020,Canterbury 020F,,,,,Good,South-East England and South London,,200000676740.0,,Not applicable,Not applicable,,,E02006856,E01032807,45.0, +118709,886,Kent,3294,St Matthew's High Brooms Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,420.0,No Special Classes,19-01-2023,354.0,178.0,176.0,43.2,Not applicable,,Not applicable,,Not under a federation,,10069087.0,,Not applicable,19-07-2018,15-04-2024,Powder Mill Lane,High Brooms,,Tunbridge Wells,Kent,TN4 9DY,www.st-matthews-school.org,1892528098.0,Mrs,Claire,Harris,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Southborough and High Brooms,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558927.0,141801.0,Tunbridge Wells 003,Tunbridge Wells 003D,,,,,Good,South-East England and South London,,100062586205.0,,Not applicable,Not applicable,,,E02005164,E01024847,153.0, +118710,886,Kent,3295,Herne Church of England Infant and Nursery School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,270.0,No Special Classes,19-01-2023,316.0,171.0,145.0,7.8,Not applicable,,Not applicable,,Not under a federation,,10079667.0,,Not applicable,29-09-2021,21-05-2024,Palmer Close,Herne,,Herne Bay,Kent,CT6 7AH,www.herne-infant.kent.sch.uk/,1227740793.0,Mrs,E,Thomas-Friend,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Herne & Broomfield,North Thanet,(England/Wales) Urban city and town,E10000016,618672.0,165903.0,Canterbury 006,Canterbury 006C,,,,,Outstanding,South-East England and South London,,200000680285.0,,Not applicable,Not applicable,,,E02005015,E01024075,21.0, +118711,886,Kent,3296,Langafel Church of England Voluntary Controlled Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,331.0,No Special Classes,19-01-2023,325.0,175.0,150.0,32.0,Not applicable,,Not applicable,,Not under a federation,,10078868.0,,Not applicable,03-10-2018,26-02-2024,Main Road,,,Longfield,Kent,DA3 7PW,www.langafel.kent.sch.uk/,1474703398.0,Mrs,Catherine,Maynard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,30.0,29.0,,,South East,Dartford,"Longfield, New Barn & Southfleet",Dartford,(England/Wales) Urban city and town,E10000016,561395.0,168619.0,Dartford 013,Dartford 013D,,,,,Good,South-East England and South London,,200000538032.0,,Not applicable,Not applicable,,,E02005040,E01024160,104.0, +118712,886,Kent,3297,Southborough CofE Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,630.0,No Special Classes,19-01-2023,615.0,309.0,306.0,17.6,Not applicable,,Not applicable,,Not under a federation,,10074251.0,,Not applicable,21-06-2018,30-04-2024,Broomhill Park Road,Southborough,,Tunbridge Wells,Kent,TN4 0JY,http://www.southborough.kent.sch.uk,1892529682.0,Mrs,Emma,Savage,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tunbridge Wells,Southborough and High Brooms,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,557592.0,141923.0,Tunbridge Wells 002,Tunbridge Wells 002C,,,,,Good,South-East England and South London,,100062585335.0,,Not applicable,Not applicable,,,E02005163,E01024846,108.0, +118713,886,Kent,3303,St Katharine's Knockholt Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,201.0,No Special Classes,19-01-2023,170.0,80.0,90.0,4.7,Not applicable,,Not applicable,,Not under a federation,,10069086.0,,Not applicable,16-11-2022,12-09-2023,Main Road,Knockholt,,Sevenoaks,Kent,TN14 7LS,www.knockholt.kent.sch.uk/,1959532237.0,Miss,Sarah-Jane,Tormey,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Halstead, Knockholt and Badgers Mount",Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,546750.0,158787.0,Sevenoaks 008,Sevenoaks 008D,,,,,Good,South-East England and South London,,100061014242.0,,Not applicable,Not applicable,,,E02005094,E01024440,8.0, +118715,886,Kent,3307,"Chevening, St Botolph's Church of England Voluntary Aided Primary School",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,179.0,87.0,92.0,2.8,Not applicable,,Not applicable,,Not under a federation,,10078460.0,,Not applicable,27-11-2019,18-04-2024,Chevening Road,Chipstead,,Sevenoaks,Kent,TN13 2SA,http://www.chevening.kent.sch.uk,1732452895.0,Miss,Karen,Minnis,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Brasted, Chevening and Sundridge",Sevenoaks,(England/Wales) Urban city and town,E10000016,549789.0,156509.0,Sevenoaks 011,Sevenoaks 011A,,,,,Good,South-East England and South London,,10013771396.0,,Not applicable,Not applicable,,,E02005097,E01024416,5.0, +118716,886,Kent,3308,Colliers Green Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,112.0,No Special Classes,19-01-2023,112.0,52.0,60.0,4.5,Not applicable,,Not applicable,,Not under a federation,,10069085.0,,Not applicable,07-03-2019,11-01-2024,Colliers Green,,,Cranbrook,Kent,TN17 2LR,www.colliers-green.kent.sch.uk/,1580211335.0,Dr,Josephine,Hopkins,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Goudhurst and Lamberhurst,Maidstone and The Weald,(England/Wales) Rural hamlet and isolated dwellings,E10000016,575881.0,138768.0,Tunbridge Wells 011,Tunbridge Wells 011E,,,,,Good,South-East England and South London,,10000064641.0,,Not applicable,Not applicable,,,E02005172,E01024806,5.0, +118717,886,Kent,3309,Sissinghurst Voluntary Aided Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,175.0,No Special Classes,19-01-2023,171.0,88.0,83.0,16.4,Not applicable,,Not applicable,,Not under a federation,,10073480.0,,Not applicable,01-03-2023,03-06-2024,Common Road,Sissinghurst,,Cranbrook,Kent,TN17 2BH,www.sissinghurst.kent.sch.uk/,1580713895.0,Mrs,Sarah,Holman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tunbridge Wells,Frittenden and Sissinghurst,Maidstone and The Weald,(England/Wales) Rural village,E10000016,578960.0,137839.0,Tunbridge Wells 013,Tunbridge Wells 013E,,,,,Requires improvement,South-East England and South London,,10000070036.0,,Not applicable,Not applicable,,,E02005174,E01024803,28.0, +118718,886,Kent,3312,Hever Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,205.0,No Special Classes,19-01-2023,152.0,66.0,86.0,17.1,Not applicable,,Not applicable,,Not under a federation,,10069084.0,,Not applicable,23-03-2022,29-04-2024,Hever Road,Hever,,Edenbridge,Kent,TN8 7NH,www.hever.kent.sch.uk/,1732862304.0,Mrs,Helene,Bligh,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Cowden and Hever,Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,547634.0,144758.0,Sevenoaks 015,Sevenoaks 015A,,,,,Requires improvement,South-East England and South London,,100062593515.0,,Not applicable,Not applicable,,,E02005101,E01024420,26.0, +118720,886,Kent,3314,Penshurst Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,84.0,46.0,38.0,13.1,Not applicable,,Not applicable,,Not under a federation,,10069082.0,,Not applicable,17-11-2022,04-06-2024,High Street,Penshurst,,Tonbridge,Kent,TN11 8BX,www.penshurstschool.org.uk,1892870446.0,Mrs,Susan,Elliott,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Penshurst, Fordcombe and Chiddingstone",Tonbridge and Malling,(England/Wales) Rural village,E10000016,552486.0,143627.0,Sevenoaks 015,Sevenoaks 015D,,,,,Good,South-East England and South London,,10035181676.0,,Not applicable,Not applicable,,,E02005101,E01024456,11.0, +118721,886,Kent,3317,"Lady Boswell's Church of England Voluntary Aided Primary School, Sevenoaks",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,436.0,No Special Classes,19-01-2023,425.0,203.0,222.0,4.9,Not applicable,,Not applicable,,Not under a federation,,10079894.0,,Not applicable,25-05-2022,24-05-2024,Plymouth Drive,,,Sevenoaks,Kent,TN13 3RW,http://www.ladyboswells.kent.sch.uk,1732452851.0,Mrs,"Hannah Pullen,",Mrs Sharon Saunders,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Town and St John's,Sevenoaks,(England/Wales) Urban city and town,E10000016,553202.0,155064.0,Sevenoaks 012,Sevenoaks 012F,,,,,Outstanding,South-East England and South London,,10035184700.0,,Not applicable,Not applicable,,,E02005098,E01024471,21.0, +118722,886,Kent,3318,Ide Hill Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,154.0,No Special Classes,19-01-2023,153.0,85.0,68.0,5.9,Not applicable,,Not applicable,,Not under a federation,,10069081.0,,Not applicable,04-04-2019,13-05-2024,Sundridge Road,Ide Hill,,Sevenoaks,Kent,TN14 6JT,https://idehill.eschools.co.uk/,1732750389.0,Miss,Elizabeth,Alexander,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Sevenoaks,"Brasted, Chevening and Sundridge",Sevenoaks,(England/Wales) Rural village,E10000016,548483.0,151962.0,Sevenoaks 013,Sevenoaks 013B,,,,,Good,South-East England and South London,,50002011896.0,,Not applicable,Not applicable,,,E02005099,E01024419,9.0, +118724,886,Kent,3320,St Barnabas CofE VA Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,202.0,96.0,106.0,34.2,Not applicable,,Not applicable,,Not under a federation,,10069080.0,,Not applicable,24-01-2024,20-05-2024,Quarry Road,,,Tunbridge Wells,Kent,TN1 2EY,www.st-barnabas.kent.sch.uk/,1892522958.0,Mrs,Moira,Duncombe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,St James',Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558893.0,140276.0,Tunbridge Wells 008,Tunbridge Wells 008D,,,,,Good,South-East England and South London,,100062543182.0,,Not applicable,Not applicable,,,E02005169,E01024833,69.0, +118725,886,Kent,3322,St James' Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not Recorded,01-01-1900,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,630.0,No Special Classes,19-01-2023,631.0,302.0,329.0,7.8,Not applicable,,Not applicable,,Not under a federation,,10079666.0,,Not applicable,06-03-2024,22-05-2024,Sandrock Road,,,Tunbridge Wells,Kent,TN2 3PR,https://st-james.kent.sch.uk,1892523006.0,Mr,John,Tutt,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Park,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,559230.0,139720.0,Tunbridge Wells 008,Tunbridge Wells 008C,,,,,Good,South-East England and South London,,10000066049.0,,Not applicable,Not applicable,,,E02005169,E01024823,49.0, +118726,886,Kent,3323,Hunton Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,102.0,41.0,61.0,17.6,Not applicable,,Not applicable,,Not under a federation,,10069079.0,,Not applicable,12-05-2021,15-04-2024,Bishops Lane,Hunton,,Maidstone,Kent,ME15 0SJ,http://www.hunton.kent.sch.uk,1622820360.0,Mrs,Anita,Makey,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Coxheath and Hunton,Maidstone and The Weald,(England/Wales) Rural hamlet and isolated dwellings,E10000016,571802.0,149308.0,Maidstone 018,Maidstone 018A,,,,,Good,South-East England and South London,,200003720206.0,,Not applicable,Not applicable,,,E02005085,E01024345,18.0, +118728,886,Kent,3325,Platt Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,173.0,85.0,88.0,16.2,Not applicable,,Not applicable,,Not under a federation,,10069078.0,,Not applicable,24-04-2019,22-02-2024,Platinum Way,St Mary's Platt,,Sevenoaks,Kent,TN15 8FH,http://www.platt.kent.sch.uk,1732882596.0,Mrs,Emma,Smith,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Borough Green & Platt,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,562312.0,157440.0,Tonbridge and Malling 006,Tonbridge and Malling 006B,,,,,Good,South-East England and South London,United Kingdom,10094697223.0,,Not applicable,Not applicable,,,E02005154,E01024724,28.0, +118730,886,Kent,3328,Bapchild and Tonge Church of England Primary School and Nursery,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,245.0,136.0,109.0,11.4,Not applicable,,Not applicable,,Not under a federation,,10069077.0,,Not applicable,17-07-2019,07-05-2024,School Lane,Bapchild,,Sittingbourne,Kent,ME9 9NL,www.bapchildprimary.co.uk,1795424143.0,Mr,Christian,Kelly,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,West Downs,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,592811.0,163003.0,Swale 013,Swale 013D,,,,,Good,South-East England and South London,,10035063550.0,,Not applicable,Not applicable,,,E02005127,E01024629,28.0, +118734,886,Kent,3332,Hartlip Endowed Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,108.0,59.0,49.0,15.7,Not applicable,,Not applicable,,Not under a federation,,10069076.0,,Not applicable,21-04-2022,05-03-2024,The Street,Hartlip,,Sittingbourne,Kent,ME9 7TL,www.hartlip.kent.sch.uk,1795842473.0,Mrs,Tracey,Jerome,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,"Hartlip, Newington and Upchurch",Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,583965.0,164247.0,Swale 008,Swale 008A,,,,,Good,South-East England and South London,,200002532833.0,,Not applicable,Not applicable,,,E02005122,E01024570,17.0, +118735,886,Kent,3337,Tunstall Church of England (Aided) Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,422.0,200.0,222.0,2.1,Not applicable,,Not applicable,,Not under a federation,,10069075.0,,Not applicable,24-05-2023,23-05-2024,Tunstall Road,,,Sittingbourne,Kent,ME10 1YG,http://www.tunstall.kent.sch.uk/,1795472895.0,Mrs,Rebecca,Andrews,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Woodstock,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,589964.0,161884.0,Swale 013,Swale 013E,,,,,Outstanding,South-East England and South London,,,,Not applicable,Not applicable,,,E02005127,E01024631,9.0, +118736,886,Kent,3338,Herne Church of England Junior School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,360.0,No Special Classes,19-01-2023,360.0,184.0,176.0,13.6,Not applicable,,Not applicable,,Not under a federation,,10073479.0,,Not applicable,01-11-2023,25-03-2024,School Lane,Herne,,Herne Bay,Kent,CT6 7AL,http://www.herne-junior.kent.sch.uk,1227374069.0,Mr,Mal,Saunders,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Herne & Broomfield,North Thanet,(England/Wales) Urban city and town,E10000016,618444.0,165965.0,Canterbury 006,Canterbury 006C,,,,,Outstanding,South-East England and South London,,200000683363.0,,Not applicable,Not applicable,,,E02005015,E01024075,49.0, +118737,886,Kent,3339,Whitstable and Seasalter Endowed Church of England Junior School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,228.0,No Special Classes,19-01-2023,210.0,107.0,103.0,16.7,Not applicable,,Not applicable,,Not under a federation,,10073478.0,,Not applicable,07-12-2022,15-04-2024,High Street,,,Whitstable,Kent,CT5 1AY,http://www.whitstable-endowed.kent.sch.uk,1227273630.0,Miss,Ellen,Taylor,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Gorrell,Canterbury,(England/Wales) Urban city and town,E10000016,610765.0,166471.0,Canterbury 008,Canterbury 008C,,,,,Outstanding,South-East England and South London,,100062619665.0,,Not applicable,Not applicable,,,E02005017,E01024070,35.0, +118738,886,Kent,3340,"Ashford, St Mary's Church of England Primary School",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,420.0,215.0,205.0,25.0,Not applicable,,Not applicable,,Not under a federation,,10069074.0,,Not applicable,29-01-2020,25-01-2024,Western Avenue,,,Ashford,Kent,TN23 1ND,www.st-marys-ashford.kent.sch.uk/,1233625531.0,Mrs,Nicola,Hirst,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Victoria,Ashford,(England/Wales) Urban city and town,E10000016,600448.0,143067.0,Ashford 016,Ashford 016C,,,,,Good,South-East England and South London,,100062559492.0,,Not applicable,Not applicable,,,E02007047,E01023992,105.0, +118740,886,Kent,3346,Wittersham Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,140.0,No Special Classes,19-01-2023,141.0,66.0,75.0,17.0,Not applicable,,Not applicable,,Not under a federation,,10069073.0,,Not applicable,28-01-2020,24-04-2024,The Street,Wittersham,,Tenterden,Kent,TN30 7EA,www.wittersham.kent.sch.uk,1797270329.0,Mr,George,Hawkins,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Isle of Oxney,Ashford,(England/Wales) Rural village,E10000016,589703.0,126905.0,Ashford 014,Ashford 014B,,,,,Good,South-East England and South London,,100062567595.0,,Not applicable,Not applicable,,,E02005009,E01023998,24.0, +118741,886,Kent,3347,Elham Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,140.0,No Special Classes,19-01-2023,127.0,58.0,69.0,18.1,Not applicable,,Not applicable,,Not under a federation,,10069072.0,,Not applicable,19-07-2022,25-04-2024,Vicarage Lane,Elham,,Canterbury,Kent,CT4 6TT,http://www.elhamprimary.co.uk,1303840325.0,Mr,Dan,File,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs East,Folkestone and Hythe,(England/Wales) Rural village,E10000016,617633.0,143734.0,Folkestone and Hythe 001,Folkestone and Hythe 001A,,,,,Good,South-East England and South London,,50021810.0,,Not applicable,Not applicable,,,E02005102,E01024490,23.0, +118744,886,Kent,3350,Saltwood CofE Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,222.0,No Special Classes,19-01-2023,208.0,105.0,103.0,7.7,Not applicable,,Not applicable,,Supported by a federation,The Federation of Bodsham Church of England Primary School and Saltwood Church of England Primary School,10069071.0,,Not applicable,11-05-2022,29-01-2024,Grange Road,Saltwood,,Hythe,Kent,CT21 4QS,http://www.saltwood.kent.sch.uk,1303266058.0,Mr,Paul,Newton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Folkestone and Hythe,Hythe,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,615799.0,135758.0,Folkestone and Hythe 008,Folkestone and Hythe 008D,,,,,Good,South-East England and South London,,50017103.0,,Not applicable,Not applicable,,,E02005109,E01024550,16.0, +118745,886,Kent,3351,Ash Cartwright and Kelsey Church of England Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,158.0,89.0,69.0,31.5,Not applicable,,Not applicable,,Not under a federation,,10073477.0,,Not applicable,25-09-2019,15-04-2024,School Road,Ash,,Canterbury,Kent,CT3 2JD,www.ashckschool.org,1304812539.0,Mrs,Fiona,Crascall,Interim Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Little Stour & Ashstone,South Thanet,(England/Wales) Rural town and fringe,E10000016,628453.0,158532.0,Dover 001,Dover 001A,,,,,Good,South-East England and South London,,100062298485.0,,Not applicable,Not applicable,,,E02005041,E01024206,45.0, +118748,886,Kent,3356,"Dover, St Mary's Church of England Primary School",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,155.0,75.0,80.0,66.5,Not applicable,,Not applicable,,Not under a federation,,10069069.0,,Not applicable,16-11-2022,06-06-2024,Laureston Place,,,Dover,Kent,CT16 1QX,www.st-marys-dover.kent.sch.uk/,1304206887.0,Ms,Helen,Comfort,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Town & Castle,Dover,(England/Wales) Urban city and town,E10000016,632228.0,141684.0,Dover 012,Dover 012E,,,,,Requires improvement,South-East England and South London,,100062288992.0,,Not applicable,Not applicable,,,E02005052,E01033209,103.0, +118750,886,Kent,3360,St Peter-in-Thanet CofE Junior School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,360.0,No Special Classes,19-01-2023,365.0,190.0,175.0,32.3,Not applicable,,Not applicable,,Not under a federation,,10073476.0,,Not applicable,11-05-2023,05-06-2024,Grange Road,St Peter's,,Broadstairs,Kent,CT10 3EP,www.stpetersthanet.co.uk,1843861430.0,Mr,Tim,Whitehouse,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Beacon Road,South Thanet,(England/Wales) Urban city and town,E10000016,638767.0,169105.0,Thanet 009,Thanet 009A,,,,,Outstanding,South-East England and South London,,200003081114.0,,Not applicable,Not applicable,,,E02005140,E01024635,118.0, +118751,886,Kent,3364,"Ramsgate, Holy Trinity Church of England Primary School",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,210.0,113.0,97.0,12.4,Not applicable,,Not applicable,,Not under a federation,,10073475.0,,Not applicable,29-09-2021,21-05-2024,Dumpton Park Drive,,,Broadstairs,Kent,CT10 1RR,www.ramsgateholytrinity.co.uk,1843860744.0,Mrs,Erin,Price,Interim Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Viking,South Thanet,(England/Wales) Urban city and town,E10000016,639141.0,166318.0,Thanet 010,Thanet 010E,,,,,Outstanding,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005141,E01024708,26.0, +118754,886,Kent,3373,St Mary's Church of England Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,247.0,128.0,119.0,41.3,Not applicable,,Not applicable,,Not under a federation,,10072082.0,,Not applicable,07-12-2022,12-09-2023,St Mary's Road,,,Swanley,Kent,BR8 7BU,http://www.st-marys-swanley.kent.sch.uk,1322665212.0,Mrs,Amanda,McGarrigle,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Swanley St Mary's,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,550895.0,168371.0,Sevenoaks 002,Sevenoaks 002A,,,,,Good,South-East England and South London,,200002881970.0,,Not applicable,Not applicable,,,E02005088,E01024476,102.0, +118764,886,Kent,3722,St Ethelbert's Catholic Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,209.0,88.0,121.0,33.5,Not applicable,,Not applicable,,Not under a federation,,10072744.0,,Not applicable,13-06-2019,03-06-2024,"St Ethelbert's Catholic Primary School, Dane Park Road",,,Ramsgate,Kent,CT11 7LS,www.stethelbertsschool.co.uk,1843585555.0,Mr,Simon,Marshall,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Eastcliff,South Thanet,(England/Wales) Urban city and town,E10000016,638595.0,165666.0,Thanet 012,Thanet 012A,,,,,Good,South-East England and South London,,100062282199.0,,Not applicable,Not applicable,,,E02005143,E01024669,70.0, +118765,886,Kent,3728,St Anselm's Catholic Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,209.0,91.0,118.0,9.6,Not applicable,,Not applicable,,Not under a federation,,10072743.0,,Not applicable,19-06-2019,21-05-2024,Littlebrook Manor Way,Temple Hill,,Dartford,Kent,DA1 5EA,http://www.st-anselms.kent.sch.uk/,1322225173.0,Mrs,Laura,White,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Temple Hill,Dartford,(England/Wales) Urban major conurbation,E10000016,555174.0,174821.0,Dartford 001,Dartford 001C,,,,,Good,South-East England and South London,,200000531818.0,,Not applicable,Not applicable,,,E02005028,E01024154,20.0, +118768,886,Kent,3733,"Our Lady's Catholic Primary School, Dartford",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,218.0,No Special Classes,19-01-2023,217.0,106.0,111.0,6.5,Not applicable,,Not applicable,,Not under a federation,,10072740.0,,Not applicable,12-02-2020,16-04-2024,King Edward Avenue,,,Dartford,Kent,DA1 2HX,http://www.our-ladys.kent.sch.uk,1322222759.0,Miss,Isabel,Quinn,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,West Hill,Dartford,(England/Wales) Urban major conurbation,E10000016,553550.0,174313.0,Dartford 003,Dartford 003E,,,,,Good,South-East England and South London,,10009429007.0,,Not applicable,Not applicable,,,E02005030,E01024184,14.0, +118777,886,Kent,3749,"St Thomas' Catholic Primary School, Canterbury",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,209.0,108.0,101.0,18.7,Not applicable,,Not applicable,,Not under a federation,,10072737.0,,Not applicable,20-04-2023,12-09-2023,99 Military Road,,,Canterbury,Kent,CT1 1NE,www.st-thomas-canterbury.kent.sch.uk,1227462539.0,Miss,Lisa,D'Agostini,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,615358.0,158108.0,Canterbury 014,Canterbury 014E,,,,,Good,South-East England and South London,,200000678008.0,,Not applicable,Not applicable,,,E02005023,E01024093,39.0, +118785,886,Kent,4026,Dartford Science & Technology College,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,Does not apply,Does not apply,Not applicable,Non-selective,950.0,No Special Classes,19-01-2023,876.0,11.0,865.0,20.8,Not supported by a trust,,Not applicable,,Not under a federation,,10001855.0,,Not applicable,16-03-2022,07-05-2024,Heath Lane,,,Dartford,Kent,DA1 2LY,http://www.dstc.kent.sch.uk,1322224309.0,Miss,Joanne,Sangster,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Princes,Dartford,(England/Wales) Urban major conurbation,E10000016,553232.0,173727.0,Dartford 003,Dartford 003F,,,,,Good,South-East England and South London,,100062308838.0,,Not applicable,Not applicable,,,E02005030,E01024185,155.0, +118788,886,Kent,4040,Northfleet School for Girls,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,Does not apply,Does not apply,Not applicable,Non-selective,1145.0,No Special Classes,19-01-2023,1251.0,30.0,1221.0,33.0,Supported by a trust,Northfleet Schools Co-Operative Trust,Not applicable,,Not under a federation,,10004754.0,,Not applicable,02-03-2022,27-03-2024,Hall Road,Northfleet,,Gravesend,Kent,DA11 8AQ,http://www.nsfg.org.uk,1474831020.0,Mr,C,Norwood,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Painters Ash,Gravesham,(England/Wales) Urban major conurbation,E10000016,562828.0,172592.0,Gravesham 006,Gravesham 006E,,,,,Good,South-East England and South London,,100062310786.0,,Not applicable,Not applicable,,,E02005060,E01024288,334.0, +118789,886,Kent,4043,Tunbridge Wells Girls' Grammar School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1039.0,No Special Classes,19-01-2023,984.0,0.0,984.0,2.5,Not supported by a trust,,Not applicable,,Not under a federation,,10007075.0,,Not applicable,20-09-2023,17-04-2024,Southfield Road,,,Tunbridge Wells,Kent,TN4 9UJ,http://www.twggs.kent.sch.uk/,1892520902.0,Mrs,Linda,Wybar,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558061.0,140816.0,Tunbridge Wells 007,Tunbridge Wells 007D,,,,,Outstanding,South-East England and South London,,100062585962.0,,Not applicable,Not applicable,,,E02005168,E01024838,18.0, +118790,886,Kent,4045,Tunbridge Wells Grammar School for Boys,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,Does not apply,Does not apply,Not applicable,Selective,1308.0,No Special Classes,19-01-2023,1636.0,1586.0,50.0,5.5,Not applicable,,Not applicable,,Not under a federation,,10007076.0,,Not applicable,25-11-2021,21-05-2024,St John's Road,,,Tunbridge Wells,Kent,TN4 9XB,http://www.twgsb.org.uk/,1892529551.0,Ms,Amanda,Simpson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558237.0,141498.0,Tunbridge Wells 002,Tunbridge Wells 002A,,,,,Good,South-East England and South London,,10008662060.0,,Not applicable,Not applicable,,,E02005163,E01024837,71.0, +118806,886,Kent,4109,Dover Grammar School for Girls,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,Does not apply,Does not apply,Not applicable,Selective,885.0,No Special Classes,19-01-2023,882.0,24.0,858.0,15.2,Not applicable,,Not applicable,,Not under a federation,,10002018.0,,Not applicable,15-11-2013,08-04-2024,Frith Road,,,Dover,Kent,CT16 2PZ,http://dggs.kent.sch.uk/,1304206625.0,Mr,Robert,Benson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,St Radigunds,Dover,(England/Wales) Urban city and town,E10000016,631484.0,142361.0,Dover 012,Dover 012D,,,,,Outstanding,South-East England and South London,,100062289176.0,,Not applicable,Not applicable,,,E02005052,E01024247,103.0, +118835,886,Kent,4522,Maidstone Grammar School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1416.0,No Special Classes,19-01-2023,1425.0,1341.0,84.0,5.4,Not supported by a trust,,Not applicable,,Not under a federation,,10004156.0,,Not applicable,16-01-2019,15-04-2024,Barton Road,,,Maidstone,Kent,ME15 7BT,www.mgs.kent.sch.uk,1622752101.0,Mr,Mark,Tomkins,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,High Street,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576678.0,154844.0,Maidstone 010,Maidstone 010A,,,,,Good,South-East England and South London,,200003683439.0,,Not applicable,Not applicable,,,E02005077,E01024371,57.0, +118836,886,Kent,4523,Maidstone Grammar School for Girls,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1240.0,No Special Classes,19-01-2023,1197.0,64.0,1133.0,7.4,Not supported by a trust,,Not applicable,,Not under a federation,,10004157.0,,Not applicable,08-03-2023,29-05-2024,Buckland Road,,,Maidstone,Kent,ME16 0SF,http://www.mggs.org/,1622752103.0,Miss,Deborah,Stanley,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bridge,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575292.0,156397.0,Maidstone 006,Maidstone 006B,,,,,Outstanding,South-East England and South London,,200003670344.0,,Not applicable,Not applicable,,,E02005073,E01024340,65.0, +118840,886,Kent,4534,Simon Langton Girls' Grammar School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1082.0,No Special Classes,19-01-2023,1244.0,33.0,1211.0,6.0,Not applicable,,Not applicable,,Not under a federation,,10005848.0,,Not applicable,27-09-2023,23-04-2024,Old Dover Road,,,Canterbury,Kent,CT1 3EW,http://www.langton.kent.sch.uk/,1227463711.0,Mr,Paul,Pollard,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,616129.0,156292.0,Canterbury 016,Canterbury 016C,,,,,Good,South-East England and South London,,200000682990.0,,Not applicable,Not applicable,,,E02005025,E01024046,53.0, +118843,886,Kent,4622,The Judd School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1261.0,No Special Classes,19-01-2023,1472.0,1283.0,189.0,2.7,Not applicable,,Not applicable,,Not under a federation,,10006720.0,,Not applicable,07-05-2015,23-05-2024,Brook Street,,,Tonbridge,Kent,TN9 2PN,http://judd.online,1732770880.0,Mr,Jonathan,Wood,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,21.0,22.0,,,South East,Tonbridge and Malling,Judd,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558333.0,145666.0,Tonbridge and Malling 013,Tonbridge and Malling 013A,,,,,Outstanding,South-East England and South London,,100062594180.0,,Not applicable,Not applicable,,,E02005161,E01024757,26.0, +118846,886,Kent,5200,Snodland CofE Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,420.0,No Special Classes,19-01-2023,407.0,220.0,187.0,30.2,Not applicable,,Not applicable,,Not under a federation,,10069067.0,,Not applicable,19-10-2022,21-05-2024,Roberts Road,,,Snodland,Kent,ME6 5HL,www.snodland.kent.sch.uk,1634241251.0,Mrs,Holley,Hunt,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Snodland West & Holborough Lakes,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,569746.0,161866.0,Tonbridge and Malling 002,Tonbridge and Malling 002E,,,,,Good,South-East England and South London,,200000958756.0,,Not applicable,Not applicable,,,E02005150,E01024773,123.0, +118847,886,Kent,5201,Borough Green Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,315.0,No Special Classes,19-01-2023,265.0,131.0,134.0,22.3,Not supported by a trust,,Not applicable,,Not under a federation,,10069648.0,,Not applicable,07-03-2024,22-05-2024,School Approach,Borough Green,,Sevenoaks,Kent,TN15 8JZ,http://www.bgpschool.kent.sch.uk,1732883459.0,Mrs,Karen,Jackson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Borough Green & Platt,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,561085.0,157405.0,Tonbridge and Malling 006,Tonbridge and Malling 006C,,,,,Good,South-East England and South London,,200000965163.0,,Not applicable,Not applicable,,,E02005154,E01024725,59.0, +118849,886,Kent,5203,Roseacre Junior School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,396.0,No Special Classes,19-01-2023,423.0,217.0,206.0,3.3,Not supported by a trust,,Not applicable,,Not under a federation,,10069647.0,,Not applicable,03-11-2022,05-06-2024,The Landway,Bearsted,,Maidstone,Kent,ME14 4BL,http://www.roseacre.kent.sch.uk,1622737843.0,Mr,Duncan,Garrett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bearsted,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,579372.0,155693.0,Maidstone 005,Maidstone 005A,,,,,Outstanding,South-East England and South London,,200003691727.0,,Not applicable,Not applicable,,,E02005072,E01024330,14.0, +118852,886,Kent,5206,Herne Bay Junior School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,500.0,No Special Classes,19-01-2023,426.0,210.0,216.0,42.0,Not supported by a trust,,Not applicable,,Not under a federation,,10069646.0,,Not applicable,29-01-2020,09-05-2024,Kings Road,,,Herne Bay,Kent,CT6 5DA,http://www.hernebay-jun.kent.sch.uk/,1227374608.0,Mrs,Melody,Kingman,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Heron,North Thanet,(England/Wales) Urban city and town,E10000016,617911.0,167922.0,Canterbury 001,Canterbury 001A,,,,,Good,South-East England and South London,,200000697542.0,,Not applicable,Not applicable,,,E02005010,E01024078,179.0, +118853,886,Kent,5207,"St Francis' Catholic Primary School, Maidstone",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,420.0,No Special Classes,19-01-2023,418.0,194.0,224.0,15.3,Not applicable,,Not applicable,,Not under a federation,,10072733.0,,Not applicable,18-09-2018,05-06-2024,Queen's Road,,,Maidstone,Kent,ME16 0LB,www.st-francis.kent.sch.uk,1622771540.0,Mrs,Elisabeth,Blanden,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bridge,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574157.0,155660.0,Maidstone 003,Maidstone 003F,,,,,Good,South-East England and South London,,200003717931.0,,Not applicable,Not applicable,,,E02005070,E01024341,64.0, +118858,886,Kent,5212,Ditton Infant School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,174.0,85.0,89.0,24.1,Not supported by a trust,,Not applicable,,Not under a federation,,10078307.0,,Not applicable,05-10-2022,24-01-2024,Pear Tree Avenue,Ditton,,Aylesford,Kent,ME20 6EB,www.ditton-inf.kent.sch.uk/,1732844107.0,,Claire,Lewer,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford South & Ditton,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,571309.0,158026.0,Tonbridge and Malling 005,Tonbridge and Malling 005C,,,,,Good,South-East England and South London,,200000961077.0,,Not applicable,Not applicable,,,E02005153,E01024735,42.0, +118859,886,Kent,5213,"Holy Trinity Church of England Primary School, Dartford",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,420.0,No Special Classes,19-01-2023,423.0,214.0,209.0,15.6,Not applicable,,Not applicable,,Not under a federation,,10069066.0,,Not applicable,03-02-2023,29-05-2024,Chatsworth Road,,,Dartford,Kent,DA1 5AF,http://www.holytrinitydartford.co.uk,1322224474.0,Mrs,Vikki,Wall,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Burnham,Dartford,(England/Wales) Urban major conurbation,E10000016,553399.0,174917.0,Dartford 003,Dartford 003A,,,,,Good,South-East England and South London,,200000530383.0,,Not applicable,Not applicable,,,E02005030,E01024180,66.0, +118860,886,Kent,5214,"St Bartholomew's Catholic Primary School, Swanley",Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,350.0,No Special Classes,19-01-2023,324.0,158.0,166.0,19.4,Not applicable,,Not applicable,,Not under a federation,,10072732.0,,Not applicable,05-05-2022,07-05-2024,Sycamore Drive,,,Swanley,Kent,BR8 7AY,http://www.st-bartholomewsrc-pri.kent.sch.uk/,1322663119.0,Mrs,Giovanna,McRae,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Swanley White Oak,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,551351.0,168878.0,Sevenoaks 002,Sevenoaks 002D,,,,,Good,South-East England and South London,,100062276753.0,,Not applicable,Not applicable,,,E02005088,E01024480,63.0, +118864,886,Kent,5218,Greatstone Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,364.0,No Special Classes,19-01-2023,300.0,141.0,159.0,24.6,Not supported by a trust,,Not applicable,,Supported by a federation,The Lightyear Federation,10069645.0,,Not applicable,25-05-2022,15-05-2024,Baldwin Road,Greatstone,,New Romney,Kent,TN28 8SY,www.greatstoneschool.co.uk,1797363916.0,Mr,Matt,Rawling,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Walland & Denge Marsh,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,607768.0,122481.0,Folkestone and Hythe 013,Folkestone and Hythe 013A,,,,,Good,South-East England and South London,,50003937.0,,Not applicable,Not applicable,,,E02005114,E01024532,67.0, +118867,886,Kent,5221,Wincheap Foundation Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,445.0,Not applicable,19-01-2023,432.0,219.0,213.0,31.5,Not supported by a trust,,Not applicable,,Not under a federation,,10073718.0,,Not applicable,09-12-2021,24-04-2024,Hollow Lane,,,Canterbury,Kent,CT1 3SD,www.wincheap.kent.sch.uk,1227464134.0,Mrs,Nicola,Dawson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,17.0,20.0,,,South East,Canterbury,Wincheap,Canterbury,(England/Wales) Urban city and town,E10000016,614237.0,156789.0,Canterbury 019,Canterbury 019D,,,,,Good,South-East England and South London,,10033162353.0,,Not applicable,Not applicable,,,E02006855,E01035310,136.0, +118869,886,Kent,5223,Brookfield Junior School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,256.0,No Special Classes,19-01-2023,246.0,128.0,118.0,31.3,Not applicable,,Not applicable,,Supported by a federation,Flourish,10079029.0,,Not applicable,29-03-2023,23-04-2024,Brookfield Junior School,Swallow Road,Larkfield,Aylesford,Kent,ME20 6PY,www.flourishfederation.co.uk,1732843667.0,Mr,Nathaniel,South,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Larkfield,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,570167.0,158770.0,Tonbridge and Malling 003,Tonbridge and Malling 003F,,,,,Good,South-East England and South London,,100062628494.0,,Not applicable,Not applicable,,,E02005151,E01024765,77.0, +118871,886,Kent,5225,Harcourt Primary School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,178.0,87.0,91.0,34.3,Not supported by a trust,,Not applicable,,Not under a federation,,10069644.0,,Not applicable,06-10-2021,13-04-2024,Biggins Wood Road,,,Folkestone,Kent,CT19 4NE,www.harcourt.kent.sch.uk/,1303275294.0,Mr,Anthony,Silk,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,620022.0,137406.0,Folkestone and Hythe 002,Folkestone and Hythe 002A,,,,,Good,South-East England and South London,,50030583.0,,Not applicable,Not applicable,,,E02005103,E01024492,61.0, +118879,886,Kent,5407,Thamesview School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,910.0,Has Special Classes,19-01-2023,955.0,498.0,457.0,40.0,Not supported by a trust,,Not applicable,,Not under a federation,,10006569.0,,Not applicable,20-06-2018,03-06-2024,Thong Lane,,,Gravesend,Kent,DA12 4LF,http://www.thamesviewsch.co.uk/,1474566552.0,Mr,George,Rorke,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,,,,,South East,Gravesham,Riverview Park,Gravesham,(England/Wales) Urban major conurbation,E10000016,566808.0,172052.0,Gravesham 008,Gravesham 008B,,,,,Good,South-East England and South London,,100062312649.0,,Not applicable,Not applicable,,,E02005062,E01024298,354.0, +118884,886,Kent,5412,Simon Langton Grammar School for Boys,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1280.0,No Special Classes,19-01-2023,1247.0,1117.0,130.0,5.0,Not supported by a trust,,Not applicable,,Not under a federation,,10005849.0,,Not applicable,14-11-2013,28-05-2024,Langton Lane,Nackington Road,,Canterbury,Kent,CT4 7AS,http://www.thelangton.org.uk/,1227463567.0,Dr,Ken,Moffat,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,41.0,35.0,,,South East,Canterbury,Wincheap,Canterbury,(England/Wales) Urban city and town,E10000016,615323.0,155825.0,Canterbury 019,Canterbury 019E,,,,,Outstanding,South-East England and South London,,,,Not applicable,Not applicable,,,E02006855,E01035311,39.0, +118897,886,Kent,5425,The Malling School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1003.0,No Special Classes,19-01-2023,1010.0,547.0,463.0,23.0,Supported by a trust,The Malling Holmesdale Federation,Not applicable,,Not under a federation,,10006748.0,,Not applicable,29-03-2023,04-06-2024,Beech Road,East Malling,,West Malling,Kent,ME19 6DH,www.themallingschool.kent.sch.uk,1732840995.0,Mr,John,Vennart,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,,,,,,,,,,,,Resourced provision,124.0,121.0,,,South East,Tonbridge and Malling,"East Malling, West Malling & Offham",Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,569778.0,157407.0,Tonbridge and Malling 014,Tonbridge and Malling 014A,,,,,Good,South-East England and South London,,200000960428.0,,Not applicable,Not applicable,,,E02006833,E01024740,208.0, +118898,886,Kent,5426,The Archbishop's School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Non-selective,900.0,No Special Classes,19-01-2023,722.0,382.0,340.0,45.6,Not supported by a trust,,Not applicable,,Not under a federation,,10006581.0,,Not applicable,23-11-2023,23-04-2024,St Stephens Hill,,,Canterbury,Kent,CT2 7AP,http://www.archbishops-school.co.uk/,1227765805.0,Mr,David,Elliott,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,SpLD - Specific Learning Difficulty,VI - Visual Impairment,OTH - Other Difficulty/Disability,HI - Hearing Impairment,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,"SEMH - Social, Emotional and Mental Health",PD - Physical Disability,MLD - Moderate Learning Difficulty,,,,,Resourced provision,7.0,7.0,,,South East,Canterbury,St Stephen's,Canterbury,(England/Wales) Urban city and town,E10000016,614417.0,159483.0,Canterbury 013,Canterbury 013B,,,,,Good,South-East England and South London,,100062619198.0,,Not applicable,Not applicable,,,E02005022,E01024100,284.0, +118919,886,Kent,5447,St George's Church of England Foundation School,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,All-through,4.0,19,No boarders,No Nursery Classes,Has a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Non-selective,1375.0,No Special Classes,19-01-2023,1633.0,788.0,845.0,26.9,Not supported by a trust,,Not applicable,,Not under a federation,,10006163.0,,Not applicable,13-06-2019,21-05-2024,Westwood Road,,,Broadstairs,Kent,CT10 2LH,http://www.stgeorges-school.org.uk/,1843861696.0,Mr,Adam,Mirams,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,St Peters,South Thanet,(England/Wales) Urban city and town,E10000016,637425.0,167972.0,Thanet 011,Thanet 011E,,,,,Good,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005142,E01024691,404.0, +118928,886,Kent,5456,Northfleet Technology College,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Non-selective,989.0,No Special Classes,19-01-2023,903.0,900.0,3.0,29.2,Supported by a trust,Northfleet Schools Co-Operative Trust,Not applicable,,Not under a federation,,10004755.0,,Not applicable,22-09-2022,21-05-2024,Colyer Road,Northfleet,,Gravesend,Kent,DA11 8BG,http://www.ntc.kent.sch.uk/,1474533802.0,Mr,Steven,Gallears,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Northfleet & Springhead,Gravesham,(England/Wales) Urban major conurbation,E10000016,563003.0,173050.0,Gravesham 006,Gravesham 006B,,,,,Good,South-East England and South London,,10012024936.0,,Not applicable,Not applicable,,,E02005060,E01024281,219.0, +118931,886,Kent,5459,Dover Grammar School for Boys,Foundation school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,880.0,No Special Classes,19-01-2023,835.0,822.0,13.0,15.3,Not supported by a trust,,Not applicable,,Not under a federation,,10002017.0,,Not applicable,16-10-2019,21-05-2024,Astor Avenue,,,Dover,Kent,CT17 0DQ,http://www.dgsb.co.uk,1304206117.0,Mr,Philip,Horstrup,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Tower Hamlets,Dover,(England/Wales) Urban city and town,E10000016,630305.0,141711.0,Dover 011,Dover 011H,,,,,Good,South-East England and South London,,10034874352.0,,Not applicable,Not applicable,,,E02005051,E01024248,103.0, +118933,886,Kent,5461,St John's Catholic Comprehensive,Voluntary aided school,Local authority maintained schools,"Open, but proposed to close",Not applicable,,Academy Converter,31-08-2024,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,1184.0,No Special Classes,19-01-2023,1294.0,663.0,631.0,22.5,Not applicable,,Not applicable,,Not under a federation,,10006200.0,,Not applicable,15-05-2018,06-06-2024,Rochester Road,,,Gravesend,Kent,DA12 2JW,http://www.stjohnscs.com,1474534718.0,Mr,Matthew,Barron,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Denton,Gravesham,(England/Wales) Urban major conurbation,E10000016,565876.0,173278.0,Gravesham 003,Gravesham 003C,,,,,Good,South-East England and South London,,100062311894.0,,Not applicable,Not applicable,,,E02005057,E01024293,218.0, +118937,886,Kent,6000,Ashford School,Other independent school,Independent schools,Open,Not applicable,01-01-1918,Not applicable,,Not applicable,,18,Boarding school,Has Nursery Classes,Has a sixth form,Mixed,Christian,Christian,Not applicable,Not applicable,1084.0,No Special Classes,20-01-2022,1050.0,550.0,500.0,0.0,Not applicable,,Not applicable,,Not applicable,,10008077.0,,Not applicable,,28-03-2024,East Hill,,,Ashford,Kent,TN24 8PB,https://www.ashfordschool.co.uk,1233625171.0,Mr,Michael,Hall,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Ashford,Victoria,Ashford,(England/Wales) Urban city and town,E10000016,601413.0,142875.0,Ashford 005,Ashford 005G,ISI,281.0,40.0,,,South-East England and South London,,100062560535.0,,Not applicable,Not applicable,,,E02005000,E01034986,0.0, +118938,886,Kent,6001,Wellesley Haddon Dene School,Other independent school,Independent schools,Open,Not applicable,01-01-1929,Not applicable,,Not applicable,2.0,13,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Not applicable,320.0,No Special Classes,20-01-2022,210.0,126.0,84.0,0.0,Not applicable,,Not applicable,,Not applicable,,10015812.0,,Not applicable,,22-04-2024,114 Ramsgate Road,Broadstairs,Kent,,,CT10 2DG,www.wellesleyhouse.org,1843862991.0,Mrs,Joanne,Parpworth,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Thanet,Viking,South Thanet,(England/Wales) Urban city and town,E10000016,638726.0,167390.0,Thanet 010,Thanet 010C,ISI,3.0,20.0,Alpha Schools,,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005141,E01024706,0.0, +118939,886,Kent,6002,Benenden School,Other independent school,Independent schools,Open,Not applicable,01-01-1926,Not applicable,,Not applicable,10.0,19,Boarding school,No Nursery Classes,Has a sixth form,Girls,None,None,Not applicable,Not applicable,620.0,No Special Classes,20-01-2022,546.0,0.0,546.0,0.0,Not applicable,,Not applicable,,Not applicable,,10014830.0,,Not applicable,,07-05-2024,Cranbrook Road,Benenden,,Cranbrook,Kent,TN17 4AA,http://www.benenden.school,1580240592.0,Mrs,S,Price,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Benenden and Cranbrook,Maidstone and The Weald,(England/Wales) Rural village,E10000016,580246.0,133803.0,Tunbridge Wells 014,Tunbridge Wells 014A,ISI,,127.0,Benenden School (Kent) Ltd,,South-East England and South London,,100062552564.0,,Not applicable,Not applicable,,,E02005175,E01024789,0.0, +118940,886,Kent,6003,Dover College,Other independent school,Independent schools,Open,Not applicable,01-01-1909,Not applicable,,Not applicable,3.0,18,Boarding school,Has Nursery Classes,Has a sixth form,Mixed,Church of England,Church of England,Not applicable,Non-selective,478.0,No Special Classes,20-01-2022,313.0,172.0,141.0,0.0,Not applicable,,Not applicable,,Not applicable,,10008199.0,,Not applicable,,03-05-2024,Effingham Crescent,,,Dover,Kent,CT17 9RH,www.dovercollege.org.uk,1304205969.0,Mr,Simon,Fisher,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Dover,Town & Castle,Dover,(England/Wales) Urban city and town,E10000016,631537.0,141684.0,Dover 013,Dover 013B,ISI,,110.0,Corporation of Dover College,,South-East England and South London,,100062290526.0,,Not applicable,Not applicable,,,E02005053,E01024215,0.0, +118941,886,Kent,6004,Northbourne Park School,Other independent school,Independent schools,Open,Not applicable,01-01-1942,Not applicable,,Not applicable,2.0,13,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Church of England,Not applicable,Non-selective,205.0,No Special Classes,20-01-2022,193.0,99.0,94.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018503.0,,Not applicable,,30-01-2024,Betteshanger,,,DEAL,Kent,CT14 0NW,http://www.northbournepark.com,1304611215.0,Mr,Mark,Hammond,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Dover,Eastry Rural,Dover,(England/Wales) Rural village,E10000016,631045.0,152538.0,Dover 005,Dover 005A,ISI,,38.0,,,South-East England and South London,,10034873947.0,,Not applicable,Not applicable,,,E02005045,E01024201,0.0, +118942,886,Kent,6005,Marlborough House School,Other independent school,Independent schools,Open,Not applicable,01-01-1932,Not applicable,,Not applicable,2.0,13,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,Christian,Not applicable,Non-selective,350.0,No Special Classes,20-01-2022,248.0,119.0,129.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018722.0,,Not applicable,,04-06-2024,Hawkhurst,,,Cranbrook,Kent,TN18 4PY,www.marlboroughhouseschool.co.uk,1580753555.0,Mr,Eddy,Newton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Hawkhurst and Sandhurst,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,575350.0,130678.0,Tunbridge Wells 014,Tunbridge Wells 014E,ISI,,63.0,Marlborough House Sch Trust Ltd,,South-East England and South London,,100062106196.0,,Not applicable,Not applicable,,,E02005175,E01024810,0.0, +118943,886,Kent,6006,St Ronan's School,Other independent school,Independent schools,Open,Not applicable,01-01-1917,Not applicable,,Not applicable,2.0,13,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Non-selective,468.0,No Special Classes,20-01-2022,455.0,241.0,214.0,0.0,Not applicable,,Not applicable,,Not applicable,,10017665.0,,Not applicable,,04-06-2024,Water Lane,Hawkhurst,,Cranbrook,Kent,TN18 5DJ,www.saintronans.co.uk,1580752271.0,Mr,William,Trelawney-Vernon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Hawkhurst and Sandhurst,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,577801.0,130807.0,Tunbridge Wells 014,Tunbridge Wells 014E,ISI,,85.0,St Ronan's (Hawkhurst),,South-East England and South London,,10024135777.0,,Not applicable,Not applicable,,,E02005175,E01024810,0.0, +118944,886,Kent,6007,Gad's Hill School,Other independent school,Independent schools,Open,Not applicable,01-01-1948,Not applicable,,Not applicable,3.0,16,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,755.0,No Special Classes,20-01-2022,384.0,194.0,190.0,0.0,Not applicable,,Not applicable,,Not applicable,,10015390.0,,Not applicable,,15-05-2024,Higham,,,Rochester,Kent,ME3 7PA,www.gadshill.org,1474822366.0,Mr,Paul,Savage,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Gravesham,Higham & Shorne,Gravesham,(England/Wales) Rural town and fringe,E10000016,570991.0,170882.0,Gravesham 010,Gravesham 010B,ISI,6.0,30.0,Gad's Hill School,,South-East England and South London,,10012012444.0,,Not applicable,Not applicable,,,E02005064,E01024266,0.0, +118946,886,Kent,6009,Kent College Pembury,Other independent school,Independent schools,Open,Not applicable,01-01-1933,Not applicable,,Not applicable,3.0,20,Boarding school,Has Nursery Classes,Has a sixth form,Girls,Methodist,Methodist,Not applicable,Selective,678.0,No Special Classes,20-01-2022,512.0,19.0,493.0,0.0,Not applicable,,Not applicable,,Not applicable,,10008307.0,,Not applicable,,08-05-2024,Old Church Road,Pembury,,Tunbridge Wells,Kent,TN2 4AX,http://www.kent-college.co.uk/,1892822006.0,Miss,Katrina,Handford,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Pembury,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,562692.0,143044.0,Tunbridge Wells 004,Tunbridge Wells 004B,ISI,7.0,155.0,Methodist Independent Schools Trust,,South-East England and South London,,10008667752.0,,Not applicable,Not applicable,,,E02005165,E01024825,0.0, +118947,886,Kent,6010,St Lawrence College,Other independent school,Independent schools,Open,Not applicable,01-01-1912,Not applicable,,Not applicable,10.0,18,Boarding school,No Nursery Classes,Has a sixth form,Mixed,Christian/Evangelical,Church of England,Not applicable,Not applicable,500.0,No Special Classes,20-01-2022,409.0,218.0,191.0,0.0,Not applicable,,Not applicable,,Not applicable,,10017219.0,,Not applicable,,18-04-2024,College Road,,,Ramsgate,Kent,CT11 7AE,www.slcuk.com,1843572900.0,Mr,Barney,Durrant,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Thanet,Eastcliff,South Thanet,(England/Wales) Urban city and town,E10000016,637926.0,165987.0,Thanet 015,Thanet 015C,ISI,1.0,,Corporation of St Lawrence College,,South-East England and South London,,100062281951.0,,Not applicable,Not applicable,,,E02005146,E01024668,0.0, +118949,886,Kent,6011,Beechwood School,Other independent school,Independent schools,Open,Not applicable,01-01-1943,Not applicable,,Not applicable,3.0,19,Boarding school,Has Nursery Classes,Has a sixth form,Mixed,None,Christian,Not applicable,Non-selective,420.0,No Special Classes,20-01-2022,309.0,161.0,148.0,0.0,Not applicable,,Not applicable,,Not applicable,,10014837.0,,Not applicable,,07-06-2024,12 Pembury Road,,,Tunbridge Wells,Kent,TN2 3QD,http://www.beechwood.org.uk,1892532747.0,Mr,Justin,Foster-Gandey,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Park,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,560091.0,139893.0,Tunbridge Wells 009,Tunbridge Wells 009B,ISI,,64.0,Alpha Schools,,South-East England and South London,,100062554131.0,,Not applicable,Not applicable,,,E02005170,E01024822,0.0, +118950,886,Kent,6012,Holmewood House School,Other independent school,Independent schools,Open,Not applicable,01-01-1951,Not applicable,,Not applicable,2.0,13,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,540.0,No Special Classes,20-01-2022,449.0,242.0,207.0,0.0,Not applicable,,Not applicable,,Not applicable,,10015872.0,,Not applicable,,07-06-2024,Barrow Lane,Langton Green,,Tunbridge Wells,Kent,TN3 0EB,www.holmewoodhouse.co.uk,1892860000.0,Mrs,Ruth,O'Sullivan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Speldhurst and Bidborough,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,555212.0,138621.0,Tunbridge Wells 006,Tunbridge Wells 006C,ISI,4.0,139.0,,,South-East England and South London,,100062108061.0,,Not applicable,Not applicable,,,E02005167,E01024852,0.0, +118951,886,Kent,6013,Rose Hill School,Other independent school,Independent schools,Open,Not applicable,01-01-1949,Not applicable,,Not applicable,3.0,13,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,320.0,No Special Classes,20-01-2022,265.0,154.0,111.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018845.0,,Not applicable,,15-05-2024,Coniston Avenue,,,Tunbridge Wells,Kent,TN4 9SY,,1892525591.0,Ms,Emma,Neville,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Culverden,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,557231.0,140251.0,Tunbridge Wells 007,Tunbridge Wells 007B,ISI,6.0,82.0,Grange Rose Hill School Ltd,,South-East England and South London,,100062585961.0,,Not applicable,Not applicable,,,E02005168,E01024800,0.0, +118952,886,Kent,6014,Sevenoaks School,Other independent school,Independent schools,Open,Not applicable,01-01-1918,Not applicable,,Not applicable,11.0,18,Boarding school,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Not applicable,1250.0,No Special Classes,20-01-2022,1181.0,624.0,557.0,0.0,Not applicable,,Not applicable,,Not applicable,,10005765.0,,Not applicable,,08-05-2024,High Street,Sevenoaks,Kent,Sevenoaks,Kent,TN13 1HU,www.sevenoaksschool.org,1732455133.0,Mr,Jesse,Elzinga,Head,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Town and St John's,Sevenoaks,(England/Wales) Urban city and town,E10000016,553207.0,154098.0,Sevenoaks 012,Sevenoaks 012F,ISI,,169.0,,,South-East England and South London,,100062546708.0,,Not applicable,Not applicable,,,E02005098,E01024471,0.0, +118953,886,Kent,6015,Sevenoaks Preparatory School,Other independent school,Independent schools,Open,Not applicable,26-03-1958,Not applicable,,Not applicable,3.0,14,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Non-selective,444.0,No Special Classes,20-01-2022,381.0,181.0,200.0,0.0,Not applicable,,Not applicable,,Not applicable,,10017399.0,,Not applicable,,09-04-2024,Fawke Cottage,Godden Green,,Sevenoaks,Kent,TN15 0JU,http://www.theprep.org.uk,1732762336.0,Mr,Luke,Harrison,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Seal and Weald,Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,555440.0,154482.0,Sevenoaks 012,Sevenoaks 012A,ISI,,63.0,,,South-East England and South London,,10035182256.0,,Not applicable,Not applicable,,,E02005098,E01024458,0.0, +118954,886,Kent,6016,St Michael's Prep School,Other independent school,Independent schools,Open,Not applicable,01-01-1945,Not applicable,,Not applicable,2.0,13,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Non-selective,488.0,No Special Classes,20-01-2022,476.0,244.0,232.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018594.0,,Not applicable,,02-05-2024,St Michael's Preparatory School,Otford Court,Row Dow,Otford,,TN14 5RY,www.stmichaels.kent.sch.uk,1959522137.0,Mr,Nik,Pears,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Otford and Shoreham,Sevenoaks,(England/Wales) Rural town and fringe,E10000016,554260.0,159647.0,Sevenoaks 009,Sevenoaks 009D,ISI,2.0,71.0,,,South-East England and South London,United Kingdom,10035184625.0,,Not applicable,Not applicable,,,E02005095,E01024453,0.0, +118955,886,Kent,6017,The New Beacon School,Other independent school,Independent schools,Open,Not applicable,01-01-1917,Not applicable,,Not applicable,2.0,14,Boarding school,Has Nursery Classes,Does not have a sixth form,Boys,None,None,Not applicable,Non-selective,397.0,No Special Classes,20-01-2022,348.0,347.0,1.0,0.0,Not applicable,,Not applicable,,Not applicable,,10017253.0,,Not applicable,,29-04-2024,Brittains Lane,,,Sevenoaks,Kent,TN13 2PB,www.newbeacon.org.uk,1732452131.0,Mrs,Sarah,Brownsdon,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Kippington,Sevenoaks,(England/Wales) Urban city and town,E10000016,552009.0,153717.0,Sevenoaks 011,Sevenoaks 011E,ISI,1.0,77.0,Tonbridge School of High Street,,South-East England and South London,,100062074386.0,,Not applicable,Not applicable,,,E02005097,E01024464,0.0, +118957,886,Kent,6018,Radnor House Sevenoaks,Other independent school,Independent schools,Open,New Provision,26-02-1997,Not applicable,,Not applicable,2.0,18,No boarders,Has Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Selective,750.0,No Special Classes,20-01-2022,533.0,308.0,225.0,0.0,Not applicable,,Not applicable,,Not applicable,,10008175.0,,Not applicable,,25-04-2024,Combe Bank Drive,Sevenoaks,,,Kent,TN14 6AE,www.radnor-sevenoaks.org,1959563720.0,Mr,David,Paton,Head,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Brasted, Chevening and Sundridge",Sevenoaks,(England/Wales) Rural village,E10000016,548115.0,155824.0,Sevenoaks 013,Sevenoaks 013A,ISI,,37.0,Radnor House Sevenoaks (Holdings) Ltd,,South-East England and South London,United Kingdom,10035181671.0,,Not applicable,Not applicable,,,E02005099,E01024417,0.0, +118958,886,Kent,6019,Sutton Valence School,Other independent school,Independent schools,Open,Not applicable,01-01-1908,Not applicable,,Not applicable,2.0,19,Boarding school,Has Nursery Classes,Has a sixth form,Mixed,Christian,Christian,Not applicable,Not applicable,880.0,No Special Classes,20-01-2022,868.0,516.0,352.0,0.0,Not applicable,,Not applicable,,Not applicable,,10017608.0,,Not applicable,,15-05-2024,North Street,Sutton Valence,,Maidstone,Kent,ME17 3HL,http://www.svs.org.uk/index.html,1622845203.0,Mr,James,Thomas,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Maidstone,Sutton Valence and Langley,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,581234.0,149347.0,Maidstone 017,Maidstone 017D,ISI,2.0,29.0,United Westminster Grey Coat Foundation,,South-East England and South London,,200003719418.0,,Not applicable,Not applicable,,,E02005084,E01024411,0.0, +118959,886,Kent,6020,Tonbridge School,Other independent school,Independent schools,Open,Not applicable,01-01-1918,Not applicable,,Not applicable,13.0,18,Boarding school,No Nursery Classes,Has a sixth form,Boys,None,Church of England,Not applicable,Not applicable,820.0,No Special Classes,20-01-2022,794.0,794.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10006936.0,,Not applicable,,10-05-2024,,,,Tonbridge,Kent,TN9 1JP,www.tonbridge-school.co.uk,1732365555.0,Mr,James,Priory,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Judd,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559065.0,147021.0,Tonbridge and Malling 012,Tonbridge and Malling 012A,ISI,,150.0,,,South-East England and South London,,200000969928.0,,Not applicable,Not applicable,,,E02005160,E01024732,0.0, +118960,886,Kent,6021,Somerhill,Other independent school,Independent schools,Open,Not applicable,01-01-1935,Not applicable,,Not applicable,2.0,13,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Christian,Not applicable,Non-selective,654.0,No Special Classes,20-01-2022,608.0,383.0,225.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018591.0,,Not applicable,,21-05-2024,Tudeley Road,Tonbridge,Kent,,,TN11 0NJ,www.somerhill.org,1732352124.0,Mr,Duncan,Sinclair,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Capel,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,560772.0,145340.0,Tunbridge Wells 001,Tunbridge Wells 001A,ISI,,41.0,Somerhill Charitable Trust Limited,,South-East England and South London,,10008663663.0,,Not applicable,Not applicable,,,E02005162,E01024798,0.0, +118965,886,Kent,6024,Steephill School,Other independent school,Independent schools,Open,Not applicable,09-10-1957,Not applicable,,Not applicable,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Non-selective,125.0,No Special Classes,20-01-2022,132.0,59.0,73.0,0.0,Not applicable,,Not applicable,,Not applicable,,10070236.0,,Not applicable,,03-04-2024,Off Castle Hill,Fawkham,,Longfield,Kent,DA3 7BG,www.steephill.co.uk,1474702107.0,Mr,John,Abbott,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Fawkham and West Kingsdown,Sevenoaks,(England/Wales) Urban city and town,E10000016,559847.0,168076.0,Sevenoaks 007,Sevenoaks 007C,ISI,1.0,38.0,,,South-East England and South London,,10035182828.0,,Not applicable,Not applicable,,,E02005093,E01024437,0.0, +118967,886,Kent,6026,Bronte School,Other independent school,Independent schools,Open,Not applicable,17-10-1957,Not applicable,,Not applicable,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Not applicable,160.0,No Special Classes,20-01-2022,148.0,72.0,76.0,0.0,Not applicable,,Not applicable,,Not applicable,,10070235.0,,Not applicable,,04-06-2024,7 Pelham Road,,,Gravesend,Kent,DA11 0HN,,1474533805.0,Mrs,Emma,Wood,Headmistress,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Gravesham,Pelham,Gravesham,(England/Wales) Urban major conurbation,E10000016,564341.0,173911.0,Gravesham 002,Gravesham 002C,ISI,1.0,18.0,Nicholas Clements,,South-East England and South London,,200001873251.0,,Not applicable,Not applicable,,,E02005056,E01024290,0.0, +118971,886,Kent,6029,The Granville School,Other independent school,Independent schools,Open,Not applicable,31-03-1958,Not applicable,,Not applicable,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Inter- / non- denominational,Not applicable,Non-selective,215.0,No Special Classes,20-01-2022,162.0,7.0,155.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080538.0,,Not applicable,,29-05-2024,2 Bradbourne Park Road,,,Sevenoaks,Kent,TN13 3LJ,www.granvilleschool.org,1732453039.0,Mrs,Louise,Lawrance,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Town and St John's,Sevenoaks,(England/Wales) Urban city and town,E10000016,552345.0,155661.0,Sevenoaks 012,Sevenoaks 012E,ISI,,13.0,,,South-East England and South London,,100062546713.0,,Not applicable,Not applicable,,,E02005098,E01024470,0.0, +118973,886,Kent,6031,Hilden Grange School,Other independent school,Independent schools,Open,Not applicable,01-01-1957,Not applicable,,Not applicable,2.0,13,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Selective,370.0,No Special Classes,20-01-2022,298.0,193.0,105.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018656.0,,Not applicable,,11-04-2024,Dry Hill Park Road,,,Tonbridge,Kent,TN10 3BX,www.hildengrange.co.uk,1732352706.0,Mr,Malcolm,Gough,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Cage Green & Angel,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558953.0,147481.0,Tonbridge and Malling 012,Tonbridge and Malling 012A,ISI,1.0,84.0,Inspired Education Group,,South-East England and South London,,100062543570.0,,Not applicable,Not applicable,,,E02005160,E01024732,0.0, +118974,886,Kent,6032,Hilden Oaks Preparatory School and Nursery,Other independent school,Independent schools,Open,Not applicable,17-10-1957,Not applicable,,Not applicable,,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,222.0,No Special Classes,20-01-2022,166.0,70.0,96.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080536.0,,Not applicable,,22-05-2024,38 Dry Hill Park Road,,,Tonbridge,Kent,TN10 3BU,www.hildenoaks.co.uk,1732353941.0,Mrs,Katy,Joiner,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Cage Green & Angel,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558953.0,147481.0,Tonbridge and Malling 012,Tonbridge and Malling 012B,ISI,,27.0,Derick Walker,,South-East England and South London,,100062543569.0,,Not applicable,Not applicable,,,E02005160,E01024733,0.0, +118975,886,Kent,6033,The Mead School,Other independent school,Independent schools,Open,Not applicable,16-10-1957,Not applicable,,Not applicable,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Christian,Christian,Not applicable,Non-selective,250.0,No Special Classes,20-01-2022,238.0,111.0,127.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080535.0,,Not applicable,,10-04-2024,16 Frant Road,,,Tunbridge Wells,Kent,TN2 5SN,www.themeadschool.co.uk,1892525837.0,Mrs,Catherine,Openshaw,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Pantiles and St Mark's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558193.0,138430.0,Tunbridge Wells 012,Tunbridge Wells 012B,ISI,2.0,19.0,The Mead School Ltd,,South-East England and South London,,10000066049.0,,Not applicable,Not applicable,,,E02005173,E01024817,0.0, +118977,886,Kent,6035,Chartfield School,Other independent school,Independent schools,Open,Not applicable,08-10-1957,Not applicable,,Not applicable,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Christian,Not applicable,Non-selective,84.0,No Special Classes,20-01-2022,42.0,22.0,20.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080534.0,,Not applicable,21-09-2023,20-02-2024,45 Minster Road,,,Westgate-on-Sea,Kent,CT8 8DA,www.chartfieldschool.org.uk,1843831716.0,Miss,Sarah,Neale,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Thanet,Westgate-on-Sea,North Thanet,(England/Wales) Urban city and town,E10000016,632551.0,169556.0,Thanet 007,Thanet 007E,Ofsted,,5.0,Chartfield School Ltd,Requires improvement,South-East England and South London,,,,Not applicable,Not applicable,,,E02005138,E01024716,0.0, +118978,886,Kent,6036,Bethany School,Other independent school,Independent schools,Open,Not applicable,06-11-1951,Not applicable,,Not applicable,11.0,18,Boarding school,No Nursery Classes,Has a sixth form,Mixed,Christian,Christian,Not applicable,Non-selective,450.0,No Special Classes,20-01-2022,348.0,234.0,114.0,0.0,Not applicable,,Not applicable,,Not applicable,,10013351.0,,Not applicable,,24-04-2024,Goudhurst,,,Cranbrook,Kent,TN17 1LB,http://www.bethanyschool.org.uk,1580211273.0,Mr,Francie,Healy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Goudhurst and Lamberhurst,Tunbridge Wells,(England/Wales) Rural hamlet and isolated dwellings,E10000016,573928.0,140377.0,Tunbridge Wells 011,Tunbridge Wells 011E,ISI,,240.0,Bethany School Limited,,South-East England and South London,,10008665682.0,,Not applicable,Not applicable,,,E02005172,E01024806,0.0, +118981,886,Kent,6038,Solefield School,Other independent school,Independent schools,Open,Not applicable,07-10-1957,Not applicable,,Not applicable,3.0,13,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England,Not applicable,Non-selective,190.0,No Special Classes,20-01-2022,151.0,151.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018776.0,,Not applicable,,08-05-2024,Solefields Road,,,Sevenoaks,Kent,TN13 1PH,http://www.solefieldschool.org,1732452142.0,Mrs,Helen,McClure,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Kippington,Sevenoaks,(England/Wales) Urban city and town,E10000016,553072.0,153650.0,Sevenoaks 012,Sevenoaks 012C,ISI,4.0,55.0,,,South-East England and South London,,,,Not applicable,Not applicable,,,E02005098,E01024462,0.0, +118984,886,Kent,6039,Russell House School,Other independent school,Independent schools,Open,Not applicable,01-01-1952,Not applicable,,Not applicable,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,218.0,No Special Classes,20-01-2022,183.0,88.0,95.0,0.0,Not applicable,,Not applicable,,Not applicable,,10071116.0,,Not applicable,,23-05-2024,Station Road,Otford,,Sevenoaks,Kent,TN14 5QU,www.russellhouseschool.co.uk,1959522352.0,Mr,Craig,McCarthy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Otford and Shoreham,Sevenoaks,(England/Wales) Rural hamlet and isolated dwellings,E10000016,552991.0,159400.0,Sevenoaks 009,Sevenoaks 009D,ISI,1.0,12.0,Dr Yvonne Lindsay RHS Ltd,,South-East England and South London,,100062621727.0,,Not applicable,Not applicable,,,E02005095,E01024453,0.0, +118986,886,Kent,6040,St Lawrence College Junior School,Other independent school,Independent schools,Open,Not applicable,01-01-1954,Not applicable,,Not applicable,2.0,11,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England/Christian,Not applicable,Not applicable,220.0,No Special Classes,20-01-2022,160.0,98.0,62.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080532.0,,Not applicable,,07-05-2024,College Road,,,Ramsgate,Kent,CT11 7AF,https://www.slcuk.com/,1843572912.0,Mrs,Ellen,Rowe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Thanet,Eastcliff,South Thanet,(England/Wales) Urban city and town,E10000016,637926.0,165987.0,Thanet 015,Thanet 015C,ISI,,36.0,Corporation of St Lawrence College,,South-East England and South London,,100062099015.0,,Not applicable,Not applicable,,,E02005146,E01024668,0.0, +118990,886,Kent,6043,The Dulwich School Cranbrook,Other independent school,Independent schools,Open,Not applicable,01-01-1920,Not applicable,,Not applicable,2.0,16,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,Christian,Not applicable,Non-selective,592.0,No Special Classes,20-01-2022,343.0,175.0,168.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018413.0,,Not applicable,,08-05-2024,Coursehorn,,,CRANBROOK,Kent,TN17 3NP,www.dulwichcranbrook.org,1580712179.0,Mrs,Sophie,Bradshaw,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Benenden and Cranbrook,Maidstone and The Weald,(England/Wales) Rural hamlet and isolated dwellings,E10000016,579266.0,135858.0,Tunbridge Wells 013,Tunbridge Wells 013A,ISI,1.0,119.0,Dulwich Prep Cranbrook,,South-East England and South London,,10000064585.0,,Not applicable,Not applicable,,,E02005174,E01024787,0.0, +118991,886,Kent,6044,Cobham Hall,Other independent school,Independent schools,Open,Not applicable,18-10-1962,Not applicable,,Not applicable,11.0,19,Boarding school,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,250.0,No Special Classes,20-01-2022,137.0,1.0,136.0,0.0,Not applicable,,Not applicable,,Not applicable,,10001524.0,,Not applicable,,23-04-2024,Cobham Hall,Brewers Road,Gravesend,,Kent,DA12 3BL,www.cobhamhall.com,1474823371.0,Headteacher,Wendy,Barrett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Gravesham,"Istead Rise, Cobham & Luddesdown",Gravesham,(England/Wales) Rural hamlet and isolated dwellings,E10000016,568367.0,168915.0,Gravesham 010,Gravesham 010E,ISI,3.0,27.0,The Mill Hill School Foundation,,South-East England and South London,United Kingdom,10012014498.0,,Not applicable,Not applicable,,,E02005064,E01024301,0.0, +118992,886,Kent,6045,Spring Grove School 2003 Ltd,Other independent school,Independent schools,Open,Not applicable,03-11-1967,Not applicable,,Not applicable,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,245.0,No Special Classes,20-01-2022,225.0,107.0,118.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080530.0,,Not applicable,,08-05-2024,Harville Road,Wye,,Ashford,Kent,TN25 5EZ,www.springgroveschool.co.uk,1233812337.0,Mrs,Therésa,Jaggard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Ashford,Wye with Hinxhill,Ashford,(England/Wales) Rural hamlet and isolated dwellings,E10000016,604242.0,146688.0,Ashford 001,Ashford 001E,ISI,1.0,19.0,,,South-East England and South London,,200004393352.0,,Not applicable,Not applicable,,,E02004996,E01024041,0.0, +118996,886,Kent,6048,The King's School Canterbury,Other independent school,Independent schools,Open,Not applicable,01-01-1908,Not applicable,,Not applicable,13.0,18,Boarding school,No Nursery Classes,Has a sixth form,Mixed,Church of England,Church of England,Not applicable,Selective,960.0,No Special Classes,20-01-2022,943.0,485.0,458.0,0.0,Not applicable,,Not applicable,,Not applicable,,10008320.0,,Not applicable,,08-05-2024,25 The Precincts,,,Canterbury,Kent,CT1 2ES,www.kings-school.co.uk,1227595501.0,Ms,Jude,Lowson,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Canterbury,Westgate,Canterbury,(England/Wales) Urban city and town,E10000016,615194.0,158056.0,Canterbury 020,Canterbury 020D,ISI,,220.0,The King’s School Governors,,South-East England and South London,,100062279503.0,,Not applicable,Not applicable,,,E02006856,E01024124,0.0, +118998,886,Kent,6050,St Edmund's School Canterbury,Other independent school,Independent schools,Open,Not applicable,01-01-1918,Not applicable,,Not applicable,2.0,19,Boarding school,Has Nursery Classes,Has a sixth form,Mixed,Church of England,Christian,Not applicable,Not applicable,670.0,No Special Classes,20-01-2022,631.0,335.0,296.0,0.0,Not applicable,,Not applicable,,Not applicable,,10013984.0,,Not applicable,,26-04-2024,St Thomas Hill,,,Canterbury,Kent,CT2 8HU,http://www.stedmunds.org.uk/,1227475600.0,Mr,Edward,O'Connor,Head,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,Not applicable,,,,,South East,Canterbury,Blean Forest,Canterbury,(England/Wales) Urban city and town,E10000016,613410.0,159177.0,Canterbury 012,Canterbury 012E,ISI,11.0,80.0,,,South-East England and South London,,100062619208.0,,Not applicable,Not applicable,,,E02005021,E01024123,0.0, +119001,886,Kent,6053,Kent College (Canterbury),Other independent school,Independent schools,Open,Not applicable,30-09-1980,Not applicable,,Not applicable,11.0,19,Boarding school,Not applicable,Has a sixth form,Mixed,Methodist,Methodist,Not applicable,Not applicable,600.0,Has Special Classes,20-01-2022,574.0,311.0,263.0,0.0,Not applicable,,Not applicable,,Not applicable,,10016349.0,,Not applicable,,23-04-2024,Whitstable Road,,,Canterbury,Kent,CT2 9DT,,1227763231.0,Mr,Mark,Turnbull,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Canterbury,Blean Forest,Canterbury,(England/Wales) Urban city and town,E10000016,613117.0,159367.0,Canterbury 012,Canterbury 012D,ISI,3.0,36.0,Methodist Independent Schools Trust,,South-East England and South London,,100062293085.0,,Not applicable,Not applicable,,,E02005021,E01024068,0.0, +119002,886,Kent,6054,Walthamstow Hall,Other independent school,Independent schools,Open,Not applicable,30-09-1980,Not applicable,,Not applicable,2.0,19,No boarders,Has Nursery Classes,Has a sixth form,Mixed,None,Christian,Not applicable,Not applicable,700.0,No Special Classes,20-01-2022,549.0,0.0,549.0,0.0,Not applicable,,Not applicable,,Not applicable,,10007324.0,,Not applicable,,30-04-2024,Holly Bush Lane,,,Sevenoaks,Kent,TN13 3UL,www.walthamstow-hall.co.uk,1732451334.0,Ms,Louise,Chamberlain,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Eastern,Sevenoaks,(England/Wales) Urban city and town,E10000016,553388.0,155609.0,Sevenoaks 010,Sevenoaks 010C,ISI,84.0,,,,South-East England and South London,,100062547957.0,,Not applicable,Not applicable,,,E02005096,E01024461,0.0, +119007,886,Kent,6058,Sackville School,Other independent school,Independent schools,Open,Not applicable,17-09-1987,Not applicable,,Not applicable,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Not applicable,242.0,No Special Classes,20-01-2022,206.0,158.0,48.0,0.0,Not applicable,,Not applicable,,Not applicable,,10017557.0,,Not applicable,,10-04-2024,Tonbridge Road,Hildenborough,,Tonbridge,Kent,TN11 9HN,www.sackvilleschool.co.uk,1732838888.0,Mrs,Leoni,Ellis,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Hildenborough,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,556338.0,148693.0,Tonbridge and Malling 010,Tonbridge and Malling 010D,ISI,51.0,23.0,,,South-East England and South London,,200000963023.0,,Not applicable,Not applicable,,,E02005158,E01024754,0.0, +119008,886,Kent,6059,St Faith's At Ash School Limited,Other independent school,Independent schools,Open,Not applicable,28-10-1987,Not applicable,,Not applicable,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,260.0,No Special Classes,20-01-2022,228.0,116.0,112.0,0.0,Not applicable,,Not applicable,,Not applicable,,10075226.0,,Not applicable,,22-04-2024,5 The Street,Ash,,Canterbury,Kent,CT3 2HH,www.stfaithsprep.com,1304813409.0,Mrs,Helen,Coombs,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,SpLD - Specific Learning Difficulty,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,MSI - Multi-Sensory Impairment,,,,,,,,,,Resourced provision,28.0,30.0,,,South East,Dover,Little Stour & Ashstone,South Thanet,(England/Wales) Rural town and fringe,E10000016,628411.0,158389.0,Dover 001,Dover 001B,ISI,4.0,19.0,St Faith's At Ash School Ltd,,South-East England and South London,,100060886066.0,,Not applicable,Not applicable,,,E02005041,E01024207,0.0, +119010,886,Kent,6061,Junior King's School,Other independent school,Independent schools,Open,Not applicable,16-10-1989,Not applicable,,Not applicable,2.0,14,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Church of England,Not applicable,Not applicable,386.0,No Special Classes,20-01-2022,381.0,209.0,172.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018042.0,,Not applicable,,08-05-2024,Milner Court,Sturry,,Canterbury,Kent,CT2 0AY,www.junior-kings.co.uk,1227714000.0,Mrs,Emma,Karolyi,Head,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Canterbury,Sturry,Canterbury,(England/Wales) Rural town and fringe,E10000016,617596.0,160193.0,Canterbury 011,Canterbury 011E,ISI,1.0,16.0,,,South-East England and South London,,200000682998.0,,Not applicable,Not applicable,,,E02005020,E01024113,0.0, +119014,886,Kent,6064,Lorenden Preparatory School,Other independent school,Independent schools,Open,Not applicable,12-10-1993,Not applicable,,Not applicable,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Christian,Not applicable,Non-selective,140.0,No Special Classes,20-01-2022,120.0,60.0,60.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080527.0,,Not applicable,,12-04-2024,Painters Forstal Road,Painters Forstal,,,,ME13 0EN,www.lorenden.org,1795590030.0,Mr,Richard,McIntosh,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Swale,East Downs,Faversham and Mid Kent,(England/Wales) Rural hamlet and isolated dwellings,E10000016,599351.0,159200.0,Swale 016,Swale 016A,ISI,1.0,19.0,Methodist Independent Schools Trust,,South-East England and South London,,200002534108.0,,Not applicable,Not applicable,,,E02005130,E01024565,0.0, +119020,886,Kent,6069,"Kent College Nursery, Infant and Junior School",Other independent school,Independent schools,Open,Not applicable,23-09-1980,Not applicable,,Not applicable,3.0,11,Boarding school,Has Nursery Classes,Does not have a sixth form,Mixed,None,Church of England/Methodist,Not applicable,Non-selective,240.0,Has Special Classes,20-01-2022,222.0,122.0,100.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080525.0,,Not applicable,,23-04-2024,Harbledown,Canterbury,,Canterbury,Kent,CT2 9AQ,,1227762436.0,Mr,Simon,James,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Canterbury,Chartham & Stone Street,Canterbury,(England/Wales) Rural village,E10000016,612417.0,158066.0,Canterbury 012,Canterbury 012C,ISI,,22.0,,,South-East England and South London,,200000690375.0,,Not applicable,Not applicable,,,E02005021,E01024067,0.0, +130938,886,Kent,2682,New Ash Green Primary School,Community school,Local authority maintained schools,Open,Not applicable,01-09-1997,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,413.0,212.0,201.0,13.3,Not applicable,,Not applicable,,Not under a federation,,10075572.0,,Not applicable,25-02-2022,01-02-2024,North Square,New Ash Green,,Longfield,Kent,DA3 8JT,http://www.new-ash.kent.sch.uk,1474873858.0,Mrs,Caroline,Cain,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Ash and New Ash Green,Sevenoaks,(England/Wales) Urban city and town,E10000016,560624.0,165724.0,Sevenoaks 016,Sevenoaks 016A,,,,,Good,South-East England and South London,,100062314601.0,,Not applicable,Not applicable,,,E02006832,E01024413,55.0, +130948,886,Kent,3298,St. Edmund's Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,01-09-1996,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,Not applicable,19-01-2023,169.0,88.0,81.0,30.2,Not applicable,,Not applicable,,Supported by a federation,The Compass Federation,10075571.0,,Not applicable,14-11-2018,09-04-2024,Fawkham Road,West Kingsdown,,Sevenoaks,Kent,TN15 6JP,www.st-edmunds.kent.sch.uk,1474853484.0,Mr,Benjamin,Hulme,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Fawkham and West Kingsdown,Sevenoaks,(England/Wales) Rural town and fringe,E10000016,558036.0,162680.0,Sevenoaks 007,Sevenoaks 007A,,,,,Good,South-East England and South London,,100062550147.0,,Not applicable,Not applicable,,,E02005093,E01024435,51.0, +130952,886,Kent,2680,Kings Hill School Primary and Nursery,Community school,Local authority maintained schools,Open,Not applicable,01-09-1997,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,485.0,229.0,256.0,7.4,Not applicable,,Not applicable,,Not under a federation,,10075570.0,,Not applicable,21-02-2024,20-05-2024,Crispin Way,Kings Hill,,West Malling,Kent,ME19 4LS,www.kingshillschool.org.uk/,1732842739.0,Miss,Lottie,Barnden,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Kings Hill,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,567411.0,155232.0,Tonbridge and Malling 007,Tonbridge and Malling 007F,,,,,Outstanding,South-East England and South London,,10002912179.0,,Not applicable,Not applicable,,,E02005155,E01032826,36.0, +131020,886,Kent,3902,Hythe Bay CofE Primary School,Voluntary controlled school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2006,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,Has Special Classes,19-01-2023,297.0,170.0,127.0,38.7,Not applicable,,Not applicable,,Not under a federation,,10075218.0,,Not applicable,25-01-2023,09-04-2024,Cinque Ports Avenue,,,Hythe,Kent,CT21 6HS,www.hythebay.kent.sch.uk,1303267802.0,Mrs,Carolyn,Chivers,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,21.0,22.0,,,South East,Folkestone and Hythe,Hythe,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,615731.0,134394.0,Folkestone and Hythe 010,Folkestone and Hythe 010B,,,,,Good,South-East England and South London,,50113532.0,,Not applicable,Not applicable,,,E02005111,E01024524,115.0, +131181,886,Kent,6073,Beech Grove School,Other independent school,Independent schools,Open,Not applicable,21-02-1997,Not applicable,,Not applicable,4.0,19,Boarding school,No Nursery Classes,Has a sixth form,Mixed,Christian,Christian,Not applicable,Selective,120.0,Not applicable,20-01-2022,83.0,48.0,35.0,0.0,Not applicable,,Not applicable,,Not applicable,,10043893.0,,Not applicable,06-12-2019,03-06-2024,Forest Drive,,Nonington,Dover,Kent,CT15 4FB,beechgroveschool.co.uk,1304842980.0,Mr,Jeffrey,Maendel,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Dover,"Aylesham, Eythorne & Shepherdswell",Dover,(England/Wales) Rural village,E10000016,626361.0,152647.0,Dover 006,Dover 006A,Ofsted,,5.0,Church Communities UK,Good,South-East England and South London,,100060912465.0,,Not applicable,Not applicable,,,E02005046,E01024190,0.0, +131411,886,Kent,6075,The Worthgate School,Other independent school,Independent schools,Open,Not applicable,06-11-1997,Not applicable,,Not applicable,13.0,22,Boarding school,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Selective,500.0,Not applicable,20-01-2022,304.0,136.0,168.0,0.0,Not applicable,,Not applicable,,Not applicable,,10008526.0,,Not applicable,,13-05-2024,68 New Dover Road,,,Canterbury,Kent,CT1 3LQ,www.worthgateschool.com,1227866540.0,Dr,Ian,Gross,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,615887.0,156832.0,Canterbury 016,Canterbury 016C,ISI,,14.0,CEG Colleges Limited,,South-East England and South London,,100062280087.0,,Not applicable,Not applicable,,,E02005025,E01024046,0.0, +131567,886,Kent,6113,St Helens Montessori School,Other independent school,Independent schools,Open,New Provision,13-04-2006,Not applicable,,Not applicable,2.0,12,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Christian,Not applicable,Not applicable,130.0,Not applicable,20-01-2022,45.0,19.0,26.0,0.0,Not applicable,,Not applicable,,Not applicable,,10071874.0,,Not applicable,08-07-2021,15-04-2024,Lower Road,East Farleigh,,Maidstone,Kent,ME15 0JT,www.sthelensmontessori.co.uk,1622721731.0,Miss,Jeannelle,Dening Smitherman,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Maidstone,Coxheath and Hunton,Maidstone and The Weald,(England/Wales) Rural village,E10000016,572488.0,153442.0,Maidstone 014,Maidstone 014B,Ofsted,,1.0,Marie-Elise Jeannelle Dening-Smitherman,Good,South-East England and South London,,200003673434.0,,Not applicable,Not applicable,,,E02005081,E01024346,0.0, +132764,886,Kent,2689,The Craylands School,Community school,Local authority maintained schools,Open,New Provision,01-09-2003,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,417.0,224.0,193.0,23.7,Not applicable,,Not applicable,,Not under a federation,,10074200.0,,Not applicable,25-09-2019,01-05-2024,Craylands Lane,,,Swanscombe,Kent,DA10 0LP,http://www.craylands.kent.sch.uk,1322388230.0,Mr,Kris,Hiscock,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Swanscombe,Dartford,(England/Wales) Urban major conurbation,E10000016,560051.0,174464.0,Dartford 004,Dartford 004D,,,,,Good,South-East England and South London,,10002021725.0,,Not applicable,Not applicable,,,E02005031,E01024178,99.0, +133177,886,Kent,3904,Castle Hill Community Primary School,Community school,Local authority maintained schools,Open,Fresh Start,01-01-2007,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,406.0,Has Special Classes,19-01-2023,406.0,220.0,186.0,54.8,Not applicable,,Not applicable,,Not under a federation,,10076676.0,,Not applicable,13-10-2021,21-05-2024,Sidney Street,,,Folkestone,Kent,CT19 6HG,www.castlehill.kent.sch.uk/,1303251583.0,Mr,Peter,Talbot,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,,,,,,,,,,,,,Resourced provision,15.0,12.0,,,South East,Folkestone and Hythe,East Folkestone,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,623192.0,137077.0,Folkestone and Hythe 004,Folkestone and Hythe 004A,,,,,Requires improvement,South-East England and South London,,50049270.0,,Not applicable,Not applicable,,,E02005105,E01024499,194.0, +133627,886,Kent,3299,The John Wesley Church of England Methodist Voluntary Aided Primary School,Voluntary aided school,Local authority maintained schools,Open,New Provision,01-09-2007,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England/Methodist,Does not apply,Diocese of Canterbury,Not applicable,420.0,Not applicable,19-01-2023,462.0,231.0,231.0,20.1,Not applicable,,Not applicable,,Not under a federation,,10069422.0,,Not applicable,11-11-2021,18-03-2024,Wesley School Road,Cuckoo Lane,Singleton,Ashford,Kent,TN23 5LW,www.john-wesley.org.uk,1233614660.0,Miss,Rachael,Harrington,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,10.0,14.0,,,South East,Ashford,Washford,Ashford,(England/Wales) Urban city and town,E10000016,598815.0,141017.0,Ashford 007,Ashford 007E,,,,,Good,South-East England and South London,,10012841657.0,,Not applicable,Not applicable,,,E02005002,E01024017,93.0, +133961,886,Kent,3893,Phoenix Community Primary School,Foundation school,Local authority maintained schools,Open,Result of Amalgamation,01-04-2003,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,213.0,95.0,118.0,43.7,Supported by a trust,CARE Foundation Trust,Not applicable,,Not under a federation,,10074158.0,,Not applicable,29-06-2022,12-03-2024,Belmont Road,Kennington,,Ashford,Kent,TN24 9LS,www.phoenix-primary.kent.sch.uk/,1233622510.0,Mr,Leon,Robichaud,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Bybrook,Ashford,(England/Wales) Urban city and town,E10000016,601583.0,144521.0,Ashford 015,Ashford 015C,,,,,Good,South-East England and South London,,100062561305.0,,Not applicable,Not applicable,,,E02007046,E01023984,93.0, +134057,886,Kent,2065,The Discovery School,Community school,Local authority maintained schools,Open,New Provision,01-09-2003,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,625.0,317.0,308.0,7.5,Not applicable,,Not applicable,,Not under a federation,,10074152.0,,Not applicable,22-02-2023,22-05-2024,Discovery Drive,Kings Hill,,West Malling,Kent,ME19 4GJ,www.discovery.kent.sch.uk,1732847000.0,Miss,Tina,Gobell,Interim Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Kings Hill,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,568757.0,155312.0,Tonbridge and Malling 007,Tonbridge and Malling 007G,,,,,Outstanding,South-East England and South London,,10002911481.0,,Not applicable,Not applicable,,,E02005155,E01032827,47.0, +134452,886,Kent,6104,OneSchool Global UK - Maidstone Campus,Other independent school,Independent schools,Open,New Provision,29-08-2003,Not applicable,,Not applicable,7.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Plymouth Brethren Christian Church,Christian,Not applicable,Not applicable,225.0,Not applicable,20-01-2022,163.0,81.0,82.0,0.0,Not applicable,,Not applicable,,Not applicable,,10018081.0,,Not applicable,,06-06-2024,Heath Road,Linton,,Maidstone,Kent,ME17 4HT,https://www.oneschoolglobal.com/campus/united-kingdom/maidstone/,3000700507.0,Mrs,Keryn,Van Der Westhuizen,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Maidstone,Coxheath and Hunton,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575491.0,154914.0,Maidstone 018,Maidstone 018A,ISI,2.0,10.0,OneSchool Global UK,,South-East England and South London,,10014308592.0,,Not applicable,Not applicable,,,E02005085,E01024345,0.0, +134515,886,Kent,3896,Downsview Community Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-01-2004,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,174.0,97.0,77.0,39.7,Not applicable,,Not applicable,,Not under a federation,,10074133.0,,Not applicable,26-04-2023,13-09-2023,Beech Avenue,,,Swanley,Kent,BR8 8AU,www.downsview-primary.kent.sch.uk/,1322662594.0,Mr,Richard,Moore,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Swanley Christchurch and Swanley Village,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,552107.0,168558.0,Sevenoaks 003,Sevenoaks 003B,,,,,Requires improvement,South-East England and South London,,100062276972.0,,Not applicable,Not applicable,,,E02005089,E01024473,69.0, +134582,886,Kent,6097,Kent College International Study Centre,Other independent school,Independent schools,Open,New Provision,20-08-2003,Not applicable,,Not applicable,11.0,18,Boarding school,No Nursery Classes,Has a sixth form,Mixed,None,Methodist,Not applicable,Not applicable,100.0,Not applicable,20-01-2022,18.0,14.0,4.0,0.0,Not applicable,,Not applicable,,Not applicable,,10016222.0,,Not applicable,,23-04-2024,Whitstable Road,,,Canterbury,Kent,CT2 9DT,,1227763231.0,Mr,Mark,Turnbull,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Canterbury,Blean Forest,Canterbury,(England/Wales) Urban city and town,E10000016,613117.0,159367.0,Canterbury 012,Canterbury 012D,ISI,,,Methodist Secondary Trustees,,South-East England and South London,,100062293085.0,,Not applicable,Not applicable,,,E02005021,E01024068,0.0, +134857,886,Kent,3898,Greenfields Community Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,11-04-2005,Not applicable,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,367.0,Not applicable,19-01-2023,420.0,241.0,179.0,40.3,Not applicable,,Not applicable,,Not under a federation,,10071741.0,,Not applicable,15-05-2019,11-04-2024,Oxford Road,Shepway,,Maidstone,Kent,ME15 8DF,http://www.greenfieldscps.kent.sch.uk,1622758538.0,Mr,Daniel,Andrews,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Shepway North,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,578070.0,153905.0,Maidstone 010,Maidstone 010D,,,,,Good,South-East England and South London,,200003717312.0,,Not applicable,Not applicable,,,E02005077,E01024395,143.0, +135106,886,Kent,3906,Palace Wood Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-01-2007,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,417.0,225.0,192.0,16.5,Not applicable,,Not applicable,,Not under a federation,,10072360.0,,Not applicable,15-09-2022,13-09-2023,Ash Grove,Allington,,Maidstone,Kent,ME16 0AB,www.palacewoodprimary.org.uk/,1622750084.0,Mrs,Clare,Cairns,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,Allington,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574102.0,156747.0,Maidstone 003,Maidstone 003D,,,,,Good,South-East England and South London,,200003659713.0,,Not applicable,Not applicable,,,E02005070,E01024323,69.0, +135118,886,Kent,3907,Hextable Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2007,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,609.0,320.0,289.0,17.9,Not applicable,,Not applicable,,Not under a federation,,10072357.0,,Not applicable,27-09-2023,04-06-2024,Rowhill Road,Hextable,,Swanley,Kent,BR8 7RL,www.hextable-primary.kent.sch.uk/,1322663792.0,Mrs,Suzie,Hall,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Hextable,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,551783.0,170689.0,Sevenoaks 001,Sevenoaks 001A,,,,,Good,South-East England and South London,,10013768802.0,,Not applicable,Not applicable,,,E02005087,E01024445,109.0, +135125,886,Kent,3909,Ashford Oaks Community Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2008,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,460.0,238.0,222.0,51.0,Not applicable,,Not applicable,,Not under a federation,,10071720.0,,Not applicable,29-03-2023,15-04-2024,Oak Tree Road,,,Ashford,Kent,TN23 4QR,http://www.ashfordoaks.kent.sch.uk/,1223631259.0,Mr,Phil,Chantler,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,"SEMH - Social, Emotional and Mental Health",MLD - Moderate Learning Difficulty,,,,,,,,,,Resourced provision,13.0,8.0,,,South East,Ashford,Beaver,Ashford,(England/Wales) Urban city and town,E10000016,599912.0,141837.0,Ashford 007,Ashford 007B,,,,,Good,South-East England and South London,,100062559273.0,,Not applicable,Not applicable,,,E02005002,E01023975,221.0, +135130,886,Kent,3910,Joy Lane Primary Foundation School,Foundation school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2007,Not applicable,,Primary,1.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Has Special Classes,19-01-2023,619.0,327.0,292.0,24.7,Supported by a trust,The Coastal Alliance Co-operative Trust,Not applicable,,Not under a federation,,10076634.0,,Not applicable,19-10-2018,02-04-2024,Joy Lane,,,Whitstable,Kent,CT5 4LT,www.joylane.kent.sch.uk/,1227261430.0,Ms,Debra,Hines,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,34.0,30.0,,,South East,Canterbury,Seasalter,Canterbury,(England/Wales) Urban city and town,E10000016,610128.0,165477.0,Canterbury 008,Canterbury 008E,,,,,Good,South-East England and South London,,200002882935.0,,Not applicable,Not applicable,,,E02005017,E01024106,153.0, +135164,886,Kent,3913,Rusthall St Paul's CofE VA Primary School,Voluntary aided school,Local authority maintained schools,"Open, but proposed to close",Result of Amalgamation,01-09-2007,Academy Converter,31-08-2024,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,150.0,No Special Classes,19-01-2023,138.0,79.0,59.0,31.9,Not applicable,,Not applicable,,Not under a federation,,10072353.0,,Not applicable,20-04-2023,13-05-2024,High Street,Rusthall,,Tunbridge Wells,Kent,TN4 8RZ,www.rusthall-cep.kent.sch.uk,1892520582.0,Mrs,Lyndsay,Smurthwaite,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Rusthall,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,555920.0,139686.0,Tunbridge Wells 010,Tunbridge Wells 010C,,,,,Requires improvement,South-East England and South London,,10008659425.0,,Not applicable,Not applicable,,,E02005171,E01024830,44.0, +135197,886,Kent,3916,Green Park Community Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-08-2007,Not applicable,,Primary,4.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,390.0,206.0,184.0,45.6,Not applicable,,Not applicable,,Not under a federation,,10076633.0,,Not applicable,25-05-2023,17-04-2024,The Linces,Buckland,Green Park Community Primary School,Dover,Kent,CT16 2BN,www.greenparkcps.co.uk/,1304822663.0,Mr,Richard,Hawkins,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,,,,,South East,Dover,Buckland,Dover,(England/Wales) Urban city and town,E10000016,630734.0,143609.0,Dover 011,Dover 011D,,,,,Outstanding,South-East England and South London,,100062289415.0,,Not applicable,Not applicable,,,E02005051,E01024196,178.0, +135212,886,Kent,3917,Garlinge Primary School and Nursery,Foundation school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2007,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,840.0,No Special Classes,19-01-2023,769.0,406.0,363.0,41.6,Supported by a trust,Thanet Endeavour Learning Trust,Not applicable,,Not under a federation,,10076631.0,,Not applicable,29-11-2023,08-05-2024,Westfield Road,,,Margate,Kent,CT9 5PA,www.garlingeprimary.co.uk,1843221877.0,Mr,James,Williams,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,PD - Physical Disability,,,,,,,,,,,,Resourced provision,22.0,23.0,,,South East,Thanet,Garlinge,North Thanet,(England/Wales) Urban city and town,E10000016,633980.0,169704.0,Thanet 005,Thanet 005C,,,,,Good,South-East England and South London,,100062307988.0,,Not applicable,Not applicable,,,E02005136,E01024674,310.0, +135214,886,Kent,3918,Newington Community Primary School,Community school,Local authority maintained schools,"Open, but proposed to close",Result of Amalgamation,01-09-2007,Academy Converter,30-06-2024,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,694.0,No Special Classes,19-01-2023,703.0,370.0,333.0,43.3,Not applicable,,Not applicable,,Not under a federation,,10071712.0,,Not applicable,16-03-2017,16-05-2024,Princess Margaret Avenue,,,Ramsgate,Kent,CT12 6HX,www.newington-ramsgate.org.uk/,1843593412.0,Headteacher,Hannah,Tudor,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Newington,South Thanet,(England/Wales) Urban city and town,E10000016,636335.0,165855.0,Thanet 013,Thanet 013A,,,,,Outstanding,South-East England and South London,United Kingdom,100062284319.0,,Not applicable,Not applicable,,,E02005144,E01024682,290.0, +135290,886,Kent,6909,The Marsh Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2007,Not applicable,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1110.0,No Special Classes,19-01-2023,1064.0,533.0,531.0,30.0,Supported by a multi-academy trust,SKINNERS' ACADEMIES TRUST,Linked to a sponsor,The Skinners' Company,Not applicable,,10021032.0,,Not applicable,16-11-2022,30-05-2024,Station Road,,,New Romney,Kent,TN28 8BB,www.marshacademy.org.uk,1797364593.0,Mr,Shaun,Simmons,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,20.0,24.0,,,South East,Folkestone and Hythe,New Romney,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,607023.0,124940.0,Folkestone and Hythe 012,Folkestone and Hythe 012C,,,,,Good,South-East England and South London,,50002925.0,,Not applicable,Not applicable,,,E02005113,E01024539,270.0, +135297,886,Kent,6910,The Leigh Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2007,Not applicable,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1500.0,No Special Classes,19-01-2023,1359.0,768.0,591.0,23.6,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10021033.0,,Not applicable,26-04-2023,30-04-2024,Green Street Green Road,,,Dartford,Kent,DA1 1QE,http://www.leighacademy.org.uk/,1322620400.0,Mrs,Julia,Collins,Academy Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,"SLCN - Speech, language and Communication",,,,,,,,,,,,Resourced provision,16.0,16.0,,,South East,Dartford,Brent,Dartford,(England/Wales) Urban major conurbation,E10000016,555487.0,173173.0,Dartford 008,Dartford 008B,,,,,Good,South-East England and South London,,100062308614.0,,Not applicable,Not applicable,,,E02005035,E01024136,272.0, +135305,886,Kent,6911,Spires Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2007,Not applicable,,Secondary,11.0,16,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,750.0,No Special Classes,19-01-2023,690.0,324.0,366.0,37.4,Supported by a multi-academy trust,EDUCATION FOR THE 21ST CENTURY,Linked to a sponsor,Education for the 21st Century,Not applicable,,10021093.0,,Not applicable,11-01-2023,26-09-2023,Bredlands Lane,Sturry,,Canterbury,Kent,CT2 0HD,http://www.spiresacademy.com/,1227710392.0,Mrs,Anna,Burden,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Sturry,Canterbury,(England/Wales) Rural hamlet and isolated dwellings,E10000016,619494.0,161797.0,Canterbury 011,Canterbury 011B,,,,,Good,South-East England and South London,,10090317463.0,,Not applicable,Not applicable,,,E02005020,E01024110,258.0, +135371,886,Kent,6913,Cornwallis Academy,Academy sponsor led,Academies,Open,New Provision,03-09-2007,Not applicable,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1825.0,No Special Classes,19-01-2023,1337.0,761.0,576.0,27.8,Supported by a multi-academy trust,FUTURE SCHOOLS TRUST,Linked to a sponsor,Future Schools Trust,Not applicable,,10021031.0,,Not applicable,12-01-2023,22-04-2024,Hubbards Lane,Linton,,Maidstone,Kent,ME17 4HX,http://www.futureschoolstrust.com/,1622743152.0,Mrs,Samantha,McMahon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Loose,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575978.0,150975.0,Maidstone 016,Maidstone 016D,,,,,Good,South-East England and South London,,200003720219.0,,Not applicable,Not applicable,,,E02005083,E01024376,277.0, +135372,886,Kent,6912,New Line Learning Academy,Academy sponsor led,Academies,Open,New Provision,03-09-2007,Not applicable,,Secondary,11.0,16,No boarders,Not applicable,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1050.0,No Special Classes,19-01-2023,719.0,372.0,347.0,43.7,Supported by a multi-academy trust,FUTURE SCHOOLS TRUST,Linked to a sponsor,Future Schools Trust,Not applicable,,10021098.0,,Not applicable,13-11-2019,21-05-2024,Boughton Lane,,,Maidstone,Kent,ME15 9QL,https://www.newlinelearning.com/,1622743286.0,Ms,Sharry,Mackie,Interim Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,VI - Visual Impairment,PD - Physical Disability,,,,,,,,,,,,Resourced provision,6.0,12.0,,,South East,Maidstone,South,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576813.0,153053.0,Maidstone 012,Maidstone 012D,,,,,Good,South-East England and South London,,200003677498.0,,Not applicable,Not applicable,,,E02005079,E01024404,313.0, +135432,886,Kent,1123,The Rosewood School,Pupil referral unit,Local authority maintained schools,Open,New Provision,01-11-2007,Not applicable,,Not applicable,11.0,18,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,120.0,Has Special Classes,19-01-2023,0.0,0.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10025535.0,,Not applicable,23-06-2022,17-11-2023,40 Teddington Drive,Leybourne,,West Malling,Kent,ME19 5FF,http://www.trs.kent.sch.uk/,1732875694.0,Mrs,Tina,Hamer,Executive Headteacher,Not applicable,,,,Provides places for Teen Mothers,,Does not have child care facilities,PRU Does have Provision for SEN,PRU Does not have EBD provision,120.0,PRU offers full time provision,Does not offer tuition by another provider,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"Birling, Leybourne & Ryarsh",Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,567730.0,159105.0,Tonbridge and Malling 014,Tonbridge and Malling 014G,,,,,Good,South-East England and South London,,100062387483.0,,Not applicable,Not applicable,,,E02006833,E01035010,0.0, +135462,886,Kent,1124,Birchwood,Pupil referral unit,Local authority maintained schools,Open,,01-01-2008,Not applicable,,Not applicable,14.0,16,Not applicable,Not applicable,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,44.0,Has Special Classes,19-01-2023,0.0,0.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10025590.0,,Not applicable,06-02-2019,07-05-2024,Bowen Road,,,Folkestone,Kent,CT19 4FP,www.birchwoodpru.kent.sch.uk,3000658450.0,Ms,Jane,Waters,Headteacher,Not applicable,,,,Does not provide places for Teen Mothers,,Does not have child care facilities,PRU Does have Provision for SEN,PRU Does have EBD provision,92.0,PRU offers full time provision,Does not offer tuition by another provider,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,620521.0,137026.0,Folkestone and Hythe 006,Folkestone and Hythe 006B,,,,,Good,South-East England and South London,United Kingdom,50125207.0,,Not applicable,Not applicable,,,E02005107,E01024512,0.0, +135465,886,Kent,1127,Maidstone and Malling Alternative Provision,Pupil referral unit,Local authority maintained schools,Open,,01-01-2008,Not applicable,,Not applicable,12.0,17,Not applicable,Not applicable,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,52.0,Not applicable,19-01-2023,0.0,0.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10025522.0,,Not applicable,06-11-2019,05-06-2024,8 Bower Mount Road,,,Maidstone,Kent,ME16 8AU,www.m-map.co.uk,1622753772.0,Mrs,Stacie,Smith,Headteacher,Not applicable,,,,Does not provide places for Teen Mothers,,Does not have child care facilities,PRU Does have Provision for SEN,PRU Does have EBD provision,52.0,PRU offers full time provision,PRU does offer tuition by another provider,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Bridge,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574902.0,155729.0,Maidstone 006,Maidstone 006B,,,,,Good,South-East England and South London,,200003728228.0,,Not applicable,Not applicable,,,E02005073,E01024340,0.0, +135466,886,Kent,1128,Enterprise Learning Alliance,Pupil referral unit,Local authority maintained schools,Open,,01-01-2008,Not applicable,,Not applicable,11.0,16,Not applicable,Not applicable,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,140.0,Not applicable,19-01-2023,0.0,0.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10025536.0,,Not applicable,06-06-2019,10-05-2024,Westwood Centre,Westwood Industrial Estate,Enterprise Road,Margate,Kent,CT9 4JA,www.ela.kent.sch.uk,1843606666.0,Mrs,Micheala,Clay,Executive Head Teacher,Not applicable,,,,Does not provide places for Teen Mothers,,Does not have child care facilities,PRU Does have Provision for SEN,PRU Does have EBD provision,168.0,PRU offers full time provision,Does not offer tuition by another provider,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Salmestone,North Thanet,(England/Wales) Urban city and town,E10000016,636075.0,168581.0,Thanet 004,Thanet 004E,,,,,Good,South-East England and South London,,10013309226.0,,Not applicable,Not applicable,,,E02005135,E01024696,0.0, +135467,886,Kent,1129,Two Bridges School,Pupil referral unit,Local authority maintained schools,Open,,01-01-2008,Not applicable,,Not applicable,11.0,18,Not applicable,Not applicable,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,,Not applicable,19-01-2023,0.0,0.0,0.0,0.0,Not applicable,,Not applicable,,Not applicable,,10025613.0,,Not applicable,28-02-2024,22-05-2024,Charles Street,Southborough,Two Bridges School,Tunbridge Wells,Kent,TN4 0DS,www.twobridgesschool.com,1892518461.0,Mrs,Kate,Middleton,Head Teacher,Not applicable,,,,Does not provide places for Teen Mothers,,Does not have child care facilities,PRU Does have Provision for SEN,PRU Does have EBD provision,94.0,PRU offers full time provision,PRU does offer tuition by another provider,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Southborough and High Brooms,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558073.0,141848.0,Tunbridge Wells 002,Tunbridge Wells 002C,,,,,Special Measures,South-East England and South London,,100062585348.0,,Not applicable,Not applicable,,,E02005163,E01024846,0.0, +135630,886,Kent,6914,Longfield Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2008,Not applicable,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1150.0,No Special Classes,19-01-2023,1035.0,533.0,502.0,25.1,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10024300.0,,Not applicable,27-09-2023,08-05-2024,Main Road,,,Longfield,Kent,DA3 7PH,http://www.longfieldacademy.org,1474700700.0,Dr,Felix,Donkor,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,37.0,40.0,,,South East,Dartford,"Longfield, New Barn & Southfleet",Dartford,(England/Wales) Urban city and town,E10000016,560573.0,168906.0,Dartford 013,Dartford 013B,,,,,Good,South-East England and South London,,200000538016.0,,Not applicable,Not applicable,,,E02005040,E01024158,224.0, +135721,886,Kent,6915,Oasis Academy Isle of Sheppey,Academy sponsor led,Academies,"Open, but proposed to close",New Provision,01-09-2009,Result of Amalgamation/Merger,31-08-2024,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,2450.0,Not applicable,19-01-2023,1486.0,763.0,723.0,49.2,Supported by a multi-academy trust,OASIS COMMUNITY LEARNING,Linked to a sponsor,Oasis Community Learning,Not applicable,,10027535.0,,Not applicable,08-06-2022,28-03-2024,Minster Road,,,Minster-on-Sea,Kent,ME12 3JQ,www.oasisacademyisleofsheppey.org,1795873591.0,Mr,Andrew,Booth,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Queenborough and Halfway,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,593655.0,172623.0,Swale 004,Swale 004A,,,,,Special Measures,South-East England and South London,,200002529794.0,,Not applicable,Not applicable,,,E02005118,E01024595,665.0, +135888,886,Kent,6916,Skinners' Kent Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2009,Not applicable,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1150.0,Not applicable,19-01-2023,1084.0,580.0,504.0,22.8,Supported by a multi-academy trust,SKINNERS' ACADEMIES TRUST,Linked to a sponsor,The Skinners' Company,Not applicable,,10027550.0,,Not applicable,11-05-2023,21-05-2024,Sandown Park,,,Tunbridge Wells,Kent,TN2 4PY,https://www.skinnerskentacademy.org.uk/,1892534377.0,Miss,Hannah,Knowles,Executive Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Sherwood,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,560482.0,140402.0,Tunbridge Wells 005,Tunbridge Wells 005C,,,,,Good,South-East England and South London,,10008671585.0,,Not applicable,Not applicable,,,E02005166,E01024842,215.0, +136128,886,Kent,6905,Knole Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2010,Not applicable,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1550.0,No Special Classes,19-01-2023,1373.0,671.0,702.0,17.9,Supported by a single-academy trust,KNOLE ACADEMY TRUST,Linked to a sponsor,Gordon Phillips (Knole academy),Not applicable,,10030458.0,,Not applicable,23-11-2022,10-05-2024,Knole Academy,Bradbourne Vale Road,,Sevenoaks,Kent,TN13 3LE,www.knoleacademy.org,1732454608.0,Mr,David,Collins,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Sevenoaks Northern,Sevenoaks,(England/Wales) Urban city and town,E10000016,552379.0,156485.0,Sevenoaks 011,Sevenoaks 011F,,,,,Good,South-East England and South London,,100062548177.0,,Not applicable,Not applicable,,,E02005097,E01024467,217.0, +136177,886,Kent,6918,Duke of York's Royal Military School,Academy sponsor led,Academies,Open,New Provision,01-09-2010,Not applicable,,Secondary,11.0,18,Boarding school,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,722.0,No Special Classes,19-01-2023,501.0,299.0,202.0,1.3,Supported by a single-academy trust,DYRMS - AN ACADEMY WITH MILITARY TRADITIONS,Linked to a sponsor,Ministry of Defence,Not applicable,,10030792.0,,Not applicable,09-02-2023,30-05-2024,Duke of York's Royal Military School,,,Dover,Kent,CT15 5EQ,www.doyrms.com,1304245023.0,Mr,Alex,Foreman,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Guston, Kingsdown & St Margaret's-at-Cliffe",Dover,(England/Wales) Rural village,E10000016,632730.0,143466.0,Dover 012,Dover 012B,,,,,Good,South-East England and South London,,10034881145.0,,Not applicable,Not applicable,,,E02005052,E01024238,5.0, +136197,886,Kent,6919,The John Wallis Church of England Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2010,Not applicable,,All-through,3.0,19,No boarders,Has Nursery Classes,Has a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Non-selective,1790.0,No Special Classes,19-01-2023,1763.0,889.0,874.0,37.8,Supported by a single-academy trust,"THE JOHN WALLIS CHURCH OF ENGLAND ACADEMY, ASHFORD",Linked to a sponsor,The Diocese of Canterbury Academies Company Limited,Not applicable,,10030997.0,,Not applicable,01-02-2024,20-05-2024,Millbank Road,Kingsnorth,,Ashford,Kent,TN23 3HG,http://www.thejohnwallisacademy.org,1233623465.0,Mr,Damian,McBeath,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Stanhope,Ashford,(England/Wales) Urban city and town,E10000016,599919.0,140348.0,Ashford 008,Ashford 008B,,,,,Good,South-East England and South London,,100062558283.0,,Not applicable,Not applicable,,,E02005003,E01024019,619.0, +136205,886,Kent,6920,Wilmington Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2010,Not applicable,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1400.0,No Special Classes,19-01-2023,1388.0,867.0,521.0,17.8,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10031094.0,,Not applicable,05-05-2023,22-04-2024,Common Lane,,,Wilmington,Kent,DA2 7DR,www.wilmingtonacademy.org.uk,1322272111.0,Mr,Michael,Gore,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,17.0,17.0,,,South East,Dartford,Maypole & Leyton Cross,Dartford,(England/Wales) Urban major conurbation,E10000016,552526.0,172176.0,Dartford 011,Dartford 011D,,,,,Outstanding,South-East England and South London,,200000534711.0,,Not applicable,Not applicable,,,E02005038,E01024189,213.0, +136251,886,Kent,3920,Goat Lees Primary School,Foundation school,Local authority maintained schools,Open,New Provision,01-09-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,,Not applicable,210.0,No Special Classes,19-01-2023,212.0,112.0,100.0,41.5,Not supported by a trust,,Not applicable,,Not under a federation,,10072306.0,,Not applicable,22-01-2020,05-06-2024,Hurst Road,Kennington,,Ashford,Kent,TN24 9RR,www.goatlees.kent.sch.uk/,1233630201.0,Ms,Teresa,Adams,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Goat Lees,Ashford,(England/Wales) Urban city and town,E10000016,600985.0,145239.0,Ashford 001,Ashford 001F,,,,,Good,South-East England and South London,,200004392081.0,,Not applicable,Not applicable,,,E02004996,E01032810,88.0, +136270,886,Kent,3912,Westlands Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2010,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,540.0,No Special Classes,19-01-2023,578.0,303.0,275.0,32.4,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10031923.0,,Not applicable,26-06-2019,16-05-2024,Homewood Avenue,,,Sittingbourne,Kent,ME10 1XN,www.westlandsprimary.org.uk/,1795470862.0,Mrs,Victoria,Pettett,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Homewood,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,589625.0,163383.0,Swale 012,Swale 012E,,,,,Good,South-East England and South London,,200002527704.0,,Not applicable,Not applicable,,,E02005126,E01024632,187.0, +136286,886,Kent,5434,Westlands School,Academy converter,Academies,Open,Academy Converter,01-09-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1604.0,Has Special Classes,19-01-2023,1784.0,935.0,849.0,22.3,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10031371.0,,Not applicable,27-02-2019,13-05-2024,Westlands Avenue,,,Sittingbourne,Kent,ME10 1PF,http://www.westlands.org.uk/,1795477475.0,Miss,Christina,Honess,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,SpLD - Specific Learning Difficulty,PD - Physical Disability,,,,,,,,,,,,Resourced provision and SEN unit,10.0,10.0,28.0,32.0,South East,Swale,Borden and Grove Park,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,588904.0,163864.0,Swale 012,Swale 012C,,,,,Good,South-East England and South London,,100062375039.0,,Not applicable,Not applicable,,,E02005126,E01024569,356.0, +136302,886,Kent,5421,The Canterbury Academy,Academy converter,Academies,Open,Academy Converter,07-10-2010,,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1300.0,Has Special Classes,19-01-2023,1844.0,949.0,895.0,27.6,Supported by a multi-academy trust,THE CANTERBURY ACADEMY TRUST,-,,Not applicable,,10031579.0,,Not applicable,22-02-2023,18-04-2024,Knight Avenue,,,Canterbury,Kent,CT2 8QA,http://www.canterbury.kent.sch.uk/,1227463971.0,Mr,Jon,Watson,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,,,,,,,,,,,Resourced provision,47.0,47.0,,,South East,Canterbury,Westgate,Canterbury,(England/Wales) Urban city and town,E10000016,613730.0,157724.0,Canterbury 020,Canterbury 020C,,,,,Requires improvement,South-East England and South London,,100062292855.0,,Not applicable,Not applicable,,,E02006856,E01024122,311.0, +136304,886,Kent,4031,Orchards Academy,Academy converter,Academies,Open,Academy Converter,01-11-2010,,,Secondary,11.0,18,No boarders,Not applicable,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,828.0,No Special Classes,19-01-2023,585.0,280.0,305.0,39.8,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10032208.0,,Not applicable,02-07-2021,30-04-2024,St Mary's Road,,,Swanley,Kent,BR8 7TE,http://www.orchards-tkat.org,1322665231.0,Mr,Andy,Lazenby,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,6.0,12.0,,,South East,Sevenoaks,Swanley St Mary's,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,551053.0,168589.0,Sevenoaks 002,Sevenoaks 002A,,,,,Good,South-East England and South London,,200002881968.0,,Not applicable,Not applicable,,,E02005088,E01024476,233.0, +136305,886,Kent,4080,Highsted Grammar School,Academy converter,Academies,Open,Academy Converter,01-10-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,830.0,No Special Classes,19-01-2023,889.0,7.0,882.0,8.1,Supported by a single-academy trust,HIGHSTED ACADEMY TRUST,-,,Not applicable,,10031571.0,,Not applicable,18-01-2023,07-06-2024,Highsted Road,,,Sittingbourne,Kent,ME10 4PT,http://www.highsted.kent.sch.uk,1795424223.0,Ms,Anne,Kelly,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Woodstock,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,590779.0,162957.0,Swale 013,Swale 013A,,,,,Good,South-East England and South London,,100062376278.0,,Not applicable,Not applicable,,,E02005127,E01024606,58.0, +136317,886,Kent,5463,Sandwich Technology School,Academy converter,Academies,Open,Academy Converter,01-11-2010,,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1368.0,No Special Classes,19-01-2023,1353.0,684.0,669.0,29.2,Supported by a single-academy trust,SANDWICH TECHNOLOGY SCHOOL,-,,Not applicable,,10032210.0,,Not applicable,02-05-2019,16-04-2024,Deal Road,,,Sandwich,Kent,CT13 0FA,http://www.sandwich-tech.kent.sch.uk,1304610000.0,Mrs,Tracey,Savage,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Sandwich,South Thanet,(England/Wales) Rural town and fringe,E10000016,632791.0,157060.0,Dover 002,Dover 002B,,,,,Good,South-East England and South London,,10034879953.0,,Not applicable,Not applicable,,,E02005042,E01024242,354.0, +136324,886,Kent,5414,Fulston Manor School,Academy converter,Academies,Open,Academy Converter,01-10-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1104.0,No Special Classes,19-01-2023,1340.0,661.0,679.0,20.9,Supported by a multi-academy trust,FULSTON MANOR ACADEMIES TRUST,Linked to a sponsor,Fulston Manor Academies Trust,Not applicable,,10031570.0,,Not applicable,15-11-2023,05-06-2024,Brenchley Road,,,Sittingbourne,Kent,ME10 4EG,http://www.fulstonmanor.kent.sch.uk,1795475228.0,Mrs,Susie,Burden,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Woodstock,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,590750.0,162857.0,Swale 013,Swale 013G,,,,,Requires improvement,South-East England and South London,,100062376597.0,,Not applicable,Not applicable,,,E02005127,E01032737,222.0, +136344,886,Kent,2654,The Canterbury Primary School,Academy converter,Academies,Open,Academy Converter,07-10-2010,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,None,Does not apply,Not applicable,Not applicable,435.0,Has Special Classes,19-01-2023,408.0,209.0,199.0,41.9,Supported by a multi-academy trust,THE CANTERBURY ACADEMY TRUST,-,,Not applicable,,10031917.0,,Not applicable,08-12-2022,09-04-2024,City View,Franklyn Road,,Canterbury,Kent,CT2 8PT,www.canterbury.kent.sch.uk/,1227462883.0,Mrs,Bev,Farrell,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,17.0,15.0,,,South East,Canterbury,Westgate,Canterbury,(England/Wales) Urban city and town,E10000016,613518.0,157596.0,Canterbury 020,Canterbury 020E,,,,,Good,South-East England and South London,,100062293053.0,,Not applicable,Not applicable,,,E02006856,E01024126,171.0, +136349,886,Kent,5455,Leigh Academy Tonbridge,Academy converter,Academies,Open,Academy Converter,01-12-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Non-selective,1020.0,No Special Classes,19-01-2023,477.0,476.0,1.0,25.7,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10031580.0,,Not applicable,07-12-2022,29-04-2024,Brook Street,,,Tonbridge,Kent,TN9 2PH,http://leighacademytonbridge.org.uk,1732500600.0,Mr,Michael,Crow,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Judd,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,558063.0,145659.0,Tonbridge and Malling 013,Tonbridge and Malling 013A,,,,,Good,South-East England and South London,,200000962881.0,,Not applicable,Not applicable,,,E02005161,E01024757,109.0, +136351,886,Kent,2656,Meopham Community Academy,Academy converter,Academies,Open,Academy Converter,01-12-2010,,,Primary,2.0,11,No boarders,Has Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,467.0,240.0,227.0,6.9,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10032229.0,,Not applicable,16-10-2018,18-04-2024,Longfield Road,Meopham,,Gravesend,Kent,DA13 0JW,http://www.meophamca.com,1474812259.0,Mr,Thomas,Waterman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Gravesham,Meopham North,Gravesham,(England/Wales) Rural town and fringe,E10000016,564320.0,166643.0,Gravesham 012,Gravesham 012D,,,,,Good,South-East England and South London,,100062313197.0,,Not applicable,Not applicable,,,E02005066,E01024271,30.0, +136359,886,Kent,5406,Dartford Grammar School,Academy converter,Academies,Open,Academy Converter,01-12-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1550.0,No Special Classes,19-01-2023,1516.0,1291.0,225.0,5.9,Supported by a single-academy trust,DARTFORD GRAMMAR SCHOOL,-,,Not applicable,,10032228.0,,Not applicable,07-12-2022,07-05-2024,West Hill,,,Dartford,Kent,DA1 2HW,http://www.dartfordgrammarschool.org.uk,1322223039.0,Mr,Julian,Metcalf,Headmaster,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dartford,West Hill,Dartford,(England/Wales) Urban major conurbation,E10000016,553364.0,174104.0,Dartford 003,Dartford 003F,,,,,Outstanding,South-East England and South London,,200000540995.0,,Not applicable,Not applicable,,,E02005030,E01024185,54.0, +136379,886,Kent,4092,Highworth Grammar School,Academy converter,Academies,Open,Academy Converter,01-01-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,Does not apply,Does not apply,Not applicable,Selective,1227.0,No Special Classes,19-01-2023,1542.0,100.0,1442.0,6.7,Supported by a single-academy trust,HIGHWORTH GRAMMAR SCHOOL TRUST,-,,Not applicable,,10032347.0,,Not applicable,14-06-2013,21-05-2024,Maidstone Road,,,Ashford,Kent,TN24 8UD,http://www.highworth.kent.sch.uk/,1233624910.0,Mr,Duncan,Beer,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Furley,Ashford,(England/Wales) Urban city and town,E10000016,600386.0,143340.0,Ashford 015,Ashford 015B,,,,,Outstanding,South-East England and South London,,100062560536.0,,Not applicable,Not applicable,,,E02007046,E01023981,73.0, +136382,886,Kent,5462,Chatham & Clarendon Grammar School,Academy converter,Academies,Open,Academy Converter,01-01-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Selective,1600.0,No Special Classes,19-01-2023,1503.0,766.0,737.0,13.4,Supported by a multi-academy trust,CHATHAM & CLARENDON GRAMMAR SCHOOL,-,,Not applicable,,10032384.0,,Not applicable,16-05-2018,14-05-2024,Chatham Street,,,Ramsgate,Kent,CT11 7PS,http://www.ccgrammarschool.co.uk,1843591075.0,Mrs,Debra,Liddicoat,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Eastcliff,South Thanet,(England/Wales) Urban city and town,E10000016,638046.0,165251.0,Thanet 015,Thanet 015D,,,,,Good,South-East England and South London,,100062281955.0,,Not applicable,Not applicable,,,E02005146,E01024670,135.0, +136393,886,Kent,2608,St Stephen's Junior School,Academy converter,Academies,Open,Academy Converter,01-01-2011,,,Primary,7.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,404.0,218.0,186.0,30.0,Supported by a single-academy trust,ST STEPHEN'S ACADEMY CANTERBURY,-,,Not applicable,,10032362.0,,Not applicable,01-03-2023,21-05-2024,Hales Drive,St Stephens,,Canterbury,Kent,CT2 7AD,www.ststephensjuniorschool.co.uk/,1227464119.0,Co Headteacher,Laura Cutts,Sarah Heaney,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,St Stephen's,Canterbury,(England/Wales) Urban city and town,E10000016,614829.0,159308.0,Canterbury 013,Canterbury 013B,,,,,Good,South-East England and South London,,200000677176.0,,Not applicable,Not applicable,,,E02005022,E01024100,121.0, +136417,886,Kent,5443,Tonbridge Grammar School,Academy converter,Academies,Open,Academy Converter,01-01-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1260.0,No Special Classes,19-01-2023,1131.0,29.0,1102.0,1.9,Supported by a single-academy trust,TONBRIDGE GRAMMAR SCHOOL,-,,Not applicable,,10032608.0,,Not applicable,17-10-2019,22-05-2024,Deakin Leas,,,Tonbridge,Kent,TN9 2JR,http://www.tgs.kent.sch.uk,1732365125.0,Mrs,Rebecca,Crean,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Vauxhall,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559055.0,145279.0,Tonbridge and Malling 013,Tonbridge and Malling 013C,,,,,Outstanding,South-East England and South London,,100062594177.0,,Not applicable,Not applicable,,,E02005161,E01024778,17.0, +136455,886,Kent,4046,Weald of Kent Grammar School,Academy converter,Academies,Open,Academy Converter,01-02-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,2475.0,No Special Classes,19-01-2023,1966.0,81.0,1885.0,3.3,Supported by a single-academy trust,WEALD OF KENT GRAMMAR SCHOOL ACADEMY TRUST,-,,Not applicable,,10032960.0,,Not applicable,27-04-2022,21-05-2024,Tudeley Lane,,,Tonbridge,Kent,TN9 2JP,http://www.wealdgs.org,1732373500.0,Mr,Richard,Booth,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Vauxhall,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559489.0,145217.0,Tonbridge and Malling 012,Tonbridge and Malling 012E,,,,,Requires improvement,South-East England and South London,,200000962006.0,,Not applicable,Not applicable,,,E02005160,E01024767,50.0, +136465,886,Kent,5448,Herne Bay High School,Academy converter,Academies,Open,Academy Converter,01-03-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1494.0,No Special Classes,19-01-2023,1586.0,826.0,760.0,25.2,Supported by a single-academy trust,HERNE BAY HIGH SCHOOL,-,,Not applicable,,10032981.0,,Not applicable,15-06-2022,08-04-2024,Bullockstone Road,,,Herne Bay,Kent,CT6 7NS,http://www.hernebayhigh.org,1227361221.0,Mr,Jon,Boyes,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Greenhill,North Thanet,(England/Wales) Urban city and town,E10000016,616784.0,167105.0,Canterbury 003,Canterbury 003A,,,,,Good,South-East England and South London,,10033162926.0,,Not applicable,Not applicable,,,E02005012,E01024064,333.0, +136499,886,Kent,2141,Amherst School,Academy converter,Academies,Open,Academy Converter,01-03-2011,,,Primary,7.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,384.0,No Special Classes,19-01-2023,379.0,205.0,174.0,5.0,Supported by a multi-academy trust,AMHERST SCHOOL (ACADEMY) TRUST,-,,Not applicable,,10033038.0,,Not applicable,11-05-2022,23-05-2024,Witches Lane,Riverhead,,Sevenoaks,Kent,TN13 2AX,http://www.amherst.kent.sch.uk,1732452577.0,Mr,Andrew,Reid,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Brasted, Chevening and Sundridge",Sevenoaks,(England/Wales) Urban city and town,E10000016,550933.0,155835.0,Sevenoaks 011,Sevenoaks 011B,,,,,Good,South-East England and South London,,50002017544.0,,Not applicable,Not applicable,,,E02005097,E01024418,19.0, +136501,886,Kent,5428,Sir Roger Manwood's School,Academy converter,Academies,Open,Academy Converter,01-03-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Selective,1070.0,No Special Classes,19-01-2023,979.0,474.0,505.0,9.4,Supported by a single-academy trust,SIR ROGER MANWOOD'S SCHOOL,-,,Not applicable,,10033035.0,,Not applicable,28-09-2022,25-03-2024,Manwood Road,,,Sandwich,Kent,CT13 9JX,http://www.manwoods.co.uk,1304610200.0,Mr,Lee,Hunter,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Sandwich,South Thanet,(England/Wales) Rural town and fringe,E10000016,633436.0,157821.0,Dover 002,Dover 002B,,,,,Good,South-East England and South London,,100062284711.0,,Not applicable,Not applicable,,,E02005042,E01024242,71.0, +136570,886,Kent,5449,Queen Elizabeth's Grammar School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Selective,1000.0,No Special Classes,19-01-2023,1020.0,503.0,517.0,7.3,Supported by a multi-academy trust,QUEEN ELIZABETH'S GRAMMAR SCHOOL TRUST FAVERSHAM,-,,Not applicable,,10033266.0,,Not applicable,01-03-2023,21-05-2024,Abbey Place,,,Faversham,Kent,ME13 7BQ,http://www.queenelizabeths.kent.sch.uk,1795533132.0,Mr,David,Anderson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Abbey,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,601923.0,161598.0,Swale 015,Swale 015C,,,,,Good,South-East England and South London,,10034900923.0,,Not applicable,Not applicable,,,E02005129,E01024553,56.0, +136571,886,Kent,4172,Hartsdown Academy,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,1175.0,Not applicable,19-01-2023,773.0,408.0,365.0,59.9,Supported by a multi-academy trust,COASTAL ACADEMIES TRUST,Linked to a sponsor,Coastal Academies Trust,Not applicable,,10033264.0,,Not applicable,08-12-2021,07-05-2024,George V Avenue,,,Margate,Kent,CT9 5RE,http://hartsdown.org/,1843227957.0,Mr,Matthew,Tate,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Thanet,Garlinge,North Thanet,(England/Wales) Urban city and town,E10000016,634567.0,169921.0,Thanet 005,Thanet 005A,,,,,Good,South-East England and South London,,100062627471.0,,Not applicable,Not applicable,,,E02005136,E01024672,438.0, +136581,886,Kent,4249,Valley Park School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1157.0,Not applicable,19-01-2023,1655.0,779.0,876.0,15.5,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10033421.0,,Not applicable,05-03-2020,12-04-2024,Huntsman Lane,,,Maidstone,Kent,ME14 5DT,http://www.valleypark.viat.org.uk/,1622679421.0,Mr,D,Jones,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Boxley,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,577242.0,155795.0,Maidstone 005,Maidstone 005B,,,,,Good,South-East England and South London,,200003716140.0,,Not applicable,Not applicable,,,E02005072,E01024336,206.0, +136582,886,Kent,4058,Invicta Grammar School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1319.0,No Special Classes,19-01-2023,1647.0,43.0,1604.0,5.4,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10033423.0,,Not applicable,21-09-2012,06-06-2024,Huntsman Lane,,,Maidstone,Kent,ME14 5DS,http://www.invicta.viat.org.uk,1622755856.0,Mrs,Van,Beales (Executive Headteacher),Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,High Street,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,577046.0,155881.0,Maidstone 004,Maidstone 004G,,,,,Outstanding,South-East England and South London,,200003717570.0,,Not applicable,Not applicable,,,E02005071,E01033092,68.0, +136583,886,Kent,4196,Towers School and Sixth Form Centre,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,1625.0,No Special Classes,19-01-2023,1460.0,676.0,784.0,24.5,Supported by a single-academy trust,TOWERS SCHOOL ACADEMY TRUST,-,,Not applicable,,10033255.0,,Not applicable,23-01-2019,03-06-2024,Faversham Road,Kennington,,Ashford,Kent,TN24 9AL,www.towers.kent.sch.uk,1233634171.0,Mr,Richard,Billings,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Kennington,Ashford,(England/Wales) Urban city and town,E10000016,601709.0,145611.0,Ashford 003,Ashford 003B,,,,,Good,South-East England and South London,,200004392081.0,,Not applicable,Not applicable,,,E02004998,E01023999,312.0, +136584,886,Kent,4120,King Ethelbert School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,16,No boarders,Not applicable,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,750.0,No Special Classes,19-01-2023,757.0,379.0,378.0,25.2,Supported by a multi-academy trust,COASTAL ACADEMIES TRUST,Linked to a sponsor,Coastal Academies Trust,Not applicable,,10033425.0,,Not applicable,03-10-2018,28-05-2024,Canterbury Road,,,Birchington,Kent,CT7 9BL,http://www.kingethelbert.com/,1843831999.0,,Tom,Sellen,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Birchington South,North Thanet,(England/Wales) Urban city and town,E10000016,631297.0,169465.0,Thanet 008,Thanet 008C,,,,,Good,South-East England and South London,,100062303868.0,,Not applicable,Not applicable,,,E02005139,E01024638,191.0, +136585,886,Kent,5460,Dane Court Grammar School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Selective,1154.0,No Special Classes,19-01-2023,1236.0,589.0,647.0,16.2,Supported by a multi-academy trust,COASTAL ACADEMIES TRUST,Linked to a sponsor,Coastal Academies Trust,Not applicable,,10033426.0,,Not applicable,11-05-2022,08-05-2024,Broadstairs Road,,,Broadstairs,Kent,CT10 2RT,http://danecourt.kent.sch.uk/,1843864941.0,Mr,Martin,Jones,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,St Peters,South Thanet,(England/Wales) Urban city and town,E10000016,638084.0,167977.0,Thanet 011,Thanet 011C,,,,,Good,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005142,E01024689,142.0, +136603,886,Kent,5464,Bennett Memorial Diocesan School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Non-selective,1452.0,No Special Classes,19-01-2023,1865.0,954.0,911.0,6.7,Supported by a multi-academy trust,THE TENAX SCHOOLS TRUST,Linked to a sponsor,The Tenax Schools Trust,Not applicable,,10033233.0,,Not applicable,14-12-2023,14-05-2024,Culverden Down,,,Tunbridge Wells,Kent,TN4 9SH,https://www.bennettmemorial.co.uk/,1892521595.0,Dr,Karen,Brookes,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,557344.0,140560.0,Tunbridge Wells 007,Tunbridge Wells 007D,,,,,Outstanding,South-East England and South London,,100062585960.0,,Not applicable,Not applicable,,,E02005168,E01024838,98.0, +136727,886,Kent,5422,Oakwood Park Grammar School,Academy converter,Academies,Open,Academy Converter,01-05-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1035.0,No Special Classes,19-01-2023,1096.0,1047.0,49.0,5.6,Supported by a multi-academy trust,OAKWOOD PARK GRAMMAR SCHOOL,-,,Not applicable,,10033587.0,,Not applicable,07-02-2019,05-06-2024,Oakwood Park,,,Maidstone,Kent,ME16 8AH,http://www.opgs.org,1622726683.0,Mr,Kevin,Moody,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Heath,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574384.0,155244.0,Maidstone 008,Maidstone 008C,,,,,Good,South-East England and South London,,200003717871.0,,Not applicable,Not applicable,,,E02005075,E01024367,44.0, +136794,886,Kent,2249,Regis Manor Primary School,Academy converter,Academies,Open,Academy Converter,01-06-2011,,,Primary,3.0,11,No boarders,Has Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,570.0,No Special Classes,19-01-2023,580.0,293.0,287.0,24.1,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10033808.0,,Not applicable,04-07-2023,30-04-2024,North Street,,,Sittingbourne,Kent,ME10 2HW,www.regismanor.org.uk,1795472971.0,Mr,Matthew,Perry,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Milton Regis,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,590298.0,165171.0,Swale 007,Swale 007F,,,,,Good,South-East England and South London,United Kingdom,100062375806.0,,Not applicable,Not applicable,,,E02005121,E01024583,130.0, +136847,886,Kent,5439,Mascalls Academy,Academy converter,Academies,Open,Academy Converter,01-07-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1450.0,Has Special Classes,19-01-2023,1356.0,731.0,625.0,20.1,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10034109.0,,Not applicable,17-11-2021,13-03-2024,Maidstone Road,Paddock Wood,,Tonbridge,Kent,TN12 6LT,http://www.mascallsacademy.org.uk,1892835366.0,Mrs,Jo,Brooks,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Paddock Wood West,Tunbridge Wells,(England/Wales) Rural town and fringe,E10000016,567107.0,143847.0,Tunbridge Wells 001,Tunbridge Wells 001G,,,,,Good,South-East England and South London,,100062545662.0,,Not applicable,Not applicable,,,E02005162,E01024816,231.0, +136923,886,Kent,4000,St Augustine Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2011,Not applicable,,Secondary,11.0,16,No boarders,Not applicable,Does not have a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Non-selective,750.0,Not applicable,19-01-2023,765.0,400.0,365.0,29.9,Supported by a multi-academy trust,WOODARD ACADEMIES TRUST,Linked to a sponsor,Woodard Academies Trust,Not applicable,,10034935.0,,Not applicable,13-07-2023,07-06-2024,Oakwood Road,,,Maidstone,Kent,ME16 8AE,http://www.saa.woodard.co.uk/,1622752490.0,Mr,Steffan,Ball,Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,12.0,12.0,,,South East,Maidstone,Heath,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574440.0,155313.0,Maidstone 008,Maidstone 008C,,,,,Requires improvement,South-East England and South London,,10014313780.0,,Not applicable,Not applicable,,,E02005075,E01024367,229.0, +137071,886,Kent,2000,St Johns Church of England Primary School,Voluntary controlled school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2012,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,419.0,218.0,201.0,50.7,Not applicable,,Not applicable,,Not under a federation,,10079922.0,,Not applicable,24-01-2024,21-05-2024,St John's Place,Northgate,,Canterbury,Kent,CT1 1BD,www.stjohns-canterbury.kent.sch.uk/,1227462360.0,Mrs,Jo,Williamson,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Northgate,Canterbury,(England/Wales) Urban city and town,E10000016,615068.0,158363.0,Canterbury 014,Canterbury 014E,,,,,Good,South-East England and South London,,200000678007.0,,Not applicable,Not applicable,,,E02005023,E01024093,212.0, +137099,886,Kent,5465,Gravesend Grammar School,Academy converter,Academies,Open,Academy Converter,01-08-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1032.0,No Special Classes,19-01-2023,1424.0,1340.0,84.0,7.4,Supported by a multi-academy trust,THE DECUS EDUCATIONAL TRUST,Linked to a sponsor,Gravesend Grammar School,Not applicable,,10034589.0,,Not applicable,26-06-2015,06-06-2024,Church Walk,,,Gravesend,Kent,DA12 2PR,http://gravesendgrammar.com/,1474331893.0,Mr,Malcolm,Moaby,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Denton,Gravesham,(England/Wales) Urban major conurbation,E10000016,565789.0,173594.0,Gravesham 005,Gravesham 005A,,,,,Outstanding,South-East England and South London,,10012012057.0,,Not applicable,Not applicable,,,E02005059,E01024259,76.0, +137104,886,Kent,5450,Hillview School for Girls,Academy converter,Academies,Open,Academy Converter,01-08-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Non-selective,1340.0,No Special Classes,19-01-2023,1496.0,58.0,1438.0,12.6,Supported by a single-academy trust,HILLVIEW SCHOOL FOR GIRLS ACADEMY TRUST,-,,Not applicable,,10034679.0,,Not applicable,20-09-2023,23-05-2024,Brionne Gardens,,,Tonbridge,Kent,TN9 2HE,http://www.hillview.kent.sch.uk,1732352793.0,Mrs,Hilary,Burkett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Vauxhall,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559690.0,145686.0,Tonbridge and Malling 012,Tonbridge and Malling 012E,,,,,Good,South-East England and South London,,200000962877.0,,Not applicable,Not applicable,,,E02005160,E01024767,144.0, +137136,886,Kent,2001,Horizon Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2011,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,None,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,202.0,109.0,93.0,41.6,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10034965.0,,Not applicable,15-11-2018,21-05-2024,Hilda May Avenue,,,Swanley,Kent,BR8 7BT,www.horizon-tkat.org/,1322665235.0,Mr,David,Moss,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Swanley White Oak,Sevenoaks,(England/Wales) Urban major conurbation,E10000016,551164.0,169131.0,Sevenoaks 002,Sevenoaks 002D,,,,,Good,South-East England and South London,,100062622098.0,,Not applicable,Not applicable,,,E02005088,E01024480,84.0, +137227,886,Kent,5403,Wilmington Grammar School for Boys,Academy converter,Academies,Open,Academy Converter,01-08-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,848.0,No Special Classes,19-01-2023,1079.0,993.0,86.0,8.1,Supported by a multi-academy trust,ENDEAVOUR MAT,-,,Not applicable,,10034780.0,,Not applicable,14-03-2023,07-05-2024,Common Lane,Wilmington,,Dartford,Kent,DA2 7DA,http://www.wgsb.co.uk,1322223090.0,Mr,Stuart,Harrington,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Maypole & Leyton Cross,Dartford,(England/Wales) Urban major conurbation,E10000016,552660.0,172195.0,Dartford 011,Dartford 011D,,,,,Good,South-East England and South London,,200000534710.0,,Not applicable,Not applicable,,,E02005038,E01024189,68.0, +137250,886,Kent,5400,Wilmington Grammar School for Girls,Academy converter,Academies,Open,Academy Converter,01-08-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1070.0,No Special Classes,19-01-2023,1068.0,92.0,976.0,4.6,Supported by a multi-academy trust,ENDEAVOUR MAT,-,,Not applicable,,10034848.0,,Not applicable,17-11-2022,05-06-2024,Wilmington Grange,Parsons Lane,Wilmington,Dartford,Kent,DA2 7BB,http://www.gsgw.org.uk/,1322226351.0,Mrs,Michelle,Lawson,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Maypole & Leyton Cross,Dartford,(England/Wales) Urban major conurbation,E10000016,552565.0,172488.0,Dartford 011,Dartford 011C,,,,,Good,South-East England and South London,,200000534735.0,,Not applicable,Not applicable,,,E02005038,E01024188,38.0, +137397,886,Kent,2246,Sheldwich Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Primary,2.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,267.0,124.0,143.0,6.0,Supported by a single-academy trust,SHELDWICH PRIMARY SCHOOL,-,,Not applicable,,10035075.0,,Not applicable,09-11-2012,22-05-2024,Lees Court Road,Sheldwich,,Faversham,Kent,ME13 0LU,http://www.sheldwich.kent.sch.uk,1795532779.0,Mrs,Sarah,Garrett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Boughton and Courtenay,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,601215.0,156561.0,Swale 017,Swale 017A,,,,,Outstanding,South-East England and South London,,200002532965.0,,Not applicable,Not applicable,,,E02005131,E01024555,16.0, +137458,886,Kent,5466,Brockhill Park Performing Arts College,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1384.0,No Special Classes,19-01-2023,1351.0,680.0,671.0,25.7,Supported by a single-academy trust,BROCKHILL PARK PERFORMING ARTS COLLEGE,-,,Not applicable,,10035076.0,,Not applicable,13-10-2021,05-04-2024,Sandling Road,Saltwood,,Hythe,Kent,CT21 4HL,http://www.brockhill.kent.sch.uk/,1303265521.0,Mr,Charles,Joseph,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Hythe,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,614826.0,135838.0,Folkestone and Hythe 008,Folkestone and Hythe 008D,,,,,Good,South-East England and South London,,50016401.0,,Not applicable,Not applicable,,,E02005109,E01024550,301.0, +137467,886,Kent,2233,Lynsted and Norton Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,73.0,41.0,32.0,45.2,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10035094.0,,Not applicable,08-03-2023,23-04-2024,Lynsted Lane,Lynsted,,Sittingbourne,Kent,ME9 0RL,www.lynsted-norton.kent.sch.uk,1795521362.0,Mrs,Catherine,McLaughlin,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Teynham and Lynsted,Sittingbourne and Sheppey,(England/Wales) Rural hamlet and isolated dwellings,E10000016,594445.0,161097.0,Swale 016,Swale 016E,,,,,Requires improvement,South-East England and South London,,200002528949.0,,Not applicable,Not applicable,,,E02005130,E01024624,33.0, +137474,886,Kent,5444,Barton Court Grammar School,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Selective,810.0,No Special Classes,19-01-2023,985.0,577.0,408.0,9.2,Supported by a multi-academy trust,BARTON COURT ACADEMY TRUST,Linked to a sponsor,Barton Court Academy Trust,Not applicable,,10035093.0,,Not applicable,12-02-2020,23-04-2024,Longport,,,Canterbury,Kent,CT1 1PH,http://www.bartoncourt.org,1227464600.0,Mr,Jonathan,Hopkins,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,615693.0,157611.0,Canterbury 016,Canterbury 016A,,,,,Good,South-East England and South London,,100062279040.0,,Not applicable,Not applicable,,,E02005025,E01024044,67.0, +137481,886,Kent,3112,Selling Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,128.0,75.0,53.0,21.1,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10035102.0,,Not applicable,11-11-2021,06-05-2024,The Street,Selling,,Faversham,Kent,ME13 9RQ,http://www.selling-faversham.kent.sch.uk/,1227752202.0,Mr,Richard,Paez,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Boughton and Courtenay,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,603872.0,156507.0,Swale 017,Swale 017A,,,,,Good,South-East England and South London,,10023196074.0,,Not applicable,Not applicable,,,E02005131,E01024555,27.0, +137483,886,Kent,3110,Milstead and Frinsted Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,70.0,No Special Classes,19-01-2023,85.0,48.0,37.0,23.5,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10035126.0,,Not applicable,03-11-2022,31-05-2024,School Lane,Milstead,,Sittingbourne,Kent,ME9 0SJ,http://www.milstead.kent.sch.uk,1795830241.0,Mr,Scott,Guy,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,West Downs,Sittingbourne and Sheppey,(England/Wales) Rural hamlet and isolated dwellings,E10000016,590320.0,158072.0,Swale 013,Swale 013C,,,,,Requires improvement,South-East England and South London,,100062626973.0,,Not applicable,Not applicable,,,E02005127,E01024628,20.0, +137484,886,Kent,5408,Homewood School and Sixth Form Centre,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,2156.0,No Special Classes,19-01-2023,2103.0,960.0,1143.0,26.5,Supported by a multi-academy trust,TENTERDEN SCHOOLS TRUST,-,,Not applicable,,10035054.0,,Not applicable,26-04-2023,14-05-2024,Ashford Road,,,Tenterden,Kent,TN30 6LT,http://www.homewood-school.co.uk/,1580764222.0,Mr,Jeremy,Single,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Tenterden North,Ashford,(England/Wales) Rural town and fringe,E10000016,588805.0,134276.0,Ashford 013,Ashford 013E,,,,,Good,South-East England and South London,,100062566868.0,,Not applicable,Not applicable,,,E02005008,E01024024,464.0, +137529,886,Kent,2288,Smarden Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2011,,,Primary,2.0,11,No boarders,Has Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,174.0,97.0,77.0,8.6,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10035486.0,,Not applicable,11-05-2023,27-03-2024,Pluckley Road,Smarden,,Ashford,Kent,TN27 8ND,www.smardenprimaryschool.co.uk/,1233770316.0,Mrs,Claudia,Miller,Executive Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Weald North,Ashford,(England/Wales) Rural village,E10000016,588338.0,142472.0,Ashford 011,Ashford 011D,,,,,Good,South-East England and South London,,100062563757.0,,Not applicable,Not applicable,,,E02005006,E01024036,15.0, +137581,886,Kent,4001,Ebbsfleet Academy,Academy sponsor led,Academies,Open,New Provision,01-11-2013,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,750.0,No Special Classes,19-01-2023,766.0,421.0,345.0,34.1,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10038343.0,,Not applicable,02-10-2019,14-03-2024,Southfleet Road,,,Swanscombe,Kent,DA10 0BZ,http://www.ebbsfleetacademy.org.uk,1322242252.0,Mrs,Gurjit Kaur,Shergill,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Swanscombe,Dartford,(England/Wales) Urban major conurbation,E10000016,560829.0,173926.0,Dartford 004,Dartford 004B,,,,,Good,South-East England and South London,,200000535991.0,,Not applicable,Not applicable,,,E02005031,E01024176,237.0, +137609,886,Kent,5404,Saint George's Church of England School,Academy converter,Academies,Open,Academy Converter,01-11-2011,,,All-through,4.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Non-selective,1410.0,No Special Classes,19-01-2023,1381.0,722.0,659.0,17.4,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10035681.0,,Not applicable,18-10-2023,25-04-2024,Meadow Road,,,Gravesend,Kent,DA11 7LS,http://www.saintgeorgescofe.kent.sch.uk,1474533082.0,Mr,Simon,Murphy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Woodlands,Gravesham,(England/Wales) Urban major conurbation,E10000016,564257.0,172583.0,Gravesham 005,Gravesham 005E,,,,,Good,South-East England and South London,,100062310647.0,,Not applicable,Not applicable,,,E02005059,E01024317,202.0, +137615,886,Kent,3372,"Maidstone, St John's Church of England Primary School",Academy converter,Academies,Open,Academy Converter,01-11-2011,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,441.0,211.0,230.0,8.4,Supported by a single-academy trust,ST JOHN'S CHURCH OF ENGLAND PRIMARY SCHOOL MAIDSTONE,-,,Not applicable,,10035701.0,,Not applicable,16-07-2015,16-04-2024,Provender Way,Grove Green,,Maidstone,Kent,ME14 5TZ,www.st-johns-maidstone.kent.sch.uk/,1622735916.0,Mr,D J,Smith,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Boxley,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,578084.0,155874.0,Maidstone 005,Maidstone 005C,,,,,Outstanding,South-East England and South London,,200003688398.0,,Not applicable,Not applicable,,,E02005072,E01024337,37.0, +137660,886,Kent,2500,Joydens Wood Infant School,Academy converter,Academies,Open,Academy Converter,01-11-2011,,,Primary,4.0,7,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,238.0,122.0,116.0,5.5,Supported by a multi-academy trust,NEXUS EDUCATION SCHOOLS TRUST,Linked to a sponsor,Nexus Education Schools Trust,Not applicable,,10035690.0,,Not applicable,05-10-2023,29-04-2024,Park Way,,,Bexley,Kent,DA5 2JD,www.joydens-wood-infant.kent.sch.uk/,1322523188.0,Headteacher,Allison Morris/,Gerard Strong,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Joyden's Wood,Dartford,(England/Wales) Urban major conurbation,E10000016,551436.0,172012.0,Dartford 010,Dartford 010D,,,,,Good,South-East England and South London,,200000534792.0,,Not applicable,Not applicable,,,E02005037,E01024153,13.0, +137661,886,Kent,2438,Joydens Wood Junior School,Academy converter,Academies,Open,Academy Converter,01-11-2011,,,Primary,7.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,280.0,No Special Classes,19-01-2023,294.0,147.0,147.0,9.5,Supported by a multi-academy trust,NEXUS EDUCATION SCHOOLS TRUST,Linked to a sponsor,Nexus Education Schools Trust,Not applicable,,10035673.0,,Not applicable,08-06-2022,04-04-2024,Birchwood Drive,Wilmington,,Dartford,Kent,DA2 7NE,http://www.joydens-wood-junior.kent.sch.uk,1322522151.0,Mr,Paul,Redford,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Joyden's Wood,Dartford,(England/Wales) Urban major conurbation,E10000016,551348.0,171986.0,Dartford 010,Dartford 010A,,,,,Requires improvement,South-East England and South London,,100060870222.0,,Not applicable,Not applicable,,,E02005037,E01024150,28.0, +137663,886,Kent,5219,Wilmington Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2011,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,None,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,210.0,112.0,98.0,7.1,Supported by a multi-academy trust,ENDEAVOUR MAT,-,,Not applicable,,10035695.0,,Not applicable,20-06-2019,22-04-2024,Common Lane,Wilmington,,Dartford,Kent,DA2 7DF,www.wilmingtonprimaryschool.co.uk,1322274080.0,Mrs,Charlotte,Scott,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,"Wilmington, Sutton-at-Hone & Hawley",Dartford,(England/Wales) Urban major conurbation,E10000016,552812.0,172220.0,Dartford 011,Dartford 011C,,,,,Good,South-East England and South London,,200000534694.0,,Not applicable,Not applicable,,,E02005038,E01024188,15.0, +137687,886,Kent,4002,The Sittingbourne School,Academy sponsor led,Academies,Open,New Provision,01-01-2012,Not applicable,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1350.0,No Special Classes,19-01-2023,1589.0,777.0,812.0,36.4,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10036067.0,,Not applicable,22-03-2023,21-05-2024,Swanstree Avenue,,,Sittingbourne,Kent,ME10 4NL,http://www.thesittingbourneschool.org.uk,1795472449.0,Mr,Nick,Smith,Trust Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,HI - Hearing Impairment,"SLCN - Speech, language and Communication",,,,,,,,,,,,Resourced provision,82.0,60.0,,,South East,Swale,Roman,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,592123.0,162907.0,Swale 011,Swale 011D,,,,,Good,South-East England and South London,,100062627032.0,,Not applicable,Not applicable,,,E02005125,E01024600,509.0, +137728,886,Kent,3025,Chiddingstone Church of England School,Academy converter,Academies,Open,Academy Converter,01-12-2011,,,Primary,5.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,209.0,96.0,113.0,4.3,Supported by a single-academy trust,CHIDDINGSTONE CHURCH OF ENGLAND SCHOOL,-,,Not applicable,,10035696.0,,Not applicable,27-03-2015,15-04-2024,Chiddingstone,,,Edenbridge,Kent,TN8 7AH,www.chiddingstoneschool.co.uk/,1892870339.0,,Kate,Haysom,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Penshurst, Fordcombe and Chiddingstone",Tonbridge and Malling,(England/Wales) Rural village,E10000016,550144.0,145154.0,Sevenoaks 015,Sevenoaks 015C,,,,,Outstanding,South-East England and South London,,10035181506.0,,Not applicable,Not applicable,,,E02005101,E01024455,9.0, +137739,886,Kent,5416,Cranbrook School,Academy converter,Academies,Open,Academy Converter,01-12-2011,,,Secondary,11.0,18,Boarding school,Not applicable,Has a sixth form,Mixed,Christian,Does not apply,Not applicable,Selective,910.0,No Special Classes,19-01-2023,884.0,515.0,369.0,2.7,Supported by a single-academy trust,CRANBROOK SCHOOL ACADEMY TRUST,-,,Not applicable,,10035657.0,,Not applicable,23-03-2022,30-04-2024,Waterloo Road,,,Cranbrook,Kent,TN17 3JD,http://www.cranbrookschool.co.uk/,1580711800.0,Mr,David,Clark,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Benenden and Cranbrook,Maidstone and The Weald,(England/Wales) Rural town and fringe,E10000016,577829.0,136133.0,Tunbridge Wells 013,Tunbridge Wells 013C,,,,,Good,South-East England and South London,,100062552225.0,,Not applicable,Not applicable,,,E02005174,E01024790,15.0, +137800,886,Kent,4527,Borden Grammar School,Academy converter,Academies,Open,Academy Converter,01-01-2012,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,802.0,No Special Classes,19-01-2023,889.0,861.0,28.0,9.8,Supported by a single-academy trust,BORDEN GRAMMAR SCHOOL TRUST,-,,Not applicable,,10035796.0,,Not applicable,24-11-2021,15-05-2024,Avenue of Remembrance,,,Sittingbourne,Kent,ME10 4DB,http://website.bordengrammar.kent.sch.uk/,1795424192.0,Mr,Ashley,Tomlin,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Homewood,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,590580.0,163359.0,Swale 013,Swale 013A,,,,,Good,South-East England and South London,,100062376270.0,,Not applicable,Not applicable,,,E02005127,E01024606,65.0, +137806,886,Kent,2002,Repton Manor Primary School,Foundation school,Local authority maintained schools,Open,New Provision,01-09-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,471.0,244.0,227.0,19.5,Not supported by a trust,,Not applicable,,Supported by a federation,The Lightyear Federation,10072299.0,,Not applicable,29-11-2023,06-03-2024,Repton Avenue,,,Ashford,Kent,TN23 3RX,http://www.reptonmanorprimary.co.uk/,1233666307.0,Mr,Matthew,Rawling,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Repton,Ashford,(England/Wales) Urban city and town,E10000016,599566.0,143671.0,Ashford 016,Ashford 016B,,,,,Good,South-East England and South London,,10012859115.0,,Not applicable,Not applicable,,,E02007047,E01032820,92.0, +137833,886,Kent,5401,The Maplesden Noakes School,Academy converter,Academies,Open,Academy Converter,01-02-2012,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1500.0,No Special Classes,19-01-2023,1359.0,675.0,684.0,18.1,Supported by a single-academy trust,THE MAPLESDEN NOAKES SCHOOL,Linked to a sponsor,The Maplesden Noakes School,Not applicable,,10036261.0,,Not applicable,14-11-2018,30-04-2024,Buckland Road,,,Maidstone,Kent,ME16 0TJ,http://www.maplesden.kent.sch.uk/,1622759036.0,Mr,Richard,Owen,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,Bridge,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575149.0,156457.0,Maidstone 006,Maidstone 006B,,,,,Good,South-East England and South London,,200003670026.0,,Not applicable,Not applicable,,,E02005073,E01024340,197.0, +137834,886,Kent,5467,"Mayfield Grammar School, Gravesend",Academy converter,Academies,Open,Academy Converter,01-02-2012,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1207.0,No Special Classes,19-01-2023,1349.0,40.0,1309.0,10.5,Supported by a single-academy trust,"MAYFIELD GRAMMAR SCHOOL, GRAVESEND",-,,Not applicable,,10036417.0,,Not applicable,12-06-2013,09-05-2024,Pelham Road,,,Gravesend,Kent,DA11 0JE,http://www.mgsg.kent.sch.uk/,1474352896.0,Headteacher,Elaine,Wilson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Pelham,Gravesham,(England/Wales) Urban major conurbation,E10000016,564094.0,173489.0,Gravesham 002,Gravesham 002C,,,,,Outstanding,South-East England and South London,,100062310034.0,,Not applicable,Not applicable,,,E02005056,E01024290,107.0, +137836,886,Kent,2684,Wentworth Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,651.0,321.0,330.0,14.9,Supported by a single-academy trust,WENTWORTH PRIMARY SCHOOL,-,,Not applicable,,10036263.0,,Not applicable,08-06-2023,21-05-2024,Wentworth Drive,,,Dartford,Kent,DA1 3NG,www.wentworthonline.co.uk/,1322225694.0,Mr,Lewis,Pollock,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Heath,Dartford,(England/Wales) Urban major conurbation,E10000016,552144.0,173935.0,Dartford 007,Dartford 007A,,,,,Good,South-East England and South London,,,,Not applicable,Not applicable,,,E02005034,E01024144,97.0, +137837,886,Kent,5437,The Folkestone School for Girls,Academy converter,Academies,Open,Academy Converter,01-02-2012,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1053.0,No Special Classes,19-01-2023,1217.0,0.0,1217.0,12.1,Supported by a multi-academy trust,THE FOLKESTONE SCHOOL FOR GIRLS ACADEMY TRUST,-,,Not applicable,,10036180.0,,Not applicable,12-10-2012,17-04-2024,Coolinge Lane,,,Folkestone,Kent,CT20 3RB,http://www.folkestonegirls.kent.sch.uk/,1303251125.0,Mr,Mark,Lester,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Sandgate & West Folkestone,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,620830.0,135551.0,Folkestone and Hythe 006,Folkestone and Hythe 006H,,,,,Outstanding,South-East England and South London,,50026109.0,,Not applicable,Not applicable,,,E02005107,E01024521,112.0, +137871,886,Kent,2229,Graveney Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,105.0,No Special Classes,19-01-2023,100.0,50.0,50.0,18.0,Supported by a multi-academy trust,GRAVENEY PRIMARY SCHOOL,-,,Not applicable,,10035872.0,,Not applicable,04-10-2023,17-04-2024,Seasalter Road,Graveney,,Faversham,Kent,ME13 9DU,www.graveneyprimary.com,1795532005.0,Mrs,Alison,Blackwell,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Boughton and Courtenay,Faversham and Mid Kent,(England/Wales) Rural village,E10000016,605092.0,162256.0,Swale 017,Swale 017B,,,,,Good,South-East England and South London,,200002531105.0,,Not applicable,Not applicable,,,E02005131,E01024556,18.0, +137881,886,Kent,2003,Oaks Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-04-2012,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,213.0,No Special Classes,19-01-2023,216.0,103.0,113.0,40.4,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10037058.0,,Not applicable,22-09-2021,28-05-2024,Oak Tree Avenue,,,Maidstone,Kent,ME15 9AX,http://www.oaksprimaryacademy.org.uk,1622755960.0,Principal,Tom,Moore,Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Shepway North,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,577321.0,153197.0,Maidstone 013,Maidstone 013C,,,,,Outstanding,South-East England and South London,,200003718069.0,,Not applicable,Not applicable,,,E02005080,E01024391,78.0, +137882,886,Kent,2004,Tree Tops Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-04-2012,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,315.0,No Special Classes,19-01-2023,314.0,148.0,166.0,48.8,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10037063.0,,Not applicable,12-06-2019,13-09-2023,Brishing Lane,Park Wood,,Maidstone,Kent,ME15 9EZ,http://www.treetopsprimaryacademy.org/,1622754888.0,Miss,Denise,White,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Park Wood,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,578482.0,152246.0,Maidstone 013,Maidstone 013I,,,,,Good,South-East England and South London,,10014314389.0,,Not applicable,Not applicable,,,E02005080,E01034996,148.0, +137961,886,Kent,2264,Hampton Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2012,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,710.0,No Special Classes,19-01-2023,675.0,348.0,327.0,28.1,Supported by a single-academy trust,HAMPTON PRIMARY SCHOOL ACADEMY,-,,Not applicable,,10036924.0,,Not applicable,11-03-2020,07-05-2024,Fitzgerald Avenue,,,Herne Bay,Kent,CT6 8NB,www.hampton.kent.sch.uk,1227372159.0,Ms,Yvonne,Nunn,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,,,,,South East,Canterbury,West Bay,North Thanet,(England/Wales) Urban city and town,E10000016,616209.0,167469.0,Canterbury 004,Canterbury 004D,,,,,Good,South-East England and South London,,200000682720.0,,Not applicable,Not applicable,,,E02005013,E01024119,188.0, +138001,886,Kent,3142,Pluckley Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,96.0,51.0,45.0,15.6,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10036989.0,,Not applicable,06-06-2019,15-04-2024,The Street,Pluckley,,Ashford,Kent,TN27 0QS,www.pluckleyprimaryschool.co.uk,1233840422.0,Mrs,Lorraine,Smith,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Upper Weald,Ashford,(England/Wales) Rural village,E10000016,592582.0,145357.0,Ashford 002,Ashford 002E,,,,,Good,South-East England and South London,,100062563578.0,,Not applicable,Not applicable,,,E02004997,E01024033,15.0, +138019,886,Kent,4528,The Norton Knatchbull School,Academy converter,Academies,Open,Academy Converter,01-04-2012,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1316.0,No Special Classes,19-01-2023,1252.0,1226.0,26.0,7.9,Supported by a single-academy trust,THE NORTON KNATCHBULL SCHOOL,-,,Not applicable,,10036999.0,,Not applicable,14-12-2023,07-06-2024,Hythe Road,,,Ashford,Kent,TN24 0QJ,http://www.nks.kent.sch.uk,1233620045.0,Mr,Ben,Greene,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Furley,Ashford,(England/Wales) Urban city and town,E10000016,602275.0,142571.0,Ashford 005,Ashford 005C,,,,,Good,South-East England and South London,,100062559840.0,,Not applicable,Not applicable,,,E02005000,E01024023,79.0, +138034,886,Kent,2232,Luddenham School,Academy converter,Academies,Open,Academy Converter,01-04-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,201.0,92.0,109.0,25.4,Supported by a single-academy trust,LUDDENHAM SCHOOL,-,,Not applicable,,10037009.0,,Not applicable,27-02-2019,04-06-2024,Luddenham,,,Faversham,Kent,ME13 0TE,www.luddenham.kent.sch.uk,1795532061.0,Mrs,Claire,Vincett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Teynham and Lynsted,Sittingbourne and Sheppey,(England/Wales) Rural hamlet and isolated dwellings,E10000016,599374.0,162425.0,Swale 016,Swale 016C,,,,,Good,South-East England and South London,,200002532858.0,,Not applicable,Not applicable,,,E02005130,E01024622,51.0, +138074,886,Kent,2006,St James the Great Academy,Academy sponsor led,Academies,Open,New Provision,01-04-2012,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,197.0,101.0,96.0,42.6,Supported by a multi-academy trust,ACADEMIES ENTERPRISE TRUST,Linked to a sponsor,Academies Enterprise Trust (AET),Not applicable,,10037082.0,,Not applicable,13-09-2023,23-05-2024,Chapman Way,,,East Malling,Kent,ME19 6SD,http://www.stjamesthegreatacademy.org/,1732841912.0,Miss,Tamasin,Springett,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"East Malling, West Malling & Offham",Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,569675.0,157748.0,Tonbridge and Malling 014,Tonbridge and Malling 014B,,,,,Good,South-East England and South London,,100062388024.0,,Not applicable,Not applicable,,,E02006833,E01024742,81.0, +138151,886,Kent,2595,Grove Park Primary School,Academy converter,Academies,Open,Academy Converter,01-06-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,412.0,214.0,198.0,21.1,Supported by a multi-academy trust,BOURNE ALLIANCE MULTI ACADEMY TRUST,Linked to a sponsor,TIMU Academy Trust,Not applicable,,10037293.0,,Not applicable,24-05-2023,16-04-2024,Hilton Drive,,,Sittingbourne,Kent,ME10 1PT,www.grovepark-ba-mat.org.uk,1795477417.0,Mrs,Lauren,Flain,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Borden and Grove Park,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,588827.0,164604.0,Swale 009,Swale 009H,,,,,Requires improvement,South-East England and South London,,200002527488.0,,Not applicable,Not applicable,,,E02005123,E01035306,87.0, +138167,886,Kent,4113,Astor Secondary School,Academy converter,Academies,Open,Academy Converter,02-06-2012,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,1230.0,No Special Classes,19-01-2023,803.0,348.0,455.0,49.3,Supported by a multi-academy trust,SAMPHIRE STAR EDUCATION TRUST,-,,Not applicable,,10037500.0,,Not applicable,01-11-2023,15-04-2024,Astor Avenue,,,Dover,Kent,CT17 0AS,http://www.astorschool.com,1304201151.0,Mr,Lee,Kane,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dover,Tower Hamlets,Dover,(England/Wales) Urban city and town,E10000016,630219.0,141531.0,Dover 011,Dover 011H,,,,,Requires improvement,South-East England and South London,,10034874352.0,,Not applicable,Not applicable,,,E02005051,E01024248,350.0, +138168,886,Kent,2315,White Cliffs Primary and Nursery School,Academy converter,Academies,Open,Academy Converter,01-06-2012,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,315.0,179.0,136.0,59.0,Supported by a multi-academy trust,SAMPHIRE STAR EDUCATION TRUST,-,,Not applicable,,10037463.0,,Not applicable,09-01-2019,22-05-2024,St Radigund's Road,,,Dover,Kent,CT17 0LB,http://www.whitecliffsprimary.com,1304206174.0,Mrs,Helen,Castle,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,St Radigunds,Dover,(England/Wales) Urban city and town,E10000016,630188.0,142202.0,Dover 011,Dover 011F,,,,,Good,South-East England and South London,,200002882635.0,,Not applicable,Not applicable,,,E02005051,E01024240,186.0, +138169,886,Kent,2310,Barton Junior School,Academy converter,Academies,Open,Academy Converter,01-06-2012,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,240.0,No Special Classes,19-01-2023,215.0,121.0,94.0,53.5,Supported by a multi-academy trust,SAMPHIRE STAR EDUCATION TRUST,-,,Not applicable,,10037462.0,,Not applicable,05-12-2018,22-05-2024,Barton Road,,,Dover,Kent,CT16 2ND,www.bartonjuniorschool.org/,1304201643.0,Miss,Melanie,O'Dell,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,St Radigunds,Dover,(England/Wales) Urban city and town,E10000016,631229.0,142506.0,Dover 012,Dover 012C,,,,,Good,South-East England and South London,,100062289419.0,,Not applicable,Not applicable,,,E02005052,E01024239,115.0, +138170,886,Kent,2316,Shatterlocks Infant and Nursery School,Academy converter,Academies,Open,Academy Converter,01-06-2012,,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,232.0,No Special Classes,19-01-2023,202.0,90.0,112.0,40.5,Supported by a multi-academy trust,SAMPHIRE STAR EDUCATION TRUST,-,,Not applicable,,10037459.0,,Not applicable,16-05-2019,27-02-2024,Heathfield Avenue,,,Dover,Kent,CT16 2PB,http://www.shatterlocks.com,1304204264.0,Miss,Melanie,O'Dell,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Buckland,Dover,(England/Wales) Urban city and town,E10000016,630755.0,142875.0,Dover 011,Dover 011A,,,,,Outstanding,South-East England and South London,,100062289420.0,,Not applicable,Not applicable,,,E02005051,E01024193,77.0, +138195,886,Kent,2007,Molehill Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-06-2012,,,Primary,3.0,11,,Has Nursery Classes,Not applicable,Mixed,None,None,Not applicable,Not applicable,315.0,Not applicable,19-01-2023,303.0,159.0,144.0,43.2,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10037485.0,,Not applicable,14-06-2023,03-06-2024,Hereford Road,,,Maidstone,Kent,ME15 7ND,http://www.molehillprimaryacademy.org.uk,1622751729.0,Principal,Laura,Smith,Head of Academy,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,HI - Hearing Impairment,"SLCN - Speech, language and Communication",,,,,,,,,,,,Resourced provision,5.0,10.0,,,South East,Maidstone,Shepway South,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,577701.0,153158.0,Maidstone 013,Maidstone 013E,,,,,Good,South-East England and South London,,200003717486.0,,Not applicable,Not applicable,,,E02005080,E01024398,128.0, +138232,886,Kent,2008,Tiger Primary School,Free schools,Free Schools,Open,New Provision,03-09-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,427.0,217.0,210.0,33.7,Supported by a multi-academy trust,FUTURE SCHOOLS TRUST,Linked to a sponsor,Future Schools Trust,Not applicable,,10038742.0,,Not applicable,20-09-2023,22-11-2023,Boughton Lane,,,Maidstone,Kent,ME15 9QL,www.futureschoolstrust.com,1622745166.0,Mr,Daniel,Siggs,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,South,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576813.0,153053.0,Maidstone 012,Maidstone 012D,Ofsted,,,,Good,South-East England and South London,,200003677498.0,,Not applicable,Not applicable,,,E02005079,E01024404,144.0, +138405,886,Kent,6138,Earlscliffe (Sussex Summer Schools Ltd),Other independent school,Independent schools,Open,New Provision,17-07-2012,,,Not applicable,14.0,19,Boarding school,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Not applicable,160.0,Not applicable,20-01-2022,129.0,83.0,46.0,0.0,Not applicable,,Not applicable,,Not applicable,,10037987.0,,Not applicable,28-11-2019,08-04-2024,29 Shorncliffe Road,,,Folkestone,Kent,CT20 2NB,www.earlscliffe.co.uk,1303253951.0,Mr,Niall,Johnson,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not approved,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Folkestone Central,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,621725.0,136091.0,Folkestone and Hythe 006,Folkestone and Hythe 006A,ISI,,12.0,,,South-East England and South London,,50033584.0,,Not applicable,Not applicable,,,E02005107,E01024511,0.0, +138434,886,Kent,2009,Northdown Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2012,,,Primary,3.0,11,No boarders,Has Nursery Classes,Not applicable,Mixed,None,None,Not applicable,Not applicable,427.0,Not applicable,19-01-2023,325.0,157.0,168.0,73.5,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10038398.0,,Not applicable,24-11-2021,09-05-2024,Tenterden Way,,,Margate,Kent,CT9 3RE,www.northdown-tkat.org,1843226077.0,Mr,Matthew,Harris,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Dane Valley,North Thanet,(England/Wales) Urban city and town,E10000016,637269.0,170097.0,Thanet 006,Thanet 006C,,,,,Good,South-East England and South London,,100062307407.0,,Not applicable,Not applicable,,,E02005137,E01024662,228.0, +138436,886,Kent,2010,Newlands Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2012,,,Primary,4.0,11,Not applicable,Not applicable,Not applicable,Mixed,None,None,Not applicable,Not applicable,315.0,Not applicable,19-01-2023,291.0,159.0,132.0,60.8,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10038052.0,,Not applicable,02-11-2022,26-04-2024,Dumpton Lane,,,Ramsgate,Kent,CT11 7AJ,www.newlands-tkat.org,1843593086.0,Mr,David,Bailey,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Sir Moses Montefiore,South Thanet,(England/Wales) Urban city and town,E10000016,638271.0,166231.0,Thanet 012,Thanet 012C,,,,,Good,South-East England and South London,,100062282198.0,,Not applicable,Not applicable,,,E02005143,E01024699,177.0, +138438,886,Kent,2011,Salmestone Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2012,,,Primary,3.0,11,,Has Nursery Classes,Not applicable,Mixed,None,None,Not applicable,Non-selective,210.0,Not applicable,19-01-2023,215.0,114.0,101.0,41.4,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10038040.0,,Not applicable,23-01-2019,22-02-2024,College Road,,,Margate,Kent,CT9 4DB,www.salmestone-tkat.org/,1843220949.0,Mr,Thomas,Platten,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Salmestone,North Thanet,(England/Wales) Urban city and town,E10000016,635418.0,169813.0,Thanet 003,Thanet 003C,,,,,Good,South-East England and South London,,100062307602.0,,Not applicable,Not applicable,,,E02005134,E01024695,84.0, +138480,886,Kent,4101,The Harvey Grammar School,Academy converter,Academies,Open,Academy Converter,01-08-2012,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,Does not apply,Does not apply,Not applicable,Selective,980.0,No Special Classes,19-01-2023,1026.0,1026.0,0.0,10.8,Supported by a multi-academy trust,THE HARVEY ACADEMY,-,,Not applicable,,10038103.0,,Not applicable,14-12-2022,21-05-2024,Cheriton Road,,,Folkestone,Kent,CT19 5JY,http://www.harveygs.kent.sch.uk/,1303252131.0,Mr,S,Norman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Broadmead,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,621203.0,136613.0,Folkestone and Hythe 015,Folkestone and Hythe 015D,,,,,Outstanding,South-East England and South London,,50032937.0,,Not applicable,Not applicable,,,E02006880,E01024517,81.0, +138579,886,Kent,2013,Water Meadows Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2012,,,Primary,4.0,11,,Not applicable,Not applicable,Mixed,None,None,Not applicable,Not applicable,150.0,Not applicable,19-01-2023,148.0,72.0,76.0,43.5,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10038477.0,,Not applicable,20-03-2019,28-05-2024,Shaftesbury Road,Hersden,,Canterbury,Kent,CT3 4HS,www.watermeadows.kent.sch.uk,1227710414.0,Mr,Ben,Martin,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Sturry,North Thanet,(England/Wales) Rural town and fringe,E10000016,620217.0,162160.0,Canterbury 010,Canterbury 010C,,,,,Good,South-East England and South London,,200000683090.0,,Not applicable,Not applicable,,,E02005019,E01024086,64.0, +138592,886,Kent,2014,St Laurence In Thanet Church of England Junior Academy,Academy sponsor led,Academies,Open,New Provision,01-12-2012,,,Primary,7.0,11,,Not applicable,Not applicable,Mixed,Church of England,None,Diocese of Canterbury,Not applicable,256.0,Not applicable,19-01-2023,177.0,95.0,82.0,60.5,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10038488.0,,Not applicable,04-07-2018,07-05-2024,Newington Road,,,Ramsgate,Kent,CT11 0QX,www.stlaurencejuniors.co.uk/,1843592257.0,Ms,Sarah,Graham,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Newington,South Thanet,(England/Wales) Urban city and town,E10000016,636976.0,165548.0,Thanet 013,Thanet 013C,,,,,Good,South-East England and South London,,200002234791.0,,Not applicable,Not applicable,,,E02005144,E01024684,107.0, +138737,886,Kent,3086,West Malling Church of England Primary School and McGinty Speech and Language Srp,Academy converter,Academies,Open,Academy Converter,01-09-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,Has Special Classes,19-01-2023,207.0,109.0,98.0,24.6,Supported by a multi-academy trust,THE TENAX SCHOOLS TRUST,Linked to a sponsor,The Tenax Schools Trust,Not applicable,,10037972.0,,Not applicable,25-01-2023,06-06-2024,Old Cricket Ground,Norman Road,,West Malling,Kent,ME19 6RL,http://www.west-malling.kent.sch.uk,1732842061.0,Mr,David,Rye,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,14.0,21.0,,,South East,Tonbridge and Malling,"East Malling, West Malling & Offham",Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,567936.0,157853.0,Tonbridge and Malling 014,Tonbridge and Malling 014C,,,,,Good,South-East England and South London,,100062388025.0,,Not applicable,Not applicable,,,E02006833,E01024783,51.0, +138738,886,Kent,3128,Sturry Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,406.0,195.0,211.0,26.8,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10038650.0,,Not applicable,28-01-2015,08-04-2024,Park View,Sturry,,Canterbury,Kent,CT2 0NR,http://www.sturry.kent.sch.uk,1227710477.0,Mrs,M,Mannings,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Sturry,Canterbury,(England/Wales) Rural town and fringe,E10000016,617998.0,161082.0,Canterbury 011,Canterbury 011C,,,,,Outstanding,South-East England and South London,,200000679701.0,,Not applicable,Not applicable,,,E02005020,E01024111,109.0, +138972,886,Kent,2015,Dame Janet Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-12-2012,,,Primary,4.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,360.0,Not applicable,19-01-2023,371.0,193.0,178.0,58.1,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10039504.0,,Not applicable,03-10-2018,22-02-2024,Newington Road,,,Ramsgate,Kent,CT12 6QY,www.damejanet-tkat.org/,1843591807.0,Mr,Sam,Atkinson,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Northwood,South Thanet,(England/Wales) Urban city and town,E10000016,636796.0,166666.0,Thanet 011,Thanet 011B,,,,,Good,South-East England and South London,,100062284279.0,,Not applicable,Not applicable,,,E02005142,E01024688,215.0, +139021,886,Kent,2017,Drapers Mills Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-12-2012,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,330.0,169.0,161.0,66.1,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10039726.0,,Not applicable,01-11-2023,18-03-2024,St Peter's Footpath,,,Margate,Kent,CT9 2SP,http://www.drapersmillsprimary.co.uk/,1843223989.0,Mrs,Kathleen,Davis,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Dane Valley,North Thanet,(England/Wales) Urban city and town,E10000016,636260.0,169934.0,Thanet 004,Thanet 004B,,,,,Good,South-East England and South London,,200003079364.0,,Not applicable,Not applicable,,,E02005135,E01024664,218.0, +139052,886,Kent,2018,Temple Grove Academy,Academy sponsor led,Academies,Open,New Provision,01-01-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,210.0,Not applicable,19-01-2023,208.0,96.0,112.0,47.0,Supported by a single-academy trust,TEMPLE GROVE ACADEMY TRUST,Linked to a sponsor,Temple Grove Schools Trust,Not applicable,,10039910.0,,Not applicable,18-09-2019,10-04-2024,Friars Way,,,Tunbridge Wells,Kent,TN2 3UA,http://www.templegroveacademy.com/,1892520562.0,Mrs,Rebekah,Leeves,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Sherwood,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,559898.0,140821.0,Tunbridge Wells 005,Tunbridge Wells 005B,,,,,Good,South-East England and South London,,10000066049.0,,Not applicable,Not applicable,,,E02005166,E01024841,93.0, +139075,886,Kent,4004,Meopham School,Academy sponsor led,Academies,Open,New Provision,01-02-2013,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,832.0,Not applicable,19-01-2023,989.0,505.0,484.0,16.0,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10039851.0,,Not applicable,20-04-2023,16-04-2024,Wrotham Road,Meopham,,Gravesend,Kent,DA13 0AH,meophamschool.org.uk,1474814646.0,Mr,Glenn,Prebble,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision and SEN unit,20.0,20.0,20.0,20.0,South East,Gravesham,Meopham North,Gravesham,(England/Wales) Rural village,E10000016,564231.0,165820.0,Gravesham 012,Gravesham 012D,,,,,Good,South-East England and South London,,100062313194.0,,Not applicable,Not applicable,,,E02005066,E01024271,144.0, +139096,886,Kent,5209,Allington Primary School,Academy converter,Academies,Open,Academy Converter,01-12-2012,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,465.0,223.0,242.0,4.9,Supported by a multi-academy trust,ORCHARD ACADEMY TRUST,Linked to a sponsor,Allington Primary School Academy Trust (Orchard Academy Trust),Not applicable,,10039700.0,,Not applicable,13-07-2022,16-04-2024,Hildenborough Crescent,London Road,,Maidstone,Kent,ME16 0PG,www.allington.kent.sch.uk/,1622757350.0,Mrs,C,Howson,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Allington,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574093.0,157190.0,Maidstone 003,Maidstone 003D,,,,,Outstanding,South-East England and South London,,200003717229.0,,Not applicable,Not applicable,,,E02005070,E01024323,23.0, +139186,886,Kent,2307,Warden House Primary School,Academy converter,Academies,Open,Academy Converter,01-01-2013,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,437.0,228.0,209.0,36.2,Supported by a multi-academy trust,VERITAS MULTI ACADEMY TRUST,Linked to a sponsor,Veritas Multi Academy Trust,Not applicable,,10040005.0,,Not applicable,03-12-2014,21-05-2024,Birdwood Avenue,,,Deal,Kent,CT14 9SF,http://www.warden-house.kent.sch.uk,1304375040.0,Mr,Robert,Hackett,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Middle Deal,Dover,(England/Wales) Urban city and town,E10000016,636229.0,152116.0,Dover 003,Dover 003A,,,,,Outstanding,South-East England and South London,,100062287038.0,,Not applicable,Not applicable,,,E02005043,E01024219,158.0, +139254,886,Kent,2019,Chantry Community Primary School,Academy sponsor led,Academies,Open,New Provision,01-06-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,454.0,229.0,225.0,43.7,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10040148.0,,Not applicable,27-01-2022,18-04-2024,Ordnance Road,,,Gravesend,Kent,DA12 2RL,www.chantryprimary.co.uk,1474350011.0,Mrs,Kathryn,Duncan,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Town,Gravesham,(England/Wales) Urban major conurbation,E10000016,565390.0,174044.0,Gravesham 002,Gravesham 002E,,,,,Good,South-East England and South London,,100062311901.0,,Not applicable,Not applicable,,,E02005056,E01024295,176.0, +139255,886,Kent,2020,"Christ Church Church of England Junior School, Ramsgate",Academy sponsor led,Academies,Open,New Provision,01-12-2013,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Not applicable,240.0,Not applicable,19-01-2023,200.0,104.0,96.0,43.5,Supported by a single-academy trust,"CHRIST CHURCH CHURCH OF ENGLAND JUNIOR SCHOOL, RAMSGATE",Linked to a sponsor,The Diocese of Canterbury Academies Company Limited,Not applicable,,10040135.0,,Not applicable,10-11-2021,14-05-2024,London Road,,,Ramsgate,Kent,CT11 0ZZ,www.christchurchjuniors.com/,1843593350.0,Mr,Neil,Tucker,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Cliffsend and Pegwell,South Thanet,(England/Wales) Urban city and town,E10000016,637368.0,164573.0,Thanet 017,Thanet 017B,,,,,Good,South-East England and South London,,100062628044.0,,Not applicable,Not applicable,,,E02005148,E01024651,87.0, +139309,886,Kent,3148,"Christ Church Cep Academy, Folkestone",Academy converter,Academies,Open,Academy Converter,01-03-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,422.0,193.0,229.0,46.7,Supported by a single-academy trust,"CHRIST CHURCH CHURCH OF ENGLAND PRIMARY ACADEMY, FOLKESTONE",-,,Not applicable,,10040025.0,,Not applicable,01-12-2022,21-05-2024,Brockman Road,,,Folkestone,Kent,CT20 1DJ,www.christchurchfolkestone.com/,1303253645.0,Mr,Robin,Flack,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Folkestone Central,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,622492.0,136267.0,Folkestone and Hythe 014,Folkestone and Hythe 014B,,,,,Good,South-East England and South London,,50041734.0,,Not applicable,Not applicable,,,E02006879,E01024507,197.0, +139310,886,Kent,3349,Folkestone St. Mary's Church of England Primary Academy,Academy converter,Academies,Open,Academy Converter,01-02-2013,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,449.0,228.0,221.0,36.3,Supported by a single-academy trust,FOLKESTONE ST MARY'S CHURCH OF ENGLAND PRIMARY ACADEMY,-,,Not applicable,,10040279.0,,Not applicable,20-10-2021,08-05-2024,Warren Road,,,Folkestone,Kent,CT19 6QH,www.stmarysfolkestone.com,1303251390.0,Mrs,Amanda,Wolfram,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Folkestone and Hythe,Folkestone Harbour,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,623841.0,136685.0,Folkestone and Hythe 003,Folkestone and Hythe 003D,,,,,Good,South-East England and South London,,50046311.0,,Not applicable,Not applicable,,,E02005104,E01024503,144.0, +139315,886,Kent,3348,St Eanswythe's Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,212.0,94.0,118.0,25.0,Supported by a single-academy trust,ST EANSWYTHE'S CHURCH OF ENGLAND PRIMARY SCHOOL,-,,Not applicable,,10040285.0,,Not applicable,13-03-2019,31-05-2024,Church Street,,,Folkestone,Kent,CT20 1SE,http://www.st-eanswythes.kent.sch.uk/,1303255516.0,Headteacher,Claire,Jacobs,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Folkestone Central,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,622986.0,135909.0,Folkestone and Hythe 014,Folkestone and Hythe 014D,,,,,Outstanding,South-East England and South London,,50036799.0,,Not applicable,Not applicable,,,E02006879,E01033215,53.0, +139396,886,Kent,2021,Kemsley Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-04-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,232.0,112.0,120.0,31.4,Supported by a multi-academy trust,REACH2 ACADEMY TRUST,Linked to a sponsor,REAch2 Academy Trust,Not applicable,,10040770.0,,Not applicable,14-02-2019,16-01-2024,Coldharbour Lane,Kemsley,,Sittingbourne,Kent,ME10 2RP,www.kemsley.kent.sch.uk,1795428689.0,,Iris,Homer,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Kemsley,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,590740.0,166282.0,Swale 007,Swale 007J,,,,,Good,South-East England and South London,,10035063593.0,,Not applicable,Not applicable,,,E02005121,E01035303,69.0, +139397,886,Kent,2022,Milton Court Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-04-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,238.0,Not applicable,19-01-2023,245.0,121.0,124.0,52.7,Supported by a multi-academy trust,REACH2 ACADEMY TRUST,Linked to a sponsor,REAch2 Academy Trust,Not applicable,,10040771.0,,Not applicable,18-09-2019,13-05-2024,Brewery Road,Milton Regis,,Sittingbourne,Kent,ME10 2EE,www.milton-court.kent.sch.uk,1795472972.0,Miss,Sarah,Gadsdon,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Milton Regis,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,590434.0,164874.0,Swale 010,Swale 010B,,,,,Good,South-East England and South London,,100062375804.0,,Not applicable,Not applicable,,,E02005124,E01024584,125.0, +139436,886,Kent,2023,Temple Ewell Church of England Primary School,Academy sponsor led,Academies,Open,New Provision,01-02-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Not applicable,140.0,Not applicable,19-01-2023,145.0,74.0,71.0,18.6,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10041585.0,,Not applicable,04-07-2023,26-03-2024,3-4 Brookside,Temple Ewell,,Dover,Kent,CT16 3DT,www.temple-ewell.kent.sch.uk/,1304822665.0,Mrs,Angela,Matthews,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Dover Downs & River,Dover,(England/Wales) Urban city and town,E10000016,628536.0,144298.0,Dover 010,Dover 010G,,,,,Good,South-East England and South London,,100062289542.0,,Not applicable,Not applicable,,,E02005050,E01033210,27.0, +139542,886,Kent,5409,Wrotham School,Academy converter,Academies,Open,Academy Converter,01-04-2013,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,716.0,No Special Classes,19-01-2023,1029.0,553.0,476.0,16.3,Supported by a multi-academy trust,CHARACTER EDUCATION TRUST,Linked to a sponsor,Wrotham School,Not applicable,,10035479.0,,Not applicable,22-05-2019,06-06-2024,Borough Green Road,Wrotham,,Sevenoaks,Kent,TN15 7RD,http://www.wrothamschool.com/,1732905860.0,Mr,Michael,Cater,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Pilgrims with Ightham,Tonbridge and Malling,(England/Wales) Rural village,E10000016,561109.0,158269.0,Tonbridge and Malling 006,Tonbridge and Malling 006F,,,,,Good,South-East England and South London,,100062550537.0,,Not applicable,Not applicable,,,E02005154,E01024786,139.0, +139554,886,Kent,4006,Trinity School,Free schools,Free Schools,Open,New Provision,01-09-2013,,,Secondary,11.0,19,Not applicable,No Nursery Classes,Has a sixth form,Mixed,Christian,Christian,,Non-selective,1140.0,Not applicable,19-01-2023,1130.0,619.0,511.0,14.8,Supported by a single-academy trust,TRINITY SCHOOL SEVENOAKS LTD,-,,Not applicable,,10041643.0,,Not applicable,02-10-2018,02-05-2024,Seal Hollow Rd,,,Sevenoaks,Kent,TN13 3SL,http://www.trinitysevenoaks.org.uk/,1732469111.0,Dr,Matthew,Pawson,Headmaster,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Seal and Weald,Sevenoaks,(England/Wales) Urban city and town,E10000016,554061.0,156627.0,Sevenoaks 012,Sevenoaks 012A,Ofsted,,,,Good,South-East England and South London,,10013774563.0,,Not applicable,Not applicable,,,E02005098,E01024458,136.0, +139615,886,Kent,2511,Hartley Primary Academy,Academy converter,Academies,Open,Academy Converter,01-05-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,443.0,230.0,213.0,9.3,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10041353.0,,Not applicable,11-10-2023,16-04-2024,Round Ash Way,Hartley,,Longfield,Kent,DA3 8BT,www.hartleyprimaryacademy.org.uk/,1474702742.0,Mr,Stuart,Mitchell,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Sevenoaks,Hartley and Hodsoll Street,Dartford,(England/Wales) Urban city and town,E10000016,560221.0,167409.0,Sevenoaks 004,Sevenoaks 004C,,,,,Outstanding,South-East England and South London,,100062688333.0,,Not applicable,Not applicable,,,E02005090,E01024443,40.0, +139664,886,Kent,4007,Wye School,Free schools,Free Schools,Open,New Provision,03-09-2013,,,Secondary,11.0,18,Not applicable,No Nursery Classes,Has a sixth form,Mixed,None,None,,Non-selective,600.0,Not applicable,19-01-2023,573.0,295.0,278.0,17.5,Supported by a multi-academy trust,UNITED LEARNING TRUST,Linked to a sponsor,United Learning Trust,Not applicable,,10041669.0,,Not applicable,11-12-2018,15-04-2024,Olantigh Road,,,Wye,Kent,TN25 5EJ,http://www.wyeschool.org.uk/,1233811110.0,Mr,Luke,Magee,Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Wye with Hinxhill,Ashford,(England/Wales) Rural town and fringe,E10000016,605615.0,146915.0,Ashford 001,Ashford 001E,Ofsted,,,,Good,South-East England and South London,,10012843405.0,,Not applicable,Not applicable,,,E02004996,E01024041,88.0, +139685,886,Kent,2024,Copperfield Academy,Academy sponsor led,Academies,Open,New Provision,01-11-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,480.0,Not applicable,19-01-2023,443.0,231.0,212.0,34.1,Supported by a multi-academy trust,REACH2 ACADEMY TRUST,Linked to a sponsor,REAch2 Academy Trust,Not applicable,,10041614.0,,Not applicable,06-05-2021,13-09-2023,Dover Road East,Northfleet,,Gravesend,Kent,DA11 0RB,www.copperfieldacademy.org,1474352488.0,,Ben,Clark,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Coldharbour & Perry Street,Gravesham,(England/Wales) Urban major conurbation,E10000016,563486.0,173483.0,Gravesham 004,Gravesham 004D,,,,,Good,South-East England and South London,,100062310549.0,,Not applicable,Not applicable,,,E02005058,E01024283,145.0, +139696,886,Kent,2025,The Wells Free School,Free schools,Free Schools,Open,New Provision,01-09-2013,,,Primary,4.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,None,None,,Not applicable,210.0,Not applicable,19-01-2023,208.0,115.0,93.0,13.0,Supported by a single-academy trust,THE WELLS FREE SCHOOL,-,,Not applicable,,10041721.0,,Not applicable,18-06-2019,07-05-2024,King Charles Square,,,Tunbridge Wells,Kent,TN4 8FA,www.thewellsfreeschool.co.uk/,1892739075.0,Mrs,Katharine,Le Page,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Culverden,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558235.0,139866.0,Tunbridge Wells 008,Tunbridge Wells 008E,Ofsted,,,,Good,South-East England and South London,,10024139359.0,,Not applicable,Not applicable,,,E02005169,E01035012,27.0, +139697,886,Kent,4009,Hadlow Rural Community School,Free schools,Free Schools,Open,New Provision,01-09-2013,,,Secondary,11.0,16,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,None,None,,Non-selective,375.0,Not applicable,19-01-2023,371.0,224.0,147.0,24.0,Supported by a single-academy trust,HADLOW RURAL COMMUNITY SCHOOL LIMITED,-,,Not applicable,,10041646.0,,Not applicable,26-02-2019,20-05-2024,Tonbridge Road,,,Hadlow,Kent,TN11 0AU,www.hrcschool.org,1732498120.0,Mr,Paul,Boxall,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Bourne,Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,562706.0,149711.0,Tonbridge and Malling 008,Tonbridge and Malling 008C,Ofsted,,,,Good,South-East England and South London,,10092971671.0,,Not applicable,Not applicable,,,E02005156,E01024745,89.0, +139810,886,Kent,2026,Petham Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,115.0,Not applicable,19-01-2023,103.0,50.0,53.0,16.5,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10042015.0,,Not applicable,05-07-2019,31-05-2024,Petham,,,Canterbury,Kent,CT4 5RD,http://www.petham.kent.sch.uk/,1227700260.0,Mr,Scott,Guy,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Chartham & Stone Street,Canterbury,(England/Wales) Rural village,E10000016,613071.0,151303.0,Canterbury 017,Canterbury 017C,,,,,Good,South-East England and South London,,200000675436.0,,Not applicable,Not applicable,,,E02005026,E01024054,17.0, +139822,886,Kent,2027,Archbishop Courtenay Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Church of England,Diocese of Canterbury,Not applicable,315.0,Not applicable,19-01-2023,307.0,162.0,145.0,36.2,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10042411.0,,Not applicable,07-06-2023,15-04-2024,Eccleston Road,Tovil,,Maidstone,Kent,ME15 6QN,www.archbishopcourtenay.org.uk/,1622754666.0,Mrs,Sue,Heather,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,South,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575645.0,154791.0,Maidstone 009,Maidstone 009D,,,,,Good,South-East England and South London,,10014313238.0,,Not applicable,Not applicable,,,E02005076,E01024401,111.0, +140012,886,Kent,2028,Cliftonville Primary School,Academy sponsor led,Academies,Open,New Provision,01-12-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,862.0,Not applicable,19-01-2023,828.0,407.0,421.0,49.1,Supported by a multi-academy trust,COASTAL ACADEMIES TRUST,Linked to a sponsor,Coastal Academies Trust,Not applicable,,10042921.0,,Not applicable,18-01-2023,22-05-2024,Northumberland Avenue,Cliftonville,,Margate,Kent,CT9 3LY,www.cliftonvilleprimary.co.uk/,1843227575.0,Ms,Claire,Whichcord,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Cliftonville East,South Thanet,(England/Wales) Urban city and town,E10000016,637095.0,170622.0,Thanet 002,Thanet 002C,,,,,Outstanding,South-East England and South London,,100062307559.0,,Not applicable,Not applicable,,,E02005133,E01024655,390.0, +140167,886,Kent,2029,Tymberwood Academy,Academy sponsor led,Academies,Open,New Provision,01-02-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,384.0,186.0,198.0,48.7,Supported by a multi-academy trust,REACH2 ACADEMY TRUST,Linked to a sponsor,REAch2 Academy Trust,Not applicable,,10043284.0,,Not applicable,03-03-2022,03-04-2024,Cerne Road,,,Gravesend,Kent,DA12 4BN,www.tymberwoodacademy.org/,1474361193.0,Mr,Frazer,Westmorland,Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,10.0,8.0,,,South East,Gravesham,Riverview Park,Gravesham,(England/Wales) Urban major conurbation,E10000016,566437.0,172049.0,Gravesham 007,Gravesham 007B,,,,,Good,South-East England and South London,,100062312652.0,,Not applicable,Not applicable,,,E02005061,E01024310,175.0, +140168,886,Kent,2030,Valley Invicta Primary School At Aylesford,Academy sponsor led,Academies,Open,New Provision,01-12-2013,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,315.0,Not applicable,19-01-2023,381.0,189.0,192.0,18.4,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10043311.0,,Not applicable,11-05-2023,25-04-2024,Teapot Lane,,,Aylesford,Kent,ME20 7JU,www.aylesford.viat.org.uk/,1622718192.0,Mr,Billy,Harrington,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford South & Ditton,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,571993.0,158332.0,Tonbridge and Malling 005,Tonbridge and Malling 005B,,,,,Outstanding,South-East England and South London,,100062389712.0,,Not applicable,Not applicable,,,E02005153,E01024718,70.0, +140322,886,Kent,2686,Furley Park Primary Academy,Academy converter,Academies,Open,Academy Converter,01-11-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,575.0,294.0,281.0,16.7,Supported by a multi-academy trust,ACE LEARNING,-,,Not applicable,,10043951.0,,Not applicable,06-07-2022,18-04-2024,Reed Crescent,Park Farm,Kingsnorth,Ashford,Kent,TN23 3PA,www.furleypark.org.uk/,1233501732.0,Mrs,Emma,Collip,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Park Farm South,Ashford,(England/Wales) Urban city and town,E10000016,601223.0,138948.0,Ashford 009,Ashford 009I,,,,,Requires improvement,South-East England and South London,,200002407842.0,,Not applicable,Not applicable,,,E02005004,E01032819,96.0, +140323,886,Kent,2286,Hamstreet Primary Academy,Academy converter,Academies,Open,Academy Converter,01-11-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,315.0,No Special Classes,19-01-2023,269.0,132.0,137.0,15.2,Supported by a multi-academy trust,ACE LEARNING,-,,Not applicable,,10043950.0,,Not applicable,18-05-2023,29-05-2024,Hamstreet,,,Ashford,Kent,TN26 2EA,www.ham-street.org.uk/,1233732577.0,Mrs,Helen,Glancy,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Weald South,Ashford,(England/Wales) Rural town and fringe,E10000016,599958.0,133831.0,Ashford 014,Ashford 014D,,,,,Good,South-East England and South London,,10012848068.0,,Not applicable,Not applicable,,,E02005009,E01024037,41.0, +140393,886,Kent,2034,Thistle Hill Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,359.0,183.0,176.0,41.2,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10053864.0,,Not applicable,27-04-2022,16-04-2024,Aspen Drive,Minster-on-Sea,,Sheerness,Kent,ME12 3UD,http://www.thistlehill.kent.sch.uk,1795899119.0,Ms,Rebecca,Handebeaux,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,10.0,10.0,,,South East,Swale,Sheppey Central,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,594815.0,172145.0,Swale 004,Swale 004H,,,,,Requires improvement,South-East England and South London,,10023203143.0,,Not applicable,Not applicable,,,E02005118,E01035298,148.0, +140430,886,Kent,2036,Valley Invicta Primary School At Leybourne Chase,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,None,,Not applicable,220.0,Not applicable,19-01-2023,227.0,119.0,108.0,11.0,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10053401.0,,Not applicable,22-02-2024,20-05-2024,Derby Drive,Leybourne Chase,,West Malling,Kent,ME19 5FF,www.leybournechase.viat.org.uk,1732840908.0,Mrs,Gemma,Robinson (Head of School),Executive Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,16.0,15.0,,,South East,Tonbridge and Malling,"Birling, Leybourne & Ryarsh",Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,567807.0,159097.0,Tonbridge and Malling 014,Tonbridge and Malling 014G,,,,,Good,South-East England and South London,,100062387483.0,,Not applicable,Not applicable,,,E02006833,E01035010,25.0, +140431,886,Kent,2037,Valley Invicta Primary School at Holborough Lakes,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,227.0,Has Special Classes,19-01-2023,228.0,105.0,123.0,13.2,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10052657.0,,Not applicable,31-01-2024,20-05-2024,Holborough Lakes,Pollyfield Close,,Snodland,Kent,ME6 5GR,www.holboroughlakes.viat.org.uk,1634242839.0,Mrs,Lisa,Vickers,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,17.0,15.0,,,South East,Tonbridge and Malling,Snodland West & Holborough Lakes,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,570281.0,162312.0,Tonbridge and Malling 002,Tonbridge and Malling 002I,,,,,Good,South-East England and South London,,10013924379.0,,Not applicable,Not applicable,,,E02005150,E01035006,30.0, +140432,886,Kent,2038,Valley Invicta Primary School At Kings Hill,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,222.0,122.0,100.0,13.1,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10053404.0,,Not applicable,14-03-2024,22-05-2024,Warwick Way (Off Tower View),Kings Hill,,West Malling,Kent,ME19 4AL,www.kingshill.viat.org.uk,1732841695.0,Mrs,Steph,Guthrie,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,15.0,15.0,,,South East,Tonbridge and Malling,Kings Hill,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,567363.0,155590.0,Tonbridge and Malling 007,Tonbridge and Malling 007E,,,,,Good,South-East England and South London,,10092970953.0,,Not applicable,Not applicable,,,E02005155,E01032825,29.0, +140433,886,Kent,2039,Martello Primary,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,172.0,97.0,75.0,64.0,Supported by a multi-academy trust,TURNER SCHOOLS,Linked to a sponsor,Turner Schools,Not applicable,,10053863.0,,Not applicable,09-03-2022,24-05-2024,Warren Way,,,Folkestone,Kent,CT19 6DT,www.turnermartello.org,1303847540.0,Miss,Natalie,Barrow,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,17.0,15.0,,,South East,Folkestone and Hythe,Folkestone Harbour,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,623927.0,137063.0,Folkestone and Hythe 003,Folkestone and Hythe 003D,,,,,Good,South-East England and South London,,50114714.0,,Not applicable,Not applicable,,,E02005104,E01024503,110.0, +140520,886,Kent,3719,"St Joseph's Catholic Primary School, Aylesham",Academy converter,Academies,Open,Academy Converter,01-01-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,133.0,No Special Classes,19-01-2023,131.0,72.0,59.0,36.6,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10044505.0,,Not applicable,02-11-2021,25-01-2024,Ackholt Road,Aylesham,,Canterbury,Kent,CT3 3AS,www.stjosephs-aylesham.co.uk,1304840370.0,Mrs,Hester,Seager-Fleming,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,"Aylesham, Eythorne & Shepherdswell",Dover,(England/Wales) Rural town and fringe,E10000016,624091.0,152415.0,Dover 006,Dover 006F,,,,,Good,South-East England and South London,,10034883661.0,,Not applicable,Not applicable,,,E02005046,E01035315,48.0, +140521,886,Kent,2435,South Avenue Primary School,Academy converter,Academies,Open,Academy Converter,01-01-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,407.0,205.0,202.0,32.2,Supported by a multi-academy trust,FULSTON MANOR ACADEMIES TRUST,Linked to a sponsor,Fulston Manor Academies Trust,Not applicable,,10044503.0,,Not applicable,12-10-2022,30-05-2024,South Avenue,,,Sittingbourne,Kent,ME10 4SU,www.southavenue.kent.sch.uk,1795477750.0,Miss,Tracy,Cadwallader,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Roman,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591229.0,163193.0,Swale 010,Swale 010D,,,,,Good,South-East England and South London,,200002532192.0,,Not applicable,Not applicable,,,E02005124,E01024599,131.0, +140537,886,Kent,5432,St Simon Stock Catholic School,Academy converter,Academies,Open,Academy Converter,01-01-2014,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,1018.0,No Special Classes,19-01-2023,1106.0,584.0,522.0,14.9,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10044507.0,,Not applicable,13-10-2021,23-04-2024,Oakwood Park,,,Maidstone,Kent,ME16 0JP,http://www.ssscs.co.uk/,1622754551.0,Mrs,Andrea,Denny,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Heath,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,574251.0,155595.0,Maidstone 008,Maidstone 008C,,,,,Good,South-East England and South London,,200003717928.0,,Not applicable,Not applicable,,,E02005075,E01024367,134.0, +140592,886,Kent,2679,The Brent Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,630.0,No Special Classes,19-01-2023,652.0,322.0,330.0,25.0,Supported by a multi-academy trust,CYGNUS ACADEMIES TRUST,Linked to a sponsor,Cygnus Academies Trust,Not applicable,,10044811.0,,Not applicable,22-02-2023,15-04-2024,London Road,Stone,,Dartford,Kent,DA2 6BA,www.brent.kent.sch.uk,1322223943.0,Mrs,Sarah,Rye,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Stone House,Dartford,(England/Wales) Urban major conurbation,E10000016,556186.0,173935.0,Dartford 006,Dartford 006A,,,,,Outstanding,South-East England and South London,,200000534113.0,,Not applicable,Not applicable,,,E02005033,E01024169,162.0, +140593,886,Kent,2685,The Gateway Primary Academy,Academy converter,Academies,Open,Academy Converter,01-02-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,210.0,110.0,100.0,12.4,Supported by a single-academy trust,THE GATEWAY PRIMARY ACADEMY,-,,Not applicable,,10044810.0,,Not applicable,29-06-2022,14-04-2024,Milestone Road,,,Dartford,Kent,DA2 6DW,www.gateway-pri.kent.sch.uk/,1322220090.0,Mr,Jamiel,Cassem,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Stone House,Dartford,(England/Wales) Urban major conurbation,E10000016,555850.0,174010.0,Dartford 005,Dartford 005D,,,,,Good,South-East England and South London,,200000544345.0,,Not applicable,Not applicable,,,E02005032,E01024163,26.0, +140595,886,Kent,5418,The Skinners' School,Academy converter,Academies,Open,Academy Converter,01-02-2014,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,945.0,No Special Classes,19-01-2023,1118.0,1118.0,0.0,4.2,Supported by a multi-academy trust,SKINNERS' ACADEMIES TRUST,Linked to a sponsor,The Skinners' Company,Not applicable,,10044808.0,,Not applicable,17-11-2021,25-04-2024,St John's Road,,,Tunbridge Wells,Kent,TN4 9PG,http://www.skinners-school.co.uk,1892520732.0,Mr,Edward,Wesson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,VI - Visual Impairment,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,Resourced provision,2.0,2.0,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558264.0,140612.0,Tunbridge Wells 007,Tunbridge Wells 007D,,,,,Good,South-East England and South London,,10008661460.0,,Not applicable,Not applicable,,,E02005168,E01024838,34.0, +140640,886,Kent,5435,St Gregory's Catholic School,Academy converter,Academies,Open,Academy Converter,01-03-2014,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,1082.0,No Special Classes,19-01-2023,1339.0,796.0,543.0,18.3,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10045187.0,,Not applicable,,07-05-2024,Reynolds Lane,,,Tunbridge Wells,Kent,TN4 9XL,http://www.sgschool.org.uk,1892527444.0,Mr,Michael,Wilson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,,,,,,,,,,,,,Resourced provision,,,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558104.0,141564.0,Tunbridge Wells 002,Tunbridge Wells 002A,,,,,,South-East England and South London,,100062586238.0,,Not applicable,Not applicable,,,E02005163,E01024837,201.0, +140641,886,Kent,3890,"St Joseph's Catholic Primary School, Broadstairs",Academy converter,Academies,Open,Academy Converter,01-03-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,Not applicable,19-01-2023,199.0,100.0,99.0,30.7,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10045193.0,,Not applicable,09-06-2022,21-05-2024,"St Joseph's Catholic Primary School, Broadstairs",,St Peter's Park Road,Broadstairs,Kent,CT10 2BA,www.st-josephs-broadstairs.kent.sch.uk,1843861738.0,Mrs,Vicki,O'Halloran,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,Not Applicable,,,,,,,,,,,,,Resourced provision,186.0,210.0,,,South East,Thanet,St Peters,South Thanet,(England/Wales) Urban city and town,E10000016,638835.0,168425.0,Thanet 009,Thanet 009E,,,,,Requires improvement,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005140,E01024693,61.0, +140800,886,Kent,3900,Whitehill Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,659.0,347.0,312.0,29.7,Supported by a multi-academy trust,THE DECUS EDUCATIONAL TRUST,Linked to a sponsor,Gravesend Grammar School,Not applicable,,10045602.0,,Not applicable,28-02-2024,22-05-2024,Sun Lane,,,Gravesend,Kent,DA12 5HN,www.whitehillprimary.com,1474352973.0,Mrs,Angela,Carpenter,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Gravesham,Whitehill & Windmill Hill,Gravesham,(England/Wales) Urban major conurbation,E10000016,565183.0,172378.0,Gravesham 005,Gravesham 005D,,,,,Requires improvement,South-East England and South London,,10012028072.0,,Not applicable,Not applicable,,,E02005059,E01024313,185.0, +140873,886,Kent,3889,"St Gregory's Catholic Primary School, Margate",Academy converter,Academies,Open,Academy Converter,01-05-2014,,,Primary,3.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,315.0,No Special Classes,19-01-2023,291.0,145.0,146.0,35.1,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10045950.0,,Not applicable,19-09-2019,14-05-2024,Nash Road,,,Margate,Kent,CT9 4BU,www.st-gregorys.kent.sch.uk,1843221896.0,Mr,David,Walker,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Salmestone,North Thanet,(England/Wales) Rural town and fringe,E10000016,635277.0,169530.0,Thanet 004,Thanet 004E,,,,,Good,South-East England and South London,,100062307601.0,,Not applicable,Not applicable,,,E02005135,E01024696,102.0, +140874,886,Kent,5446,"St Anselm's Catholic School, Canterbury",Academy converter,Academies,Open,Academy Converter,01-05-2014,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,1071.0,No Special Classes,19-01-2023,1099.0,555.0,544.0,18.0,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10045938.0,,Not applicable,13-09-2023,05-06-2024,Old Dover Road,,,Canterbury,Kent,CT1 3EN,http://www.stanselmscanterbury.org.uk/,1227826200.0,Mr,J,Rowarth,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,PD - Physical Disability,,,,,,,,,,,,Resourced provision,16.0,15.0,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,616376.0,156068.0,Canterbury 016,Canterbury 016C,,,,,Good,South-East England and South London,,200000678848.0,,Not applicable,Not applicable,,,E02005025,E01024046,163.0, +140899,886,Kent,2223,Bobbing Village School,Academy converter,Academies,Open,Academy Converter,01-06-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,208.0,114.0,94.0,8.2,Supported by a multi-academy trust,BOURNE ALLIANCE MULTI ACADEMY TRUST,Linked to a sponsor,TIMU Academy Trust,Not applicable,,10046193.0,,Not applicable,22-02-2023,19-03-2024,Sheppey Way,Bobbing,,Sittingbourne,Kent,ME9 8PL,www.bourne-alliance-mat.org.uk,1795423939.0,Mr,Tim,Harwood,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,"Bobbing, Iwade and Lower Halstow",Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,588784.0,165079.0,Swale 009,Swale 009F,,,,,Outstanding,South-East England and South London,,200002532781.0,,Not applicable,Not applicable,,,E02005123,E01035304,17.0, +140900,886,Kent,2230,Iwade School,Academy converter,Academies,Open,Academy Converter,01-06-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,621.0,326.0,295.0,12.6,Supported by a multi-academy trust,BOURNE ALLIANCE MULTI ACADEMY TRUST,Linked to a sponsor,TIMU Academy Trust,Not applicable,,10046192.0,,Not applicable,22-09-2022,19-10-2023,School Lane,Iwade,,Sittingbourne,Kent,ME9 8RS,www.iwade-ba-mat.org.uk,1795472578.0,Mrs,Katrine,Stewart,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,"Bobbing, Iwade and Lower Halstow",Sittingbourne and Sheppey,(England/Wales) Rural town and fringe,E10000016,589970.0,167845.0,Swale 007,Swale 007G,,,,,Good,South-East England and South London,,100062397902.0,,Not applicable,Not applicable,,,E02005121,E01032655,78.0, +140980,886,Kent,2041,The Holy Family Catholic Primary School,Academy sponsor led,Academies,Open,New Provision,01-06-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Roman Catholic,Archdiocese of Southwark,Not applicable,210.0,Not applicable,19-01-2023,209.0,112.0,97.0,51.7,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10046167.0,,Not applicable,12-10-2023,23-04-2024,Bicknor Road,Park Wood,,Maidstone,Kent,ME15 9PS,www.holyfamily.kent.sch.uk,1622756778.0,Mrs,Megan,Underhill,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Park Wood,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,578525.0,151872.0,Maidstone 013,Maidstone 013B,,,,,Good,South-East England and South London,,200003680167.0,,Not applicable,Not applicable,,,E02005080,E01024390,108.0, +140987,886,Kent,4012,The Leigh UTC,University technical college,Free Schools,Open,New Provision,01-09-2014,,,Secondary,11.0,18,Not applicable,No Nursery Classes,Has a sixth form,Mixed,None,Does not apply,,,960.0,Not applicable,19-01-2023,730.0,558.0,172.0,28.8,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10047247.0,,Not applicable,26-05-2022,15-04-2024,Brunel Way,,,Dartford,Kent,DA1 5TF,http://theleighutc.org.uk/,1322626600.0,Mr,Kevin,Watson,Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Bridge,Dartford,(England/Wales) Rural town and fringe,E10000016,554583.0,175781.0,Dartford 001,Dartford 001F,,,,,Good,South-East England and South London,,10023439451.0,,Not applicable,Not applicable,,,E02005028,E01035271,178.0, +141025,886,Kent,2043,Jubilee Primary School,Free schools,Free Schools,Open,New Provision,01-09-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Christian,,,420.0,Not applicable,19-01-2023,301.0,160.0,141.0,25.2,Supported by a single-academy trust,JUBILEE PRIMARY SCHOOL,-,,Not applicable,,10047086.0,,Not applicable,18-10-2023,23-04-2024,Gatland Lane,,,Maidstone,Kent,ME16 8PF,www.jubileeprimaryschool.org.uk,1622808873.0,Dr,Marilyn,Nadesan,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Fant,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,573703.0,154618.0,Maidstone 009,Maidstone 009A,,,,,Outstanding,South-East England and South London,,200003717426.0,,Not applicable,Not applicable,,,E02005076,E01024360,76.0, +141065,886,Kent,3720,St Mary's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-07-2014,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,185.0,94.0,91.0,25.9,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10046463.0,,Not applicable,17-11-2022,26-04-2024,St Richard's Road,,,Deal,Kent,CT14 9LF,www.stmarysdeal.co.uk/,1304375046.0,Mrs,Maria,Pullen,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Mill Hill,Dover,(England/Wales) Urban city and town,E10000016,635925.0,151281.0,Dover 005,Dover 005C,,,,,Good,South-East England and South London,,200001851337.0,,Not applicable,Not applicable,,,E02005045,E01024223,48.0, +141067,886,Kent,3743,"St Simon of England Roman Catholic Primary School, Ashford",Academy converter,Academies,Open,Academy Converter,01-07-2014,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,209.0,112.0,97.0,18.7,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10046324.0,,Not applicable,01-12-2022,21-05-2024,Noakes Meadow,,,Ashford,Kent,TN23 4RB,www.st-simon.kent.sch.uk,1233623199.0,Mr,Peter,McCabe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Beaver,Ashford,(England/Wales) Urban city and town,E10000016,600090.0,141538.0,Ashford 007,Ashford 007B,,,,,Requires improvement,South-East England and South London,,100062559274.0,,Not applicable,Not applicable,,,E02005002,E01023975,39.0, +141085,886,Kent,2045,Skinners' Kent Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,None,None,,,210.0,Not applicable,19-01-2023,210.0,117.0,93.0,17.1,Supported by a multi-academy trust,SKINNERS' ACADEMIES TRUST,Linked to a sponsor,The Skinners' Company,Not applicable,,10053936.0,,Not applicable,07-02-2024,21-05-2024,The Avenue,Knights Wood,,Tunbridge Wells,Kent,TN2 3GS,www.skps.org.uk,1892553060.0,,Gemma,Wyatt,Executive Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Sherwood,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,560430.0,141932.0,Tunbridge Wells 003,Tunbridge Wells 003B,,,,,Good,South-East England and South London,,10008671585.0,,Not applicable,Not applicable,,,E02005164,E01024839,36.0, +141156,886,Kent,3744,St Margaret Clitherow Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-08-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,420.0,No Special Classes,19-01-2023,397.0,191.0,206.0,7.3,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10046759.0,,Not applicable,02-11-2022,16-04-2024,Trench Road,,,Tonbridge,Kent,TN11 9NG,www.stmargaretclitherowschool.org.uk,1732358000.0,Mrs,Fiona,Oubridge,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tonbridge and Malling,Hildenborough,Tonbridge and Malling,(England/Wales) Rural village,E10000016,558678.0,148870.0,Tonbridge and Malling 010,Tonbridge and Malling 010D,,,,,Good,South-East England and South London,,10002908678.0,,Not applicable,Not applicable,,,E02005158,E01024754,29.0, +141157,886,Kent,3751,"St Thomas' Catholic Primary School, Sevenoaks",Academy converter,Academies,Open,Academy Converter,01-08-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,240.0,No Special Classes,19-01-2023,218.0,108.0,110.0,8.4,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10046758.0,,Not applicable,,17-05-2024,South Park,,,Sevenoaks,Kent,TN13 1EH,www.saintthomas.co.uk,1732453921.0,Mrs,Geraldine,Leahy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Sevenoaks,Sevenoaks Town and St John's,Sevenoaks,(England/Wales) Urban city and town,E10000016,552746.0,154671.0,Sevenoaks 012,Sevenoaks 012F,,,,,,South-East England and South London,,10035182527.0,,Not applicable,Not applicable,,,E02005098,E01024471,17.0, +141216,886,Kent,2048,Reculver Church of England Primary School,Academy sponsor led,Academies,Open,New Provision,01-07-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Church of England,Diocese of Canterbury,Not applicable,525.0,Not applicable,19-01-2023,493.0,247.0,246.0,20.7,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10047006.0,,Not applicable,04-07-2018,02-05-2024,Hillborough,,,Herne Bay,Kent,CT6 6TA,www.reculver.kent.sch.uk,1227375907.0,Mrs,Stella,Collins,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,VI - Visual Impairment,SLD - Severe Learning Difficulty,,,,,,,,,,,,Resourced provision,17.0,15.0,,,South East,Canterbury,Reculver,North Thanet,(England/Wales) Urban city and town,E10000016,621137.0,167984.0,Canterbury 002,Canterbury 002A,,,,,Outstanding,South-East England and South London,,10033163216.0,,Not applicable,Not applicable,,,E02005011,E01024094,102.0, +141217,886,Kent,4013,St Edmund's Catholic School,Academy sponsor led,Academies,Open,,01-07-2016,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Roman Catholic,None,Archdiocese of Southwark,Non-selective,600.0,Not applicable,19-01-2023,567.0,296.0,271.0,36.5,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047005.0,,Not applicable,13-07-2022,21-05-2024,Old Charlton Road,,,Dover,Kent,CT16 2QB,www.st-edmunds.com,1304201551.0,Mrs,Grainne,Parsons,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Buckland,Dover,(England/Wales) Urban city and town,E10000016,631515.0,142620.0,Dover 011,Dover 011C,,,,,Good,South-East England and South London,,100062289422.0,,Not applicable,Not applicable,,,E02005051,E01024195,207.0, +141220,886,Kent,2051,St Mary of Charity CofE (Aided) Primary School,Academy sponsor led,Academies,Open,New Provision,01-08-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Not applicable,210.0,Not applicable,19-01-2023,208.0,105.0,103.0,36.5,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10047011.0,,Not applicable,11-07-2018,28-03-2024,Orchard Place,,,Faversham,Kent,ME13 8AP,,1795532496.0,Mrs,Louise,Rowley-Jones,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Abbey,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,601949.0,161379.0,Swale 015,Swale 015A,,,,,Outstanding,South-East England and South London,,200002530657.0,,Not applicable,Not applicable,,,E02005129,E01024551,76.0, +141308,886,Kent,3119,Adisham Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,97.0,49.0,48.0,19.6,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10047192.0,,Not applicable,29-11-2023,17-05-2024,The Street,Adisham,,Canterbury,Kent,CT3 3JW,www.adisham.kent.sch.uk/,1304840246.0,Miss,Sophie,Metcalf,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Little Stour & Adisham,Canterbury,(England/Wales) Rural village,E10000016,622676.0,153952.0,Canterbury 018,Canterbury 018A,,,,,Outstanding,South-East England and South London,,100062619270.0,,Not applicable,Not applicable,,,E02005027,E01024042,19.0, +141329,886,Kent,2052,Kennington Church of England Academy,Academy sponsor led,Academies,Open,New Provision,01-11-2014,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Not applicable,360.0,Not applicable,19-01-2023,356.0,179.0,177.0,29.5,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10047449.0,,Not applicable,08-03-2023,29-01-2024,Upper Vicarage Road,Kennington,,Ashford,Kent,TN24 9AG,http://www.kenningtonacademy.co.uk,1233623744.0,Mrs,Karen,Godsell,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Kennington,Ashford,(England/Wales) Urban city and town,E10000016,602027.0,145151.0,Ashford 003,Ashford 003B,,,,,Good,South-East England and South London,,100062561303.0,,Not applicable,Not applicable,,,E02004998,E01023999,105.0, +141386,886,Kent,2054,St Edward's Catholic Primary School,Academy sponsor led,Academies,Open,,01-07-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,None,Archdiocese of Southwark,Not applicable,210.0,Not applicable,19-01-2023,197.0,97.0,100.0,50.3,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047523.0,,Not applicable,22-05-2019,05-06-2024,New Road,,,Sheerness,Kent,ME12 1BW,www.st-edwards-sheerness.co.uk,1795662708.0,Mrs,Sara,Wakefield,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheerness,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591704.0,174591.0,Swale 002,Swale 002B,,,,,Good,South-East England and South London,,200002533178.0,,Not applicable,Not applicable,,,E02005116,E01024614,99.0, +141471,886,Kent,3714,St Peter's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,212.0,107.0,105.0,11.8,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047621.0,,Not applicable,07-02-2024,20-05-2024,West Ridge,,,Sittingbourne,Kent,ME10 1UJ,www.st-peters-sittingbourne.co.uk,1795423479.0,Ms,Catherine,Vedamuttu,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Homewood,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,589880.0,162952.0,Swale 012,Swale 012D,,,,,Good,South-East England and South London,,200002527647.0,,Not applicable,Not applicable,,,E02005126,E01024630,25.0, +141472,886,Kent,3745,More Park Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,207.0,95.0,112.0,8.7,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047622.0,,Not applicable,23-02-2023,16-04-2024,Lucks Hill,,,West Malling,Kent,ME19 6HN,www.moreparkprimary.co.uk/,1732843047.0,Mrs,Deborah,Seal,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,"East Malling, West Malling & Offham",Tonbridge and Malling,(England/Wales) Rural hamlet and isolated dwellings,E10000016,568701.0,157897.0,Tonbridge and Malling 014,Tonbridge and Malling 014C,,,,,Good,South-East England and South London,,200000960428.0,,Not applicable,Not applicable,,,E02006833,E01024783,18.0, +141497,886,Kent,3740,St Richard's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,191.0,80.0,111.0,30.9,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047638.0,,Not applicable,06-10-2022,29-05-2024,Castle Avenue,,,Dover,Kent,CT16 1EZ,http://www.st-richards.kent.sch.uk,1304201118.0,Mr,Colin,Taylor,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Town & Castle,Dover,(England/Wales) Urban city and town,E10000016,631763.0,142271.0,Dover 012,Dover 012E,,,,,Good,South-East England and South London,,100062288989.0,,Not applicable,Not applicable,,,E02005052,E01033209,59.0, +141532,886,Kent,5217,"Our Lady of Hartley Catholic Primary School, Hartley, Longfield",Academy converter,Academies,Open,Academy Converter,01-11-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,218.0,No Special Classes,19-01-2023,212.0,95.0,117.0,4.2,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047893.0,,Not applicable,,14-11-2023,Stack Lane,Hartley,,Longfield,Kent,DA3 8BL,http://www.ourladyhartley.kent.sch.uk/,1474706385.0,Mr,James,Baker,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Hartley and Hodsoll Street,Dartford,(England/Wales) Urban city and town,E10000016,560720.0,167649.0,Sevenoaks 004,Sevenoaks 004A,,,,,,South-East England and South London,,10035182405.0,,Not applicable,Not applicable,,,E02005090,E01024441,9.0, +141534,886,Kent,2069,Dartford Primary Academy,Academy converter,Academies,Open,Academy Converter,01-11-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,654.0,No Special Classes,19-01-2023,673.0,333.0,340.0,17.2,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10047892.0,,Not applicable,13-09-2023,09-04-2024,York Road,,,Dartford,Kent,DA1 1SQ,www.dartfordprimary.org.uk/,1322224453.0,Miss,Rebecca,Roberts,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Brent,Dartford,(England/Wales) Urban major conurbation,E10000016,554979.0,173734.0,Dartford 008,Dartford 008C,,,,,Good,South-East England and South London,,200000533723.0,,Not applicable,Not applicable,,,E02005035,E01024137,115.0, +141548,886,Kent,2055,Lansdowne Primary School,Academy sponsor led,Academies,Open,New Provision,01-11-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,397.0,201.0,196.0,33.5,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10047877.0,,Not applicable,14-12-2022,29-05-2024,Gladstone Drive,,,Sittingbourne,Kent,ME10 3BH,www.lansdowne.kent.sch.uk/,1795423752.0,Mrs,Claire,Jobe,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Murston,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,592376.0,163394.0,Swale 011,Swale 011B,,,,,Good,South-East England and South London,,200002528849.0,,Not applicable,Not applicable,,,E02005125,E01024592,133.0, +141578,886,Kent,3019,Shorne Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-12-2014,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,209.0,114.0,95.0,4.8,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10048077.0,,Not applicable,08-03-2023,23-04-2024,Cob Drive,Shorne,,Gravesend,Kent,DA12 3DU,www.shorne.kent.sch.uk,1474822312.0,Miss,Tara,Hewett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Higham & Shorne,Gravesham,(England/Wales) Rural village,E10000016,569273.0,171239.0,Gravesham 010,Gravesham 010E,,,,,Good,South-East England and South London,,100062312571.0,,Not applicable,Not applicable,,,E02005064,E01024301,10.0, +141579,886,Kent,5210,St Botolph's Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-12-2014,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,422.0,197.0,225.0,25.6,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10048078.0,,Not applicable,29-03-2023,10-05-2024,Dover Road,Northfleet,,Gravesend,Kent,DA11 9PL,www.st-botolphs.kent.sch.uk,1474365737.0,Mrs,Alice,Martin,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Rosherville,Gravesham,(England/Wales) Urban major conurbation,E10000016,562966.0,173733.0,Gravesham 001,Gravesham 001A,,,,,Good,South-East England and South London,,100062311179.0,,Not applicable,Not applicable,,,E02005055,E01024276,108.0, +141580,886,Kent,5222,"St Joseph's Catholic Primary School, Northfleet",Academy converter,Academies,Open,Academy Converter,01-12-2014,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,208.0,106.0,102.0,15.4,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10047684.0,,Not applicable,11-01-2023,13-09-2023,Springhead Road,Northfleet,,Gravesend,Kent,DA11 9QZ,www.st-josephs-northfleet.kent.sch.uk/,1474533515.0,Mr,Andrew,Baldock,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Northfleet & Springhead,Gravesham,(England/Wales) Urban major conurbation,E10000016,562589.0,173691.0,Gravesham 001,Gravesham 001B,,,,,Outstanding,South-East England and South London,,100062311131.0,,Not applicable,Not applicable,,,E02005055,E01024277,32.0, +141628,886,Kent,4633,Ursuline College,Academy converter,Academies,Open,Academy Converter,01-01-2015,,,Secondary,11.0,18,Not applicable,Not applicable,Has a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,763.0,Not applicable,19-01-2023,978.0,473.0,505.0,26.4,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10048333.0,,Not applicable,09-11-2022,03-06-2024,225 Canterbury Road,,,Westgate-on-Sea,Kent,CT8 8LX,http://www.ursuline.kent.sch.uk/,1843834431.0,Miss,Danielle,Lancefield,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Westgate-on-Sea,North Thanet,(England/Wales) Urban city and town,E10000016,631675.0,169531.0,Thanet 007,Thanet 007B,,,,,Good,South-East England and South London,,200003078464.0,,Not applicable,Not applicable,,,E02005138,E01024713,218.0, +141629,886,Kent,5216,Stella Maris Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-01-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,211.0,106.0,105.0,19.0,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10048334.0,,Not applicable,21-06-2023,01-05-2024,Parkfield Road,,,Folkestone,Kent,CT19 5BY,www.stellamaris.kent.sch.uk/,1303252127.0,Mr,Andrew,Langley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Broadmead,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,622234.0,136791.0,Folkestone and Hythe 006,Folkestone and Hythe 006E,,,,,Good,South-East England and South London,,50040637.0,,Not applicable,Not applicable,,,E02005107,E01024515,40.0, +141650,886,Kent,2180,South Borough Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2015,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,480.0,No Special Classes,19-01-2023,509.0,240.0,269.0,23.2,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10048683.0,,Not applicable,26-04-2023,18-04-2024,Stagshaw Close,Postley Road,,Maidstone,Kent,ME15 6TL,http://www.southboroughprimary.org.uk,1622752161.0,Mr,Mathew,Currie,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,High Street,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576421.0,154655.0,Maidstone 009,Maidstone 009C,,,,,Good,South-East England and South London,,200003683342.0,,Not applicable,Not applicable,,,E02005076,E01024374,116.0, +141659,886,Kent,2058,Charlton Church of England Primary School,Academy sponsor led,Academies,Open,New Provision,01-03-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,None,Diocese of Canterbury,Not applicable,210.0,Not applicable,19-01-2023,199.0,88.0,111.0,35.7,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10048211.0,,Not applicable,22-11-2023,23-04-2024,Barton Road,,,Dover,Kent,CT16 2LX,www.charlton.kent.sch.uk/,1304201275.0,Mrs,Sally-Anne,Pettersen,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Buckland,Dover,(England/Wales) Urban city and town,E10000016,631452.0,142470.0,Dover 011,Dover 011B,,,,,Good,South-East England and South London,,100062289316.0,,Not applicable,Not applicable,,,E02005051,E01024194,71.0, +141660,886,Kent,2059,Lydd Primary School,Academy sponsor led,Academies,Open,New Provision,01-03-2015,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,280.0,Not applicable,19-01-2023,274.0,150.0,124.0,50.6,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10048212.0,,Not applicable,13-09-2023,07-05-2024,20 Skinner Road,Lydd,,Romney Marsh,Kent,TN29 9HW,www.lyddprimary.org.uk,1797320362.0,Mrs,Nicki,Man,Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Walland & Denge Marsh,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,604171.0,120623.0,Folkestone and Hythe 013,Folkestone and Hythe 013C,,,,,Good,South-East England and South London,,50114553.0,,Not applicable,Not applicable,,,E02005114,E01024534,129.0, +141754,886,Kent,2625,Godinton Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,419.0,229.0,190.0,13.6,Supported by a single-academy trust,GODINTON ACADEMY TRUST,-,,Not applicable,,10048785.0,,Not applicable,07-02-2024,03-06-2024,Lockholt Close,,,Ashford,Kent,TN23 3JR,http://www.godinton.kent.sch.uk,1233621616.0,,Jillian,Talbot,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Godinton,Ashford,(England/Wales) Urban city and town,E10000016,599006.0,143191.0,Ashford 016,Ashford 016D,,,,,Good,South-East England and South London,,100062558565.0,,Not applicable,Not applicable,,,E02007047,E01023993,57.0, +141766,886,Kent,2596,Chilton Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,418.0,211.0,207.0,18.9,Supported by a multi-academy trust,VIKING ACADEMY TRUST,-,,Not applicable,,10048967.0,,Not applicable,10-01-2019,17-09-2023,Chilton Lane,,,Ramsgate,Kent,CT11 0LQ,http://www.chiltonprimary.co.uk/,1843597695.0,Mr,Alex,McAuley,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Cliffsend and Pegwell,South Thanet,(England/Wales) Urban city and town,E10000016,636338.0,164593.0,Thanet 017,Thanet 017A,,,,,Outstanding,South-East England and South London,,100062281920.0,,Not applicable,Not applicable,,,E02005148,E01024650,79.0, +141871,886,Kent,2060,Beaver Green Primary School,Academy sponsor led,Academies,Open,New Provision,01-04-2015,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,480.0,248.0,232.0,44.6,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10049033.0,,Not applicable,14-03-2023,15-05-2024,Cuckoo Lane,,,Ashford,Kent,TN23 5DA,www.beaver-green.kent.sch.uk,1233621989.0,Ms,Tina,Oakley,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Beaver,Ashford,(England/Wales) Urban city and town,E10000016,599449.0,141135.0,Ashford 007,Ashford 007C,,,,,Good,South-East England and South London,,200004396685.0,,Not applicable,Not applicable,,,E02005002,E01023977,193.0, +141881,886,Kent,2061,Finberry Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,2.0,11,,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,350.0,Has Special Classes,19-01-2023,334.0,175.0,159.0,22.3,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10053381.0,,Not applicable,24-01-2024,20-05-2024,Avocet Way,Finberry,,Ashford,Kent,TN25 7GS,http://www.finberryprimaryschool.org.uk/,1233622686.0,Headteacher,Siobhan,Risley,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,"SEMH - Social, Emotional and Mental Health",,,,,,,,,,,,,Resourced provision,5.0,8.0,,,South East,Ashford,"Mersham, Sevington South with Finberry",Ashford,(England/Wales) Urban city and town,E10000016,602230.0,139442.0,Ashford 010,Ashford 010E,,,,,Good,South-East England and South London,United Kingdom,10012868578.0,,Not applicable,Not applicable,,,E02005005,E01034987,71.0, +142052,886,Kent,2063,Istead Rise Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,243.0,125.0,118.0,16.5,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10049666.0,,Not applicable,28-02-2024,22-05-2024,Downs Road,Northfleet,,Gravesend,Kent,DA13 9HG,,1474833177.0,Mr,Steven,Payne,Executive Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,"Istead Rise, Cobham & Luddesdown",Gravesham,(England/Wales) Rural town and fringe,E10000016,563354.0,169714.0,Gravesham 012,Gravesham 012A,,,,,Good,South-East England and South London,,10012011594.0,,Not applicable,Not applicable,,,E02005066,E01024268,40.0, +142117,886,Kent,2064,Ramsgate Arts Primary School,Free schools,Free Schools,Open,New Provision,01-09-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,None,,,420.0,Not applicable,19-01-2023,355.0,161.0,194.0,29.9,Supported by a multi-academy trust,VIKING ACADEMY TRUST,-,,Not applicable,,10053814.0,,Not applicable,28-09-2023,13-12-2023,140-144 Newington Road,,,Ramsgate,Kent,CT12 6PP,http://www.ramsgateartsprimaryschool.co.uk/,1843582847.0,Mr,Nicholas,Budge,Executive Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Northwood,South Thanet,(England/Wales) Urban city and town,E10000016,636761.0,166126.0,Thanet 013,Thanet 013D,,,,,Good,South-East England and South London,,100061133646.0,,Not applicable,Not applicable,,,E02005144,E01024685,106.0, +142156,886,Kent,3708,"St John's Catholic Primary School, Gravesend",Academy converter,Academies,Open,Academy Converter,01-07-2015,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,840.0,No Special Classes,19-01-2023,886.0,441.0,445.0,16.5,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10053098.0,,Not applicable,18-10-2023,23-04-2024,Rochester Road,,,Gravesend,Kent,DA12 2SY,http://www.stjohnsprimary.kent.sch.uk,1474534546.0,Co Headteacher,Caroline Barron,Paula Cooneyhan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Gravesham,Denton,Gravesham,(England/Wales) Urban major conurbation,E10000016,565990.0,173541.0,Gravesham 003,Gravesham 003C,,,,,Good,South-East England and South London,,100062311904.0,,Not applicable,Not applicable,,,E02005057,E01024293,142.0, +142162,886,Kent,3715,"St Mary's Catholic Primary School, Whitstable",Academy converter,Academies,Open,Academy Converter,01-07-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,420.0,No Special Classes,19-01-2023,374.0,186.0,188.0,12.3,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10053095.0,,Not applicable,08-11-2023,21-05-2024,Northwood Road,,,Whitstable,Kent,CT5 2EY,http://www.st-marys-whitstable.kent.sch.uk,1227272692.0,Mrs,Michele,Blunt,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Canterbury,Tankerton,Canterbury,(England/Wales) Urban city and town,E10000016,611618.0,166842.0,Canterbury 007,Canterbury 007E,,,,,Good,South-East England and South London,,100062300983.0,,Not applicable,Not applicable,,,E02005016,E01024116,46.0, +142188,886,Kent,2073,Langley Park Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2016,,,Primary,3.0,11,,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,451.0,232.0,219.0,15.5,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10053393.0,,Not applicable,19-06-2019,15-04-2024,Edmett Way,,,Maidstone,Kent,ME17 3FX,www.langleyparkprimaryacademy.org.uk,1622250880.0,Miss,Sally,Brading,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,10.0,10.0,,,South East,Maidstone,Park Wood,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,579116.0,151664.0,Maidstone 015,Maidstone 015H,,,,,Good,South-East England and South London,,10093304704.0,,Not applicable,Not applicable,,,E02005082,E01034999,70.0, +142346,886,Kent,2110,Culverstone Green Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,212.0,121.0,91.0,10.8,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10054199.0,,Not applicable,18-10-2018,18-04-2024,Wrotham Road,Meopham,,Gravesend,Kent,DA13 0RF,http://www.cgps.kent.sch.uk,1732822568.0,,James,Bernard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Meopham South & Vigo,Gravesham,(England/Wales) Rural hamlet and isolated dwellings,E10000016,563542.0,163114.0,Gravesham 013,Gravesham 013A,,,,,Good,South-East England and South London,,100062312941.0,,Not applicable,Not applicable,,,E02005067,E01024273,23.0, +142347,886,Kent,2650,Dymchurch Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,311.0,No Special Classes,19-01-2023,159.0,73.0,86.0,36.5,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10054200.0,,Not applicable,27-04-2022,15-05-2024,New Hall Close,Dymchurch,,Romney Marsh,Kent,TN29 0LE,http://www.dymchurch.kent.sch.uk,1303872377.0,Mr,Iain,Rudgyard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Romney Marsh,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,610150.0,129670.0,Folkestone and Hythe 011,Folkestone and Hythe 011A,,,,,Requires improvement,South-East England and South London,,50009728.0,,Not applicable,Not applicable,,,E02005112,E01024486,58.0, +142363,886,Kent,2462,Riverview Infant School,Academy converter,Academies,Open,Academy Converter,01-10-2015,,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,330.0,167.0,163.0,19.1,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10054175.0,,Not applicable,08-12-2021,09-04-2024,Cimba Wood,,,Gravesend,Kent,DA12 4SD,www.riverview-infant.com,1474566484.0,Mrs,Kerrie,Ward,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Riverview Park,Gravesham,(England/Wales) Urban major conurbation,E10000016,566337.0,171561.0,Gravesham 008,Gravesham 008B,,,,,Good,South-East England and South London,,10012012176.0,,Not applicable,Not applicable,,,E02005062,E01024298,63.0, +142372,886,Kent,5228,St Georges CofE (Aided) Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,393.0,189.0,204.0,20.6,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10054170.0,,Not applicable,07-02-2024,20-05-2024,Chequers Road,,,Sheerness,Kent,ME12 3QU,http://www.st-georges-sheppey.kent.sch.uk/,1795877667.0,Mr,Howard,Fisher,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheppey Central,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,596107.0,172691.0,Swale 005,Swale 005D,,,,,Good,South-East England and South London,,200002530151.0,,Not applicable,Not applicable,,,E02005119,E01024620,81.0, +142429,886,Kent,3140,Kingsnorth Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2015,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,420.0,202.0,218.0,11.2,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10054668.0,,Not applicable,09-10-2018,22-04-2024,Church Hill,Kingsnorth,,Ashford,Kent,TN23 3EF,www.kingsnorth.kent.sch.uk,1233622673.0,Mr,Iain,Witts,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Kingsnorth Village & Bridgefield,Ashford,(England/Wales) Urban city and town,E10000016,600571.0,139333.0,Ashford 010,Ashford 010G,,,,,Good,South-East England and South London,,100062558563.0,,Not applicable,Not applicable,,,E02005005,E01034989,47.0, +142517,886,Kent,2076,Cherry Orchard Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2017,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,,,,Not applicable,450.0,Not applicable,19-01-2023,446.0,223.0,223.0,13.6,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10064709.0,,,10-11-2021,11-01-2024,Cherry Orchard,Cherry Orchard Road,Ebbsfleet Valley,Ebbsfleet,Kent,DA10 1AD,www.cherryorchardprimaryacademy.org.uk,1322242011.0,Mrs,Julie,Forsythe,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,14.0,15.0,,,South East,Dartford,Ebbsfleet,Dartford,(England/Wales) Urban major conurbation,E10000016,560297.0,173251.0,Dartford 002,Dartford 002H,,,,,Outstanding,South-East England and South London,United Kingdom,10023446223.0,,Not applicable,Not applicable,,,E02005029,E01035280,60.0, +142591,886,Kent,3915,Manor Community Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2016,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,674.0,No Special Classes,19-01-2023,670.0,334.0,336.0,31.2,Supported by a multi-academy trust,CYGNUS ACADEMIES TRUST,Linked to a sponsor,Cygnus Academies Trust,Not applicable,,10055399.0,,Not applicable,31-10-2018,29-05-2024,Keary Road,,,Swanscombe,Kent,DA10 0BU,www.manor.kent.sch.uk/,1322383314.0,Mrs,Natalie,Hill,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Swanscombe,Dartford,(England/Wales) Urban major conurbation,E10000016,560606.0,173953.0,Dartford 004,Dartford 004B,,,,,Good,South-East England and South London,,10023438316.0,,Not applicable,Not applicable,,,E02005031,E01024176,199.0, +142613,886,Kent,2077,Westgate Primary School,Academy sponsor led,Academies,Open,New Provision,01-04-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,210.0,101.0,109.0,21.0,Supported by a multi-academy trust,CYGNUS ACADEMIES TRUST,Linked to a sponsor,Cygnus Academies Trust,Not applicable,,10055488.0,,,06-03-2019,02-06-2024,Summerhill Road,,,Dartford,Kent,DA1 2LP,https://www.westgateprimary.org/,1322223382.0,Mrs,Laura,Crosley,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Town,Dartford,(England/Wales) Urban major conurbation,E10000016,553773.0,173889.0,Dartford 003,Dartford 003C,,,,,Good,South-East England and South London,,100062616136.0,,Not applicable,Not applicable,,,E02005030,E01024182,44.0, +142689,886,Kent,3306,Brenchley and Matfield Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-05-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,196.0,93.0,103.0,9.7,Supported by a multi-academy trust,THE TENAX SCHOOLS TRUST,Linked to a sponsor,The Tenax Schools Trust,Not applicable,,10056161.0,,Not applicable,15-11-2018,13-09-2023,Market Heath,Brenchley,,Tonbridge,Kent,TN12 7NY,www.bmprimary.org.uk,1892722929.0,Miss,Jane,Mallon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Brenchley and Horsmonden,Tunbridge Wells,(England/Wales) Rural village,E10000016,567404.0,141906.0,Tunbridge Wells 004,Tunbridge Wells 004A,,,,,Good,South-East England and South London,,100062546036.0,,Not applicable,Not applicable,,,E02005165,E01024793,19.0, +142814,886,Kent,2078,St Nicholas Church of England Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-06-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,,Diocese of Canterbury,Not applicable,378.0,Not applicable,19-01-2023,396.0,212.0,184.0,34.8,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10056538.0,,,23-05-2019,08-05-2024,Fairfield Road,,,New Romney,Kent,TN28 8BP,www.st-nicholas-newromney.kent.sch.uk,1797361906.0,Mr,Christopher,Dale,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,2.0,14.0,,,South East,Folkestone and Hythe,New Romney,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,606585.0,125146.0,Folkestone and Hythe 012,Folkestone and Hythe 012C,,,,,Good,South-East England and South London,,50002925.0,,Not applicable,Not applicable,,,E02005113,E01024539,138.0, +142834,886,Kent,2079,Woodlands Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2016,,,Primary,4.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,,Not applicable,630.0,Not applicable,19-01-2023,643.0,355.0,288.0,20.7,Not applicable,,Not applicable,,Not under a federation,,10058897.0,,,12-06-2019,11-04-2024,Higham School Lane,Hunt Road,,Tonbridge,Kent,TN10 4BB,www.woodlands.kent.sch.uk,1732355577.0,Mrs,Vicki,Lonie,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tonbridge and Malling,Higham,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,560317.0,148629.0,Tonbridge and Malling 011,Tonbridge and Malling 011C,,,,,Good,South-East England and South London,,10013922163.0,,Not applicable,Not applicable,,,E02005159,E01024749,133.0, +142924,886,Kent,2080,Barming Primary School,Academy sponsor led,Academies,Open,New Provision,01-07-2016,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,467.0,244.0,223.0,25.1,Supported by a multi-academy trust,ORCHARD ACADEMY TRUST,Linked to a sponsor,Allington Primary School Academy Trust (Orchard Academy Trust),Not applicable,,10056809.0,,,09-05-2019,30-04-2024,Belmont Close,Barming,,Maidstone,Kent,ME16 9DY,https://www.barming.kent.sch.uk/,1622726472.0,Mr,Christopher,Laker,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Barming and Teston,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,572627.0,154790.0,Maidstone 014,Maidstone 014A,,,,,Good,South-East England and South London,,200003720125.0,,Not applicable,Not applicable,,,E02005081,E01024325,107.0, +143073,886,Kent,2309,Priory Fields School,Academy converter,Academies,Open,Academy Converter,01-08-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,365.0,178.0,187.0,57.3,Supported by a multi-academy trust,WHINLESS DOWN ACADEMY TRUST,-,,Not applicable,,10057450.0,,Not applicable,20-11-2018,08-05-2024,Astor Avenue,,,Dover,Kent,CT17 0FS,http://www.prioryfields.kent.sch.uk,1304211543.0,Miss,Casey,Hall,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Tower Hamlets,Dover,(England/Wales) Urban city and town,E10000016,630848.0,141634.0,Dover 011,Dover 011H,,,,,Good,South-East England and South London,,10034874352.0,,Not applicable,Not applicable,,,E02005051,E01024248,209.0, +143075,886,Kent,2313,St Martin's School,Academy converter,Academies,Open,Academy Converter,01-08-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,194.0,102.0,92.0,17.0,Supported by a multi-academy trust,WHINLESS DOWN ACADEMY TRUST,-,,Not applicable,,10057448.0,,Not applicable,08-02-2024,20-05-2024,Markland Road,,,Dover,Kent,CT17 9LY,www.stmartins.kent.sch.uk/,1304206620.0,Mrs,Helen,Thompson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Maxton & Elms Vale,Dover,(England/Wales) Urban city and town,E10000016,630181.0,141199.0,Dover 014,Dover 014C,,,,,Good,South-East England and South London,,100062290538.0,,Not applicable,Not applicable,,,E02005054,E01024213,33.0, +143218,886,Kent,3914,Oakfield Primary Academy,Academy converter,Academies,Open,Academy Converter,01-09-2016,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Has Special Classes,19-01-2023,719.0,383.0,336.0,27.5,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10057852.0,,Not applicable,06-10-2021,03-06-2024,Oakfield Lane,,,Dartford,Kent,DA1 2SW,http://www.oakfield-dartford.co.uk,1322220831.0,Mrs,Rajinder,Kaur-Gill,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,14.0,12.0,,,South East,Dartford,"Wilmington, Sutton-at-Hone & Hawley",Dartford,(England/Wales) Urban major conurbation,E10000016,553836.0,172851.0,Dartford 009,Dartford 009D,,,,,Good,South-East England and South London,,200000534405.0,,Not applicable,Not applicable,,,E02005036,E01024168,192.0, +143219,886,Kent,2657,Temple Hill Primary Academy,Academy converter,Academies,Open,Academy Converter,01-09-2016,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,914.0,Has Special Classes,19-01-2023,891.0,458.0,433.0,38.5,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10057853.0,,Not applicable,26-06-2019,03-06-2024,St Edmund's Road,Temple Hill,,Dartford,Kent,DA1 5ND,http://www.temple-hill.kent.sch.uk,1322224600.0,Mr,Leon,Dawson,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,5.0,8.0,3.0,12.0,South East,Dartford,Temple Hill,Dartford,(England/Wales) Urban major conurbation,E10000016,555040.0,175006.0,Dartford 001,Dartford 001I,,,,,Good,South-East England and South London,,200000530828.0,,Not applicable,Not applicable,,,E02005028,E01035274,329.0, +143220,886,Kent,2523,Upton Junior School,Academy converter,Academies,Open,Academy Converter,01-09-2016,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,512.0,No Special Classes,19-01-2023,513.0,263.0,250.0,22.2,Supported by a multi-academy trust,VIKING ACADEMY TRUST,-,,Not applicable,,10057856.0,,Not applicable,,05-06-2024,"Upton Junior School, Edge End Road",,,Broadstairs,Kent,CT10 2AH,http://www.uptonjunior.com/,1843861393.0,Miss,Darci,Arthur,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Viking,South Thanet,(England/Wales) Urban city and town,E10000016,638779.0,167831.0,Thanet 010,Thanet 010C,,,,,,South-East England and South London,,100062627193.0,,Not applicable,Not applicable,,,E02005141,E01024706,114.0, +143517,886,Kent,2081,Brenzett Church of England Primary School,Academy sponsor led,Academies,Open,New Provision,01-10-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,,Diocese of Canterbury,Not applicable,140.0,Not applicable,19-01-2023,83.0,40.0,43.0,38.6,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10061368.0,,,03-07-2019,18-04-2024,Straight Lane,,Brenzett,Romney Marsh,Kent,TN29 9UA,www.brenzett.kent.sch.uk,1797344335.0,Mrs,Rowan,Wright,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Walland & Denge Marsh,Folkestone and Hythe,(England/Wales) Rural hamlet and isolated dwellings,E10000016,600427.0,127014.0,Folkestone and Hythe 011,Folkestone and Hythe 011D,,,,,Good,South-East England and South London,,50100964.0,,Not applicable,Not applicable,,,E02005112,E01024548,32.0, +143605,886,Kent,5220,Halfway Houses Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,591.0,302.0,289.0,28.4,Supported by a multi-academy trust,THE ISLAND LEARNING TRUST,Linked to a sponsor,The Island Learning Trust,Not applicable,,10061768.0,,Not applicable,13-11-2018,18-04-2024,Danley Road,Minster-on-Sea,,Sheerness,Kent,ME12 3AP,www.halfwayhouses.kent.sch.uk,1795662875.0,Mrs,Lindsay,Fordyce,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Queenborough and Halfway,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,593286.0,173082.0,Swale 004,Swale 004B,,,,,Good,South-East England and South London,,10093084941.0,,Not applicable,Not applicable,,,E02005118,E01024598,168.0, +143606,886,Kent,2235,Minster in Sheppey Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,600.0,No Special Classes,19-01-2023,579.0,285.0,294.0,25.9,Supported by a multi-academy trust,THE ISLAND LEARNING TRUST,Linked to a sponsor,The Island Learning Trust,Not applicable,,10061767.0,,Not applicable,10-03-2022,25-04-2024,Brecon Chase,Minster,,Sheerness,Kent,ME12 2HX,http://www.minster-sheppey.kent.sch.uk,1795872138.0,Mrs,Michelle Jeffery co-head,Lynne Lewis co-head,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Minster Cliffs,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,595361.0,173030.0,Swale 003,Swale 003D,,,,,Good,South-East England and South London,,200002530145.0,,Not applicable,Not applicable,,,E02005117,E01024589,150.0, +143787,886,Kent,2290,Tenterden Infant School,Academy converter,Academies,Open,Academy Converter,01-12-2016,,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,211.0,No Special Classes,19-01-2023,151.0,78.0,73.0,21.2,Supported by a multi-academy trust,TENTERDEN SCHOOLS TRUST,-,,Not applicable,,10061964.0,,Not applicable,05-02-2019,13-05-2024,Recreation Ground Road,,,Tenterden,Kent,TN30 6RA,www.tenterdenprimaryfederation.kent.sch.uk/,1580762086.0,Mrs,Tina,McIntosh,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Tenterden South,Ashford,(England/Wales) Rural town and fringe,E10000016,588615.0,133196.0,Ashford 013,Ashford 013F,,,,,Good,South-East England and South London,,100062567470.0,,Not applicable,Not applicable,,,E02005008,E01024025,32.0, +143788,886,Kent,3143,St Michael's Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-12-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,160.0,93.0,67.0,25.6,Supported by a multi-academy trust,TENTERDEN SCHOOLS TRUST,-,,Not applicable,,10061960.0,,Not applicable,12-12-2018,13-05-2024,Ashford Road,St Michael's,,Tenterden,Kent,TN30 6PU,www.stmcep.school,1580763210.0,Mrs,"Sara Williamson,",Mrs Jo Paskhin,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Tenterden St Michael's,Ashford,(England/Wales) Rural town and fringe,E10000016,588469.0,135486.0,Ashford 013,Ashford 013C,,,,,Good,South-East England and South London,,100062567391.0,,Not applicable,Not applicable,,,E02005008,E01024011,41.0, +143789,886,Kent,3144,Tenterden Church of England Junior School,Academy converter,Academies,Open,Academy Converter,01-12-2016,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Diocese of Canterbury,Not applicable,240.0,No Special Classes,19-01-2023,194.0,91.0,103.0,28.9,Supported by a multi-academy trust,TENTERDEN SCHOOLS TRUST,-,,Not applicable,,10061969.0,,Not applicable,11-12-2018,13-05-2024,Recreation Ground Road,,,Tenterden,Kent,TN30 6RA,www.tenterdenprimaryfederation.kent.sch.uk/,1580763717.0,Mrs,Tina,McIntosh,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Tenterden South,Ashford,(England/Wales) Rural town and fringe,E10000016,588577.0,133233.0,Ashford 013,Ashford 013F,,,,,Good,South-East England and South London,,200004397348.0,,Not applicable,Not applicable,,,E02005008,E01024025,56.0, +143954,886,Kent,4015,The Lenham School,Academy sponsor led,Academies,Open,New Provision,01-03-2017,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Does not apply,,Not applicable,Non-selective,1012.0,Not applicable,19-01-2023,779.0,392.0,387.0,23.0,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10062504.0,,,06-11-2019,07-05-2024,Ham Lane,Lenham,,Maidstone,Kent,ME17 2LL,www.thelenham.viat.org.uk,1622858267.0,Mr,Robbie,Ferguson,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Harrietsham and Lenham,Faversham and Mid Kent,(England/Wales) Rural town and fringe,E10000016,589367.0,152380.0,Maidstone 011,Maidstone 011B,,,,,Good,South-East England and South London,,200003719333.0,,Not applicable,Not applicable,,,E02005078,E01024362,172.0, +143987,886,Kent,3324,"Leybourne, St Peter and St Paul Church of England Primary Academy",Academy converter,Academies,Open,Academy Converter,01-03-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,216.0,No Special Classes,19-01-2023,216.0,103.0,113.0,6.5,Supported by a multi-academy trust,THE TENAX SCHOOLS TRUST,Linked to a sponsor,The Tenax Schools Trust,Not applicable,,10062539.0,,Not applicable,03-11-2021,09-05-2024,Rectory Lane North,Leybourne,,West Malling,Kent,ME19 5HD,http://www.leybourne.school,1732842008.0,,Tina,Holditch,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tonbridge and Malling,"Birling, Leybourne & Ryarsh",Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,569081.0,158746.0,Tonbridge and Malling 003,Tonbridge and Malling 003G,,,,,Good,South-East England and South London,,10002908134.0,,Not applicable,Not applicable,,,E02005151,E01024782,14.0, +144005,886,Kent,2658,Westcourt Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2017,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,240.0,No Special Classes,19-01-2023,358.0,177.0,181.0,43.2,Supported by a multi-academy trust,THE PRIMARY FIRST TRUST,Linked to a sponsor,The Primary First Trust,Not applicable,,10062555.0,,Not applicable,27-11-2019,07-03-2024,Silver Road,,,Gravesend,Kent,DA12 4JG,www.westcourt.kent.sch.uk/,1474566411.0,Miss,Mags,Sexton,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Westcourt,Gravesham,(England/Wales) Urban major conurbation,E10000016,566426.0,172839.0,Gravesham 007,Gravesham 007C,,,,,Good,South-East England and South London,,100062312611.0,,Not applicable,Not applicable,,,E02005061,E01024311,145.0, +144015,886,Kent,4016,The Charles Dickens School,Academy sponsor led,Academies,Open,New Provision,01-03-2017,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,,Not applicable,Not applicable,1104.0,Not applicable,19-01-2023,1111.0,540.0,571.0,36.0,Supported by a multi-academy trust,BARTON COURT ACADEMY TRUST,Linked to a sponsor,Barton Court Academy Trust,Not applicable,,10062895.0,,,29-03-2023,23-04-2024,Broadstairs Road,,,Broadstairs,Kent,CT10 2RL,www.cds.kent.sch.uk,1843862988.0,Mr,Warren,Smith,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,VI - Visual Impairment,,,,,,,,,,,,,Resourced provision,3.0,5.0,,,South East,Thanet,St Peters,South Thanet,(England/Wales) Urban city and town,E10000016,638334.0,167981.0,Thanet 011,Thanet 011D,,,,,Good,South-East England and South London,,200003079311.0,,Not applicable,Not applicable,,,E02005142,E01024690,400.0, +144098,886,Kent,3021,Stone St Mary's CofE Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,630.0,No Special Classes,19-01-2023,645.0,327.0,318.0,18.8,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10063054.0,,Not applicable,05-02-2020,14-09-2023,Hayes Road,Horns Cross,,Greenhithe,Kent,DA9 9EF,http://www.stone.kent.sch.uk,1322382292.0,Mrs,Jane,Rolfe,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Stone Castle,Dartford,(England/Wales) Urban major conurbation,E10000016,557252.0,173910.0,Dartford 006,Dartford 006C,,,,,Good,South-East England and South London,,200000536262.0,,Not applicable,Not applicable,,,E02005033,E01024171,121.0, +144099,886,Kent,5215,Horton Kirby Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2017,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,240.0,No Special Classes,19-01-2023,236.0,114.0,122.0,20.8,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10063053.0,,Not applicable,17-05-2023,23-04-2024,Horton Road,Horton Kirby,,Dartford,Kent,DA4 9BN,www.hortonkirby.kent.sch.uk,1322863278.0,Mr,Glenn,Pollard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Farningham, Horton Kirby and South Darenth",Sevenoaks,(England/Wales) Rural town and fringe,E10000016,556340.0,168608.0,Sevenoaks 005,Sevenoaks 005B,,,,,Good,South-East England and South London,,100062317327.0,,Not applicable,Not applicable,,,E02005091,E01024432,49.0, +144100,886,Kent,5411,Dartford Grammar School for Girls,Academy converter,Academies,Open,Academy Converter,01-06-2017,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1119.0,No Special Classes,19-01-2023,1248.0,41.0,1207.0,18.0,Supported by a multi-academy trust,THE ARETÉ TRUST,-,,Not applicable,,10063935.0,,Not applicable,20-10-2021,04-06-2024,Shepherds Lane,,,Dartford,Kent,DA1 2NT,http://www.dartfordgrammargirls.org.uk,1322223123.0,Mrs,Sharon,Pritchard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,West Hill,Dartford,(England/Wales) Urban major conurbation,E10000016,553265.0,173905.0,Dartford 003,Dartford 003F,,,,,Outstanding,South-East England and South London,,200000532843.0,,Not applicable,Not applicable,,,E02005030,E01024185,160.0, +144354,886,Kent,4091,The Whitstable School,Academy converter,Academies,Open,Academy Converter,01-09-2018,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Does not apply,,Not applicable,Non-selective,1220.0,Not applicable,19-01-2023,1080.0,514.0,566.0,19.9,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10068468.0,,,13-03-2024,22-05-2024,Bellevue Road,,,Whitstable,Kent,CT5 1PX,www.thewhitstableschool.org.uk,1227931300.0,Mr,Alex,Holmes,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Gorrell,Canterbury,(England/Wales) Urban city and town,E10000016,611643.0,165739.0,Canterbury 009,Canterbury 009A,,,,,Good,South-East England and South London,,100062300161.0,,Not applicable,Not applicable,,,E02005018,E01024062,197.0, +144420,886,Kent,3716,St Teresa's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-05-2017,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,218.0,No Special Classes,19-01-2023,213.0,100.0,113.0,14.6,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10063828.0,,Not applicable,16-01-2020,20-05-2024,Quantock Drive,,,Ashford,Kent,TN24 8QN,www.st-teresas.kent.sch.uk/,1233622797.0,Mrs,H,Bennett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Furley,Ashford,(England/Wales) Urban city and town,E10000016,600520.0,143538.0,Ashford 015,Ashford 015D,,,,,Good,South-East England and South London,,100062560866.0,,Not applicable,Not applicable,,,E02007046,E01024022,31.0, +144531,886,Kent,2172,Valley Invicta Primary School At East Borough,Academy converter,Academies,Open,Academy Converter,01-11-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,455.0,No Special Classes,19-01-2023,471.0,250.0,221.0,20.4,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10065310.0,,Not applicable,14-10-2021,04-06-2024,Vinters Road,,,Maidstone,Kent,ME14 5DX,www.eastborough.viat.org.uk,1622754633.0,Mrs,C,Bacon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,9.0,10.0,,,South East,Maidstone,East,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576832.0,155960.0,Maidstone 005,Maidstone 005E,,,,,Good,South-East England and South London,,200003689415.0,,Not applicable,Not applicable,,,E02005072,E01024351,96.0, +144615,886,Kent,2085,Royal Rise Primary School,Academy sponsor led,Academies,Open,New Provision,01-07-2017,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,203.0,94.0,109.0,34.5,Supported by a multi-academy trust,CYGNUS ACADEMIES TRUST,Linked to a sponsor,Cygnus Academies Trust,Not applicable,,10064021.0,,,15-09-2021,29-04-2024,Royal Rise,,,Tonbridge,Kent,TN9 2DQ,,1732354143.0,Mrs,Sarah,Griggs,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Vauxhall,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559446.0,145748.0,Tonbridge and Malling 012,Tonbridge and Malling 012H,,,,,Good,South-East England and South London,,200000966929.0,,Not applicable,Not applicable,,,E02005160,E01035009,70.0, +144634,886,Kent,2086,Bishop Chavasse Primary School,Free schools,Free Schools,Open,,01-09-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Church of England,,Non-selective,420.0,Not applicable,19-01-2023,311.0,162.0,149.0,22.8,Supported by a multi-academy trust,THE TENAX SCHOOLS TRUST,Linked to a sponsor,The Tenax Schools Trust,Not applicable,,10064781.0,,,06-07-2022,03-04-2024,2a Baker Lane,,,Tonbridge,Kent,TN11 0FB,www.bishopchavasseschool.org.uk,1732676040.0,Mrs,Becks,Hood,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Tonbridge and Malling,Vauxhall,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,560184.0,145430.0,Tonbridge and Malling 012,Tonbridge and Malling 012E,,,,,Good,South-East England and South London,United Kingdom,10092972630.0,,Not applicable,Not applicable,,,E02005160,E01024767,71.0, +144668,886,Kent,2676,West Hill Primary Academy,Academy converter,Academies,Open,Academy Converter,01-09-2017,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,507.0,No Special Classes,19-01-2023,534.0,271.0,263.0,12.5,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10064807.0,,Not applicable,01-10-2021,03-06-2024,Dartford Road,,,Dartford,Kent,DA1 3DZ,www.west-hill.kent.sch.uk/,1322226019.0,Ms,Katy,Ward,Executive Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,West Hill,Dartford,(England/Wales) Urban major conurbation,E10000016,553136.0,174316.0,Dartford 003,Dartford 003D,,,,,Good,South-East England and South London,,100062309268.0,,Not applicable,Not applicable,,,E02005030,E01024183,67.0, +144716,886,Kent,4019,School of Science and Technology Maidstone,Free schools,Free Schools,Open,New Provision,01-09-2020,,,Secondary,11.0,19,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,,,1200.0,Not applicable,19-01-2023,576.0,385.0,191.0,9.9,Supported by a multi-academy trust,VALLEY INVICTA ACADEMIES TRUST,Linked to a sponsor,Valley Invicta Academies Trust,Not applicable,,10086463.0,,,25-01-2023,20-12-2023,New Cut Road,,,Maidstone,Kent,ME14 5GQ,,1622938444.0,Mr,Ryan,Royston (Head of School),,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Boxley,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,577636.0,155754.0,Maidstone 005,Maidstone 005B,,,,,Outstanding,South-East England and South London,United Kingdom,10094441687.0,,Not applicable,Not applicable,,,E02005072,E01024336,57.0, +144835,886,Kent,3343,Charing Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-07-2017,,,Primary,2.0,11,Not applicable,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,140.0,No Special Classes,19-01-2023,165.0,87.0,78.0,35.1,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10064466.0,,Not applicable,20-10-2021,19-02-2024,School Road,Charing,,Ashford,Kent,TN27 0JN,www.charingschool.org.uk/,1233712277.0,Mr,Thomas,Bird,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Charing,Ashford,(England/Wales) Rural town and fringe,E10000016,595188.0,149495.0,Ashford 002,Ashford 002B,,,,,Good,South-East England and South London,,100062563576.0,,Not applicable,Not applicable,,,E02004997,E01023986,52.0, +144836,886,Kent,3754,St Augustine's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-07-2017,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,285.0,No Special Classes,19-01-2023,287.0,154.0,133.0,17.1,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10064464.0,,Not applicable,15-09-2021,03-06-2024,Wilman Road,,,Tunbridge Wells,Kent,TN4 9AL,https://www.st-augustines.kent.sch.uk/,1892529796.0,Mr,Jon,Crozier,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,St John's,Tunbridge Wells,(England/Wales) Urban city and town,E10000016,558501.0,141283.0,Tunbridge Wells 002,Tunbridge Wells 002A,,,,,Good,South-East England and South London,,100062586226.0,,Not applicable,Not applicable,,,E02005163,E01024837,49.0, +144867,886,Kent,3329,Borden Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-08-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,126.0,No Special Classes,19-01-2023,123.0,64.0,59.0,14.6,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10064657.0,,Not applicable,29-06-2022,28-05-2024,School Lane,Borden,,Sittingbourne,Kent,ME9 8JS,www.borden.kent.sch.uk,1795472593.0,Miss,Georgina,Ingram,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Borden and Grove Park,Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,587791.0,163390.0,Swale 009,Swale 009A,,,,,Requires improvement,South-East England and South London,,100062626970.0,,Not applicable,Not applicable,,,E02005123,E01024554,18.0, +144868,886,Kent,3330,Bredgar Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-08-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,106.0,No Special Classes,19-01-2023,108.0,52.0,56.0,10.2,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10064679.0,,Not applicable,12-01-2022,28-05-2024,Bexon Lane,Bredgar,,Sittingbourne,Kent,ME9 8HB,www.bredgar.kent.sch.uk/,1622884359.0,Miss,Joanna,Heath,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,West Downs,Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,588053.0,160269.0,Swale 013,Swale 013C,,,,,Good,South-East England and South London,,100062626605.0,,Not applicable,Not applicable,,,E02005127,E01024628,11.0, +144869,886,Kent,2463,Minterne Junior School,Academy converter,Academies,Open,Academy Converter,01-08-2017,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,Has Special Classes,19-01-2023,374.0,182.0,192.0,16.0,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10064678.0,,Not applicable,06-10-2021,23-04-2024,Minterne Avenue,,,Sittingbourne,Kent,ME10 1SB,http://www.minterne.org,1795472323.0,Ms,Kirsty,Warner,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,,,,,South East,Swale,Homewood,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,589818.0,162822.0,Swale 012,Swale 012D,,,,,Good,South-East England and South London,,200002527646.0,,Not applicable,Not applicable,,,E02005126,E01024630,60.0, +144870,886,Kent,2513,The Oaks Infant School,Academy converter,Academies,Open,Academy Converter,01-08-2017,,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,Has Special Classes,19-01-2023,324.0,165.0,159.0,16.8,Supported by a multi-academy trust,OUR COMMUNITY MULTI ACADEMY TRUST,-,,Not applicable,,10064677.0,,Not applicable,24-11-2021,23-04-2024,Gore Court Road,,,Sittingbourne,Kent,ME10 1GL,http://www.theoaksinfantschool.co.uk,1795423619.0,Mrs,Jenny,Wynn,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,4.0,12.0,,,South East,Swale,Woodstock,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,589925.0,162789.0,Swale 012,Swale 012D,,,,,Good,South-East England and South London,,200002532277.0,,Not applicable,Not applicable,,,E02005126,E01024630,47.0, +144910,886,Kent,5204,Sutton-At-Hone Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,420.0,No Special Classes,19-01-2023,400.0,198.0,202.0,18.0,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10065030.0,,Not applicable,04-03-2020,26-03-2024,Church Road,Sutton-At-Hone,,Dartford,Kent,DA4 9EX,www.sutton-at-hone.kent.sch.uk/,1322862147.0,Mrs,Karen,Trowell,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,"Wilmington, Sutton-at-Hone & Hawley",Dartford,(England/Wales) Rural town and fringe,E10000016,555501.0,170379.0,Dartford 012,Dartford 012D,,,,,Good,South-East England and South London,,200000535148.0,,Not applicable,Not applicable,,,E02005039,E01024174,72.0, +145012,886,Kent,2087,Morehall Primary School and Nursery,Academy sponsor led,Academies,Open,Fresh Start,01-01-2017,,,Primary,2.0,11,,Has Nursery Classes,Not applicable,Mixed,None,None,,,210.0,Not applicable,19-01-2023,228.0,113.0,115.0,26.0,Supported by a multi-academy trust,TURNER SCHOOLS,Linked to a sponsor,Turner Schools,Not applicable,,10064517.0,,,02-10-2019,15-04-2024,Morehall Primary School and Nursery,Chart Road,,Folkestone,Kent,CT19 4PN,www.turnermorehall.org,1303275128.0,Mrs,Am'e,Moris,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,VI - Visual Impairment,,,,,,,,,,,,,Resourced provision,2.0,3.0,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,620753.0,136837.0,Folkestone and Hythe 006,Folkestone and Hythe 006C,,,,,Good,South-East England and South London,,50027301.0,,Not applicable,Not applicable,,,E02005107,E01024513,57.0, +145013,886,Kent,2090,Richmond Primary School,Academy sponsor led,Academies,Open,Fresh Start,01-01-2017,,,Primary,2.0,11,,Has Nursery Classes,Not applicable,Mixed,None,None,,,420.0,Not applicable,19-01-2023,325.0,156.0,169.0,64.2,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10064518.0,,,09-11-2022,07-06-2024,Unity Street,,,Sheerness,Kent,ME12 2ET,,1795662891.0,,Lesley,Conway,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheerness,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,592928.0,174502.0,Swale 001,Swale 001D,,,,,Good,South-East England and South London,,100062378139.0,,Not applicable,Not applicable,,,E02005115,E01024612,188.0, +145014,886,Kent,2092,Knockhall Primary School,Academy sponsor led,Academies,Open,Fresh Start,01-01-2017,,,Primary,3.0,11,Not applicable,Has Nursery Classes,Not applicable,Mixed,None,None,,Not applicable,682.0,Not applicable,19-01-2023,385.0,215.0,170.0,33.7,Supported by a multi-academy trust,THE WOODLAND ACADEMY TRUST,Linked to a sponsor,The Woodland Academy Trust,Not applicable,,10064519.0,,,21-06-2023,10-05-2024,Eynsford Road,,,Greenhithe,Kent,DA9 9RF,www.knockhallprimaryschool.co.uk,1322382053.0,Miss,Kathryn,Yiannadji,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Greenhithe & Knockhall,Dartford,(England/Wales) Urban major conurbation,E10000016,559140.0,174692.0,Dartford 004,Dartford 004F,,,,,Requires improvement,South-East England and South London,,200000537409.0,,Not applicable,Not applicable,,,E02005031,E01035283,126.0, +145081,886,Kent,3059,"St Mark's Church of England Primary School, Eccles",Academy converter,Academies,Open,Academy Converter,01-11-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,140.0,No Special Classes,19-01-2023,128.0,61.0,67.0,15.6,Supported by a multi-academy trust,THE PILGRIM MULTI ACADEMY TRUST,-,,Not applicable,,10065389.0,,Not applicable,22-03-2022,17-04-2024,Eccles Row,Eccles,,Aylesford,Kent,ME20 7HS,www.st-marks-aylesford.kent.sch.uk,1622717337.0,Mr,Jonathan,Bassett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford North & North Downs,Chatham and Aylesford,(England/Wales) Rural town and fringe,E10000016,572898.0,160831.0,Tonbridge and Malling 001,Tonbridge and Malling 001F,,,,,Good,South-East England and South London,,200000961684.0,,Not applicable,Not applicable,,,E02005149,E01024728,20.0, +145115,886,Kent,2093,Chilmington Green Primary School,Free schools,Free Schools,Open,,03-09-2018,,,Primary,2.0,11,,Has Nursery Classes,Not applicable,Mixed,None,None,,,460.0,Not applicable,19-01-2023,205.0,110.0,95.0,21.5,Supported by a multi-academy trust,THE STOUR ACADEMY TRUST,Linked to a sponsor,The Stour Academy Trust,Not applicable,,10068096.0,,,07-12-2022,29-05-2024,Mock Lane,,,Ashford,Kent,TN23 3DS,www.chilmingtongreen.kent.sch.uk,1233228241.0,Miss,Tamsin,Mobbs,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,Not Applicable,,,,,,,,,,,,,Resourced provision,,15.0,,,South East,Ashford,Weald Central,Ashford,(England/Wales) Rural hamlet and isolated dwellings,E10000016,600157.0,141878.0,Ashford 012,Ashford 012F,,,,,Good,South-East England and South London,United Kingdom,10012877144.0,,Not applicable,Not applicable,,,E02005007,E01032814,42.0, +145117,886,Kent,2096,Riverview Junior School,Academy sponsor led,Academies,Open,New Provision,01-10-2017,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,480.0,Not applicable,19-01-2023,504.0,257.0,247.0,24.6,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10065188.0,,,09-02-2022,22-05-2024,Cimba Wood,,,Gravesend,Kent,DA12 4SD,https://www.riverview-junior.co.uk/,1474352620.0,Mr,Aaron,Jones,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,SpLD - Specific Learning Difficulty,"SLCN - Speech, language and Communication",ASD - Autistic Spectrum Disorder,"SEMH - Social, Emotional and Mental Health",MLD - Moderate Learning Difficulty,,,,,,,,,Resourced provision,,,,,South East,Gravesham,Riverview Park,Gravesham,(England/Wales) Urban major conurbation,E10000016,566276.0,171553.0,Gravesham 008,Gravesham 008B,,,,,Good,South-East England and South London,,10012012179.0,,Not applicable,Not applicable,,,E02005062,E01024298,124.0, +145355,886,Kent,2531,Vale View Community School,Academy converter,Academies,Open,Academy Converter,01-01-2018,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,229.0,112.0,117.0,50.7,Supported by a multi-academy trust,WHINLESS DOWN ACADEMY TRUST,-,,Not applicable,,10066413.0,,Not applicable,27-04-2022,27-03-2024,Vale View Road,Elms Vale,,Dover,Kent,CT17 9NP,www.vale-view.kent.sch.uk,1304202821.0,Mrs,Lisa,Sprigmore,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Town & Castle,Dover,(England/Wales) Urban city and town,E10000016,630652.0,141278.0,Dover 013,Dover 013C,,,,,Good,South-East England and South London,,100062290539.0,,Not applicable,Not applicable,,,E02005053,E01024216,116.0, +145420,886,Kent,4020,Folkestone Academy,Academy sponsor led,Academies,Open,Fresh Start,01-12-2017,,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,None,None,,Non-selective,2170.0,Not applicable,19-01-2023,1080.0,538.0,542.0,45.8,Supported by a multi-academy trust,TURNER SCHOOLS,Linked to a sponsor,Turner Schools,Not applicable,,10066337.0,,,21-04-2022,14-05-2024,Academy Lane,,,Folkestone,Kent,CT19 5FP,www.folkestoneacademy.com,1303842400.0,Mr,Steven,Shaw,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Broadmead,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,621910.0,137354.0,Folkestone and Hythe 006,Folkestone and Hythe 006F,,,,,Good,South-East England and South London,United Kingdom,50120541.0,,Not applicable,Not applicable,,,E02005107,E01024516,426.0, +145815,886,Kent,2666,Wrotham Road Primary School,Academy converter,Academies,Open,Academy Converter,01-06-2018,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,446.0,235.0,211.0,29.5,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10067738.0,,Not applicable,06-10-2022,18-04-2024,Wrotham Road,,,Gravesend,Kent,DA11 0QF,http://www.wrotham-road.kent.sch.uk,1474534540.0,Ms,Nicole,Galinis,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Pelham,Gravesham,(England/Wales) Urban major conurbation,E10000016,564628.0,173560.0,Gravesham 002,Gravesham 002D,,,,,Good,South-East England and South London,,100062310321.0,,Not applicable,Not applicable,,,E02005056,E01024291,121.0, +145923,886,Kent,4021,Turner Free School,Free schools,Free Schools,Open,,01-09-2018,,,Secondary,11.0,18,,Not applicable,Has a sixth form,Mixed,None,None,,Non-selective,1260.0,No Special Classes,19-01-2023,826.0,430.0,396.0,35.6,Supported by a multi-academy trust,TURNER SCHOOLS,Linked to a sponsor,Turner Schools,Not applicable,,10068105.0,,,07-12-2022,21-05-2024,Tile Kiln Lane,Cheriton,,Folkestone,,CT19 4PB,www.turnerfreeschool.org,1303842400.0,Ms,Jennifer,van Deelen,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,X999999,620593.0,137066.0,Folkestone and Hythe 006,Folkestone and Hythe 006C,,,,,Good,South-East England and South London,,50114755.0,,Not applicable,Not applicable,,,E02005107,E01024513,294.0, +145951,886,Kent,2098,Pilgrims' Way Primary School,Academy sponsor led,Academies,Open,Fresh Start,01-05-2018,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,,,420.0,Not applicable,19-01-2023,348.0,184.0,164.0,52.9,Supported by a multi-academy trust,VERITAS MULTI ACADEMY TRUST,Linked to a sponsor,Veritas Multi Academy Trust,Not applicable,,10067579.0,,,22-09-2022,14-05-2024,Pilgrims Way,,,Canterbury,,CT1 1XU,https://www.pilgrims-way.kent.sch.uk,1227760084.0,Mrs,Emma,Campbell,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,616215.0,157035.0,Canterbury 016,Canterbury 016B,,,,,Good,South-East England and South London,United Kingdom,200000683218.0,,Not applicable,Not applicable,,,E02005025,E01024045,171.0, +146081,886,Kent,2099,Edenbridge Primary School,Academy sponsor led,Academies,Open,New Provision,01-07-2018,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,450.0,No Special Classes,19-01-2023,407.0,206.0,201.0,38.2,Supported by a multi-academy trust,THE PIONEER ACADEMY,Linked to a sponsor,The Pioneer Academy,Not applicable,,10068058.0,,,12-10-2022,23-04-2024,High Street,,,Edenbridge,Kent,TN8 5AB,https://edenbridge.kent.sch.uk/kent/primary/edenbridge,1732863787.0,,Mary,Gates,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Edenbridge North and East,Tonbridge and Malling,(England/Wales) Rural town and fringe,E10000016,544347.0,146395.0,Sevenoaks 014,Sevenoaks 014A,,,,,Good,South-East England and South London,United Kingdom,100061001421.0,,Not applicable,Not applicable,,,E02005100,E01024425,153.0, +146114,886,Kent,2677,Coxheath Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2018,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,434.0,212.0,222.0,21.7,Supported by a multi-academy trust,COPPICE PRIMARY PARTNERSHIP,Linked to a sponsor,The Coppice Primary Partnership,Not applicable,,10068466.0,,Not applicable,08-02-2023,14-09-2023,Stockett Lane,Coxheath,,Maidstone,Kent,ME17 4PS,www.coxheath.kent.sch.uk/,1622745553.0,Mr,Giacomo,Mazza,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Coxheath and Hunton,Maidstone and The Weald,(England/Wales) Rural town and fringe,E10000016,574377.0,151268.0,Maidstone 016,Maidstone 016A,,,,,Good,South-East England and South London,,200003715377.0,,Not applicable,Not applicable,,,E02005083,E01024342,94.0, +146143,886,Kent,2044,Loose Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2018,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,,Not applicable,630.0,Not applicable,19-01-2023,656.0,343.0,313.0,8.1,Supported by a multi-academy trust,COPPICE PRIMARY PARTNERSHIP,Linked to a sponsor,The Coppice Primary Partnership,Not applicable,,10068467.0,,Not applicable,28-06-2023,07-05-2024,Loose Road,,,Maidstone,Kent,ME15 9UW,www.loose-primary.kent.sch.uk/,1622743549.0,Mr,Trevor,North,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Loose,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,576115.0,152490.0,Maidstone 016,Maidstone 016D,,,,,Good,South-East England and South London,,200003676025.0,,Not applicable,Not applicable,,,E02005083,E01024376,53.0, +146376,886,Kent,2107,Rosherville Church of England Academy,Academy sponsor led,Academies,Open,Fresh Start,01-09-2018,,,Primary,4.0,11,,No Nursery Classes,Not applicable,Mixed,Church of England,None,,,140.0,Not applicable,19-01-2023,141.0,76.0,65.0,41.8,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10081064.0,,,28-09-2022,26-04-2024,London Road,,,Northfleet,Kent,DA11 9JQ,www.rosherville.co.uk,1474365266.0,Mr,Marc,Dockrell,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Northfleet & Springhead,Gravesham,(England/Wales) Urban major conurbation,E10000016,562934.0,174025.0,Gravesham 001,Gravesham 001B,,,,,Good,South-East England and South London,United Kingdom,100062064703.0,,Not applicable,Not applicable,,,E02005055,E01024277,59.0, +146400,886,Kent,3313,Fordcombe Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2018,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,70.0,38.0,32.0,27.1,Supported by a multi-academy trust,THE TENAX SCHOOLS TRUST,Linked to a sponsor,The Tenax Schools Trust,Not applicable,,10081146.0,,Not applicable,18-10-2022,21-05-2024,The Green,Fordcombe,,Tunbridge Wells,Kent,TN3 0RY,http://www.fordcombe.kent.sch.uk,1892740224.0,Mr,Chris,Blackburn,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Penshurst, Fordcombe and Chiddingstone",Tonbridge and Malling,(England/Wales) Rural village,E10000016,552622.0,140271.0,Sevenoaks 015,Sevenoaks 015D,,,,,Good,South-East England and South London,,10035185120.0,,Not applicable,Not applicable,,,E02005101,E01024456,19.0, +146574,886,Kent,2062,Greenlands Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2019,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,187.0,94.0,93.0,42.8,Supported by a multi-academy trust,CYGNUS ACADEMIES TRUST,Linked to a sponsor,Cygnus Academies Trust,Not applicable,,10082225.0,,Not applicable,18-05-2023,06-06-2024,Green Street Green Road,Darenth,,Dartford,Kent,DA2 8DH,www.greenlandsprimary.org.uk,1474703178.0,Mrs,Alison,Cook,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Darenth,Dartford,(England/Wales) Rural village,E10000016,557498.0,170938.0,Dartford 012,Dartford 012C,,,,,Requires improvement,South-East England and South London,,200000533883.0,,Not applicable,Not applicable,,,E02005039,E01024135,80.0, +146624,886,Kent,4023,Goodwin Academy,Academy sponsor led,Academies,Open,Fresh Start,01-09-2018,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,None,,Non-selective,1150.0,Not applicable,19-01-2023,900.0,435.0,465.0,32.5,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10081522.0,,,19-10-2022,03-06-2024,Hamilton Road,,,Deal,,CT14 9BD,https://www.goodwinacademy.org.uk/,3333602210.0,Mr,Phil,Jones,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,Resourced provision,14.0,13.0,,,South East,Dover,Middle Deal,Dover,(England/Wales) Urban city and town,E10000016,637076.0,151679.0,Dover 007,Dover 007A,,,,,Requires improvement,South-East England and South London,United Kingdom,100062286680.0,,Not applicable,Not applicable,,,E02005047,E01024218,276.0, +146950,886,Kent,5224,All Soul's Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,315.0,No Special Classes,19-01-2023,250.0,124.0,126.0,40.0,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10083010.0,,Not applicable,13-09-2023,23-04-2024,Stanley Road,,,Folkestone,Kent,CT19 4LG,www.allsouls.kent.sch.uk/,1303275967.0,Mrs,Lisa,Ransley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Cheriton,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,620109.0,136784.0,Folkestone and Hythe 005,Folkestone and Hythe 005A,,,,,Good,South-East England and South London,,50028932.0,,Not applicable,Not applicable,,,E02005106,E01024491,100.0, +147053,886,Kent,3353,Deal Parochial Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,199.0,95.0,104.0,36.7,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10083015.0,,Not applicable,28-06-2023,28-03-2024,Gladstone Road,Walmer,,Deal,Kent,CT14 7ER,http://www.deal-parochial.kent.sch.uk,1304374464.0,Ms,Justine,Brown,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Walmer,Dover,(England/Wales) Urban city and town,E10000016,637295.0,151463.0,Dover 004,Dover 004D,,,,,Good,South-East England and South London,,200002882433.0,,Not applicable,Not applicable,,,E02005044,E01024252,73.0, +147054,886,Kent,3911,Hornbeam Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,240.0,Not applicable,19-01-2023,238.0,118.0,120.0,29.8,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10083013.0,,Not applicable,11-10-2023,01-05-2024,Mongeham Road,,,Deal,Kent,CT14 9PQ,www.hornbeam.kent.sch.uk,1304374033.0,Mrs,Rose,Cope,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Mill Hill,Dover,(England/Wales) Urban city and town,E10000016,635697.0,152063.0,Dover 005,Dover 005E,,,,,Good,South-East England and South London,,100062287063.0,,Not applicable,Not applicable,,,E02005045,E01024226,71.0, +147055,886,Kent,3172,Northbourne Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,140.0,No Special Classes,19-01-2023,132.0,69.0,63.0,6.8,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10083011.0,,Not applicable,18-07-2023,07-05-2024,Coldharbour Lane,Northbourne,,Deal,Kent,CT14 0LP,www.northbourne-cep.kent.sch.uk,1304611376.0,Mr,Matthew,Reynolds,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Eastry Rural,Dover,(England/Wales) Rural village,E10000016,632267.0,151903.0,Dover 005,Dover 005A,,,,,Good,South-East England and South London,,10034875643.0,,Not applicable,Not applicable,,,E02005045,E01024201,9.0, +147056,886,Kent,2659,Sandown School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,380.0,191.0,189.0,32.1,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10083016.0,,Not applicable,20-09-2023,20-05-2024,Golf Road,,,Deal,Kent,CT14 6PY,http://www.sandown.kent.sch.uk/,1304374951.0,Ms,Kate,Luxford,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,North Deal,Dover,(England/Wales) Urban city and town,E10000016,637419.0,153252.0,Dover 004,Dover 004B,,,,,Good,South-East England and South London,,100062285576.0,,Not applicable,Not applicable,,,E02005044,E01024230,122.0, +147057,886,Kent,3358,Sholden Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,105.0,No Special Classes,19-01-2023,97.0,46.0,51.0,21.6,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10083012.0,,Not applicable,14-06-2023,20-05-2024,London Road,Sholden,,Deal,Kent,CT14 0AB,www.sholdenprimary.org.uk,1304374852.0,Mrs,Dawn,Theaker,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Eastry Rural,Dover,(England/Wales) Urban city and town,E10000016,635694.0,152316.0,Dover 003,Dover 003A,,,,,Good,South-East England and South London,,100062285027.0,,Not applicable,Not applicable,,,E02005043,E01024219,21.0, +147058,886,Kent,3163,The Downs Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,420.0,No Special Classes,19-01-2023,317.0,156.0,161.0,36.9,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10083014.0,,Not applicable,13-09-2023,07-05-2024,Owen Square,Walmer,,Deal,Kent,CT14 7TL,http://www.downs.kent.sch.uk/,1304372486.0,Ms,Natalie,Luxford,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Walmer,Dover,(England/Wales) Urban city and town,E10000016,636946.0,150846.0,Dover 007,Dover 007D,,,,,Good,South-East England and South London,,100062286224.0,,Not applicable,Not applicable,,,E02005047,E01024250,117.0, +147059,886,Kent,4024,Stone Lodge School,Free schools,Free Schools,Open,New Provision,02-09-2019,,,Secondary,11.0,19,,No Nursery Classes,Has a sixth form,Mixed,None,None,,Non-selective,930.0,Not applicable,19-01-2023,677.0,392.0,285.0,26.9,Supported by a multi-academy trust,ENDEAVOUR MAT,-,,Not applicable,,10083768.0,,,18-10-2023,23-04-2024,Stone Lodge Road,,,Dartford,Kent,DA2 6FY,https://www.stonelodgeschool.co.uk/,1322250340.0,Mr,Gavin,Barnett,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dartford,Stone Castle,Dartford,(England/Wales) Urban major conurbation,E10000016,556289.0,174355.0,Dartford 006,Dartford 006B,,,,,Good,South-East England and South London,United Kingdom,10094155641.0,,Not applicable,Not applicable,,,E02005033,E01024170,177.0, +147083,886,Kent,2112,River Mill Primary School,Free schools,Free Schools,Open,New Provision,02-09-2019,,,Primary,3.0,11,,Has Nursery Classes,Does not have a sixth form,Mixed,,,,Non-selective,420.0,Not applicable,19-01-2023,240.0,128.0,112.0,9.2,Supported by a multi-academy trust,CONNECT SCHOOLS ACADEMY TRUST,Linked to a sponsor,Connect Schools Academy Trust,Not applicable,,10083765.0,,,06-12-2023,20-05-2024,Central Road,,,Dartford,,DA1 5XR,www.rivermillprimaryschool.co.uk,1322466975.0,Ms,Suzanne,Leader,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Town,Dartford,(England/Wales) Urban major conurbation,E10000016,554517.0,175013.0,Dartford 001,Dartford 001J,,,,,Good,South-East England and South London,United Kingdom,10023444394.0,,Not applicable,Not applicable,,,E02005028,E01035275,22.0, +147104,886,Kent,2114,Cage Green Primary School,Academy sponsor led,Academies,Open,New Provision,01-07-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,,421.0,Has Special Classes,19-01-2023,225.0,126.0,99.0,44.9,Supported by a multi-academy trust,CONNECT SCHOOLS ACADEMY TRUST,Linked to a sponsor,Connect Schools Academy Trust,Not applicable,,10083593.0,,Not applicable,09-11-2023,08-05-2024,Cage Green Road,,,Tonbridge,Kent,TN10 4PT,www.cage-green.kent.sch.uk,1732354325.0,,Joanna,Styles,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,19.0,22.0,,,South East,Tonbridge and Malling,Trench,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559644.0,148493.0,Tonbridge and Malling 011,Tonbridge and Malling 011A,,,,,Good,South-East England and South London,,100062543771.0,,Not applicable,Not applicable,,,E02005159,E01024729,101.0, +147205,886,Kent,6156,VTC Independent School,Other independent school,Independent schools,Open,New Provision,04-05-2020,,,Not applicable,13.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Selective,32.0,Has Special Classes,20-01-2022,19.0,18.0,1.0,0.0,Not applicable,,Not applicable,,Not applicable,,,,,30-11-2023,04-06-2024,"Unit 2,",Centre 2000,St Michaels Road,Sittingbourne,Kent,ME10 3DZ,https://vtcindependentschool.co.uk/,1795899240.0,Mrs,Anna,Daly,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not approved,,,,,,,,,,,,,,,,,,,South East,Swale,Roman,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591338.0,163638.0,Swale 010,Swale 010D,Ofsted,18.0,1.0,Vocational Training Centre Ltd,Good,South-East England and South London,United Kingdom,200002540944.0,,Not applicable,Not applicable,,,E02005124,E01024599,0.0, +147280,886,Kent,2135,Horsmonden Primary Academy,Academy converter,Academies,Open,Academy Converter,01-09-2019,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,180.0,94.0,86.0,10.0,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10084109.0,,Not applicable,19-10-2023,09-05-2024,Back Lane,Horsmonden,,Tonbridge,Kent,TN12 8NJ,https://horsmondenprimaryacademy.org.uk/,1892722529.0,Mrs,Hayley,Sharp,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Brenchley and Horsmonden,Tunbridge Wells,(England/Wales) Rural town and fringe,E10000016,570239.0,140737.0,Tunbridge Wells 011,Tunbridge Wells 011A,,,,,Good,South-East England and South London,,10008667663.0,,Not applicable,Not applicable,,,E02005172,E01024792,18.0, +147409,886,Kent,2127,Paddock Wood Primary Academy,Academy converter,Academies,Open,Academy Converter,01-09-2019,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,611.0,325.0,286.0,18.0,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10084108.0,,Not applicable,09-11-2023,30-04-2024,Old Kent Road,Paddock Wood,,Tonbridge,Kent,TN12 6JE,www.paddockwoodprimaryacademy.org.uk,1892833654.0,Mr,Simon,Page (Interim),Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tunbridge Wells,Paddock Wood East,Tunbridge Wells,(England/Wales) Rural town and fringe,E10000016,566923.0,144859.0,Tunbridge Wells 001,Tunbridge Wells 001D,,,,,Good,South-East England and South London,,100062545661.0,,Not applicable,Not applicable,,,E02005162,E01024813,110.0, +147454,886,Kent,2117,Dartford Bridge Community Primary School,Academy sponsor led,Academies,Open,New Provision,01-10-2019,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,,446.0,No Special Classes,19-01-2023,471.0,242.0,229.0,8.6,Supported by a multi-academy trust,CYGNUS ACADEMIES TRUST,Linked to a sponsor,Cygnus Academies Trust,Not applicable,,10084107.0,,,28-02-2024,22-05-2024,Community Campus,Birdwood Avenue,,Dartford,Kent,DA1 5GB,dartfordbridgecps.com,1322470678.0,Mrs,Sarah,Smith,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Bridge,Dartford,(England/Wales) Rural town and fringe,E10000016,554858.0,176047.0,Dartford 001,Dartford 001F,,,,,Good,South-East England and South London,United Kingdom,10023437975.0,,Not applicable,Not applicable,,,E02005028,E01035271,36.0, +147563,886,Kent,2287,Rolvenden Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2019,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,98.0,No Special Classes,19-01-2023,83.0,44.0,39.0,20.5,Supported by a multi-academy trust,TENTERDEN SCHOOLS TRUST,-,,Not applicable,,10084732.0,,Not applicable,,22-05-2024,Hastings Road,Rolvenden,,Cranbrook,Kent,TN17 4LS,http://www.rolvenden.kent.sch.uk,1580241444.0,,Ben,Vincer,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Ashford,Rolvenden & Tenterden West,Ashford,(England/Wales) Rural village,E10000016,584417.0,131221.0,Ashford 013,Ashford 013B,,,,,,South-East England and South London,,100062552639.0,,Not applicable,Not applicable,,,E02005008,E01024010,17.0, +147591,886,Kent,2118,St Katherine's School & Nursery,Academy sponsor led,Academies,Open,New Provision,01-11-2019,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,511.0,257.0,254.0,28.0,Supported by a multi-academy trust,COPPICE PRIMARY PARTNERSHIP,Linked to a sponsor,The Coppice Primary Partnership,Not applicable,,10084731.0,,,31-01-2024,20-05-2024,St. Katherines Lane,,,Snodland,Kent,ME6 5EJ,www.stkatherineskent.co.uk,1634240061.0,Mr,Ray,Lang,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Snodland East & Ham Hill,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,569847.0,161292.0,Tonbridge and Malling 002,Tonbridge and Malling 002D,,,,,Good,South-East England and South London,United Kingdom,100062628713.0,,Not applicable,Not applicable,,,E02005150,E01024772,143.0, +147729,886,Kent,2126,Sunny Bank Primary School,Academy sponsor led,Academies,Open,New Provision,01-02-2020,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,315.0,Not applicable,19-01-2023,194.0,104.0,90.0,56.0,Supported by a multi-academy trust,THE ISLAND LEARNING TRUST,Linked to a sponsor,The Island Learning Trust,Not applicable,,10085416.0,,,,16-05-2024,Sunny Bank,Murston,,Sittingbourne,Kent,ME10 3QN,www.sunnybank.kent.sch.uk,1795473891.0,Mr,Jack,Allen,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Murston,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591932.0,164074.0,Swale 011,Swale 011C,,,,,,South-East England and South London,United Kingdom,200002528712.0,,Not applicable,Not applicable,,,E02005125,E01024593,94.0, +147749,886,Kent,2237,Queenborough School and Nursery,Academy converter,Academies,Open,Academy Converter,01-03-2020,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,470.0,233.0,237.0,37.2,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10085584.0,,Not applicable,04-07-2023,03-05-2024,Edward Road,,,Queenborough,Kent,ME11 5DF,http://www.queenborough.kent.sch.uk,1795662574.0,Mr,Jason,Howard,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Queenborough and Halfway,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,591694.0,172082.0,Swale 005,Swale 005A,,,,,Outstanding,South-East England and South London,,100062626308.0,,Not applicable,Not applicable,,,E02005119,E01024594,162.0, +147750,886,Kent,2534,Bysing Wood Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2020,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,236.0,128.0,108.0,46.5,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10085585.0,,Not applicable,,19-03-2024,Lower Road,,,Faversham,Kent,ME13 7NU,http://www.bysing-wood.kent.sch.uk,1795534644.0,Mr,Andrew,Harrison,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,St Ann's,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,600233.0,161634.0,Swale 014,Swale 014C,,,,,,South-East England and South London,,200002530516.0,,Not applicable,Not applicable,,,E02005128,E01024604,100.0, +147751,886,Kent,2569,Briary Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2020,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,295.0,148.0,147.0,35.9,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10085586.0,,Not applicable,21-02-2024,20-05-2024,Greenhill Road,,,Herne Bay,Kent,CT6 7RS,www.briary.kent.sch.uk/,1227373095.0,Mrs,Kate,Espley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Greenhill,North Thanet,(England/Wales) Urban city and town,E10000016,616083.0,166658.0,Canterbury 004,Canterbury 004B,,,,,Good,South-East England and South London,,200000682218.0,,Not applicable,Not applicable,,,E02005013,E01024065,106.0, +147752,886,Kent,2629,Holywell Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2020,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,196.0,104.0,92.0,14.4,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10085587.0,,Not applicable,,03-06-2024,Forge Lane,Upchurch,,Sittingbourne,Kent,ME9 7AE,http://www.holywell.kent.sch.uk/,1634388416.0,Mrs,Nicky,Murrell,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,"Hartlip, Newington and Upchurch",Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,584508.0,167486.0,Swale 008,Swale 008D,,,,,,South-East England and South London,,200002533102.0,,Not applicable,Not applicable,,,E02005122,E01024573,25.0, +147850,886,Kent,2129,Springhead Park Primary School,Free schools,Free Schools,Open,New Provision,01-09-2020,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,,Non-selective,472.0,Not applicable,19-01-2023,368.0,176.0,192.0,20.1,Supported by a multi-academy trust,THE PRIMARY FIRST TRUST,Linked to a sponsor,The Primary First Trust,Not applicable,,10086458.0,,,25-05-2023,07-05-2024,Springhead Parkway,Springhead Park,,Northfleet,Kent,DA11 8BY,www.springheadparkprimary.com,1474555155.0,Mr,Wayne,Clayton,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Northfleet & Springhead,Gravesham,(England/Wales) Urban major conurbation,E10000016,562032.0,173001.0,Gravesham 006,Gravesham 006F,,,,,Good,South-East England and South London,United Kingdom,10012022831.0,,Not applicable,Not applicable,,,E02005060,E01035284,67.0, +147866,886,Kent,2131,Bearsted Primary Academy,Free schools,Free Schools,Open,New Provision,01-09-2020,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,,Non-selective,446.0,,19-01-2023,263.0,122.0,141.0,6.5,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10086483.0,,,25-01-2023,20-05-2024,Popesfield Way,Weavering,,Maidstone,Kent,ME14 5GA,https://bearstedprimaryacademy.org.uk/,1622250040.0,Mrs,Jane,Tipple,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Maidstone,Boxley,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,578618.0,156729.0,Maidstone 005,Maidstone 005B,,,,,Outstanding,South-East England and South London,United Kingdom,10095448157.0,,Not applicable,Not applicable,,,E02005072,E01024336,17.0, +147867,886,Kent,2140,Ebbsfleet Green Primary School,Free schools,Free Schools,Open,New Provision,01-09-2020,,,Primary,3.0,11,,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,,Non-selective,461.0,Not applicable,19-01-2023,262.0,148.0,114.0,8.4,Supported by a multi-academy trust,MARITIME ACADEMY TRUST,Linked to a sponsor,Maritime Academy Trust,Not applicable,,10086480.0,,,08-03-2023,14-09-2023,Ackers Drive Weldon,,Ebbsfleet Valley,Swanscombe,Kent,DA10 1AL,http://www.ebbsfleetgreenprimary.org.uk,1987591627.0,Mrs,Joanne,Wilkinson-Tabi,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,9.0,15.0,,,South East,Dartford,Ebbsfleet,Dartford,(England/Wales) Urban major conurbation,E10000016,560812.0,173242.0,Dartford 002,Dartford 002I,,,,,Good,South-East England and South London,United Kingdom,10094156607.0,,Not applicable,Not applicable,,,E02005029,E01035281,22.0, +147897,886,Kent,4027,The Holmesdale School,Academy sponsor led,Academies,Open,New Provision,01-09-2022,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,959.0,No Special Classes,19-01-2023,509.0,252.0,257.0,36.3,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10086757.0,,,,14-05-2024,Malling Road,,,Snodland,Kent,ME6 5HS,https://www.holmesdale.kent.sch.uk,1634240416.0,Mr,Lee,Downey,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,5.0,5.0,,,South East,Tonbridge and Malling,Snodland East & Ham Hill,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,569974.0,161137.0,Tonbridge and Malling 002,Tonbridge and Malling 002D,,,,,,South-East England and South London,United Kingdom,10002905073.0,,Not applicable,Not applicable,,,E02005150,E01024772,168.0, +148068,886,Kent,2143,Folkestone Primary,Academy sponsor led,Academies,Open,Split school,01-09-2020,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,,None,,,450.0,Not applicable,19-01-2023,424.0,211.0,213.0,24.1,Supported by a multi-academy trust,TURNER SCHOOLS,Linked to a sponsor,Turner Schools,Not applicable,,10086281.0,,,28-06-2023,14-05-2024,Academy Lane,,,Folkestone,,CT19 5FP,https://www.turnerfolkestoneprimary.com/home,1303842400.0,,Louise,Feaver,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Broadmead,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,621910.0,137354.0,Folkestone and Hythe 006,Folkestone and Hythe 006F,,,,,Good,South-East England and South London,United Kingdom,50120541.0,,Not applicable,Not applicable,,,E02005107,E01024516,102.0, +148116,886,Kent,2183,Marden Primary Academy,Academy converter,Academies,Open,Academy Converter,01-09-2020,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,275.0,No Special Classes,19-01-2023,281.0,154.0,127.0,29.9,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10086832.0,,,01-03-2023,14-04-2024,Goudhurst Road,Marden,,Tonbridge,Kent,TN12 9JX,www.mardenprimaryacademy.org.uk,1622831393.0,Mrs,Hannah,Penning,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Maidstone,Marden and Yalding,Maidstone and The Weald,(England/Wales) Rural town and fringe,E10000016,574009.0,144505.0,Maidstone 018,Maidstone 018C,Not applicable,,,,Good,South-East England and South London,,200003718702.0,,Not applicable,Not applicable,,,E02005085,E01024380,84.0, +148118,886,Kent,3106,Eastchurch Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2020,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,368.0,187.0,181.0,47.0,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10086841.0,,,12-07-2023,03-05-2024,Warden Road,Eastchurch,,Sheerness,Kent,ME12 4EJ,http://www.eastchurch.kent.sch.uk,1795880279.0,Mrs,Teresa,Oliver (Acting Head),Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheppey East,Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,598963.0,171467.0,Swale 006,Swale 006F,Not applicable,,,,Requires improvement,South-East England and South London,,100062626436.0,,Not applicable,Not applicable,,,E02005120,E01035301,162.0, +148144,886,Kent,1132,North West Kent Alternative Provision Service,Academy alternative provision sponsor led,Academies,Open,New Provision,01-09-2020,,,Not applicable,11.0,16,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,,Not applicable,19-01-2023,7.0,6.0,1.0,14.3,Supported by a multi-academy trust,ALTERNATIVE LEARNING TRUST,Linked to a sponsor,Alternative Learning Trust,Not applicable,,10086859.0,,,14-06-2023,05-06-2024,Richmond Drive,,,Gravesend,,DA12 4DJ,,1474332897.0,Ms,Abigail,Woodhouse,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Westcourt,Gravesham,(England/Wales) Urban major conurbation,E10000016,566417.0,172683.0,Gravesham 007,Gravesham 007C,,,,,Good,South-East England and South London,United Kingdom,10012024036.0,,Not applicable,Not applicable,,,E02005061,E01024311,1.0, +148217,886,Kent,5202,Holy Trinity Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2020,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,470.0,No Special Classes,19-01-2023,457.0,239.0,218.0,27.3,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10087078.0,,,04-10-2023,30-04-2024,Trinity Road,,,Gravesend,Kent,DA12 1LU,www.holytrinity-gravesend.kent.sch.uk/,1474534746.0,Mrs,Pamela,Gough,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Town,Gravesham,(England/Wales) Urban major conurbation,E10000016,565155.0,173712.0,Gravesham 003,Gravesham 003A,Not applicable,,,,Good,South-East England and South London,,100062311696.0,,Not applicable,Not applicable,,,E02005057,E01024258,118.0, +148308,886,Kent,3173,Kingsdown and Ringwould Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-01-2021,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,210.0,No Special Classes,19-01-2023,213.0,114.0,99.0,14.1,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10087305.0,,,25-05-2023,20-02-2024,Glen Road,Kingsdown,,Deal,Kent,CT14 8DD,http://www.kingsdown-ringwould.kent.sch.uk,1304373734.0,Mrs,Joanne,Hygate,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Dover,"Guston, Kingsdown & St Margaret's-at-Cliffe",Dover,(England/Wales) Rural town and fringe,E10000016,637415.0,148426.0,Dover 009,Dover 009A,Not applicable,,,,Outstanding,South-East England and South London,,100062286573.0,,Not applicable,Not applicable,,,E02005049,E01024232,30.0, +148370,886,Kent,2327,Worth Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2021,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,70.0,No Special Classes,19-01-2023,63.0,37.0,26.0,20.6,Supported by a multi-academy trust,DEAL EDUCATION ALLIANCE FOR LEARNING TRUST,-,,Not applicable,,10087569.0,,,07-02-2024,20-05-2024,The Street,,,Deal,Kent,CT14 0DF,http://www.worthprimary.co.uk,1304612148.0,Mrs,Katy,Chance,Executive Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Eastry Rural,South Thanet,(England/Wales) Rural village,E10000016,633761.0,156191.0,Dover 002,Dover 002B,Not applicable,,,,Good,South-East England and South London,,100062620829.0,,Not applicable,Not applicable,,,E02005042,E01024242,13.0, +148500,886,Kent,2259,Chartham Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2021,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,382.0,199.0,183.0,26.4,Supported by a multi-academy trust,INSPIRA ACADEMY TRUST,-,,Not applicable,,10088095.0,,,07-02-2024,20-05-2024,Shalmsford Street,Chartham,,Canterbury,Kent,CT4 7QN,http://www.charthamprimary.org.uk,1227738225.0,Mr,Jamie,Noble,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Chartham & Stone Street,Canterbury,(England/Wales) Rural hamlet and isolated dwellings,E10000016,610126.0,154535.0,Canterbury 017,Canterbury 017A,Not applicable,,,,Good,South-East England and South London,,200000682327.0,,Not applicable,Not applicable,,,E02005026,E01024052,101.0, +148501,886,Kent,2611,St Stephen's Infant School,Academy converter,Academies,Open,Academy Converter,01-04-2021,,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,269.0,146.0,123.0,28.3,Supported by a multi-academy trust,INSPIRA ACADEMY TRUST,-,,Not applicable,,10088094.0,,,24-01-2024,06-06-2024,Hales Drive,St Stephen's,,Canterbury,Kent,CT2 7AB,www.st-stephens-infant.kent.sch.uk,1227769204.0,Mrs,Alice,Edgington,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,St Stephen's,Canterbury,(England/Wales) Urban city and town,E10000016,614817.0,159227.0,Canterbury 013,Canterbury 013B,Not applicable,,,,Good,South-East England and South London,,200002882763.0,,Not applicable,Not applicable,,,E02005022,E01024100,76.0, +148502,886,Kent,2626,Sandwich Infant School,Academy converter,Academies,Open,Academy Converter,01-04-2021,,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,168.0,No Special Classes,19-01-2023,148.0,73.0,75.0,23.1,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10088093.0,,,30-01-2024,20-05-2024,School Road,,,Sandwich,Kent,CT13 9HT,www.sandwich-infant.kent.sch.uk/,1304612228.0,Miss,Leanne,Bennett,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dover,Sandwich,South Thanet,(England/Wales) Rural town and fringe,E10000016,632813.0,158407.0,Dover 002,Dover 002D,Not applicable,,,,Good,South-East England and South London,,100062284920.0,,Not applicable,Not applicable,,,E02005042,E01024244,34.0, +148519,886,Kent,5229,Fleetdown Primary Academy,Academy converter,Academies,Open,Academy Converter,01-04-2021,,,Primary,4.0,11,,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,654.0,No Special Classes,19-01-2023,648.0,345.0,303.0,13.3,Supported by a multi-academy trust,THE GOLDEN THREAD ALLIANCE,-,,Not applicable,,10088076.0,,,,03-06-2024,Lunedale Road,Darenth,,Dartford,Kent,DA2 6JX,http://www.fleetdown.kent.sch.uk,1322226891.0,Mrs,Alice,Harrington,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,HI - Hearing Impairment,,,,,,,,,,,,,Resourced provision,6.0,12.0,,,South East,Dartford,Brent,Dartford,(England/Wales) Urban major conurbation,E10000016,556256.0,173026.0,Dartford 008,Dartford 008E,Not applicable,,,,,South-East England and South London,,200000545083.0,,Not applicable,Not applicable,,,E02005035,E01024139,86.0, +148632,886,Kent,6164,MEPA ACADEMY,Other independent school,Independent schools,Open,New Provision,09-09-2021,,,Not applicable,11.0,16,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,,50.0,Not applicable,20-01-2022,17.0,3.0,14.0,0.0,Not applicable,,Not applicable,,Not applicable,,,,,29-09-2022,07-05-2024,27 & 29 EARL STREET,MAIDSTONE,KENT,,,ME14 1PF,https://www.mepaacademy.com/,1622756644.0,Mrs,Mandy,Ellen,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not approved,,,,,,,,,,,,,,,,,,,South East,Maidstone,High Street,Maidstone and The Weald,(England/Wales) Urban city and town,E10000016,575951.0,155842.0,Maidstone 004,Maidstone 004H,Ofsted,,6.0,Mandy Ellen Cook,Good,South-East England and South London,United Kingdom,10091815091.0,,Not applicable,Not applicable,,,E02005071,E01034991,0.0, +148711,886,Kent,2296,Mundella Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2021,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,173.0,87.0,86.0,56.1,Supported by a multi-academy trust,VERITAS MULTI ACADEMY TRUST,Linked to a sponsor,Veritas Multi Academy Trust,Not applicable,,10088877.0,,,,08-04-2024,Black Bull Road,,,Folkestone,Kent,CT19 5QX,http://www.mundella.kent.sch.uk,1303252265.0,Mrs,Lisa Paez and Lauren Wharmby,.,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,East Folkestone,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,622754.0,136911.0,Folkestone and Hythe 004,Folkestone and Hythe 004C,Not applicable,,,,,South-East England and South London,,50040639.0,,Not applicable,Not applicable,,,E02005105,E01024501,97.0, +148712,886,Kent,4246,The North School,Academy converter,Academies,Open,Academy Converter,01-01-2022,,,Secondary,11.0,19,No boarders,No Nursery Classes,Has a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Non-selective,1265.0,Not applicable,19-01-2023,1267.0,700.0,567.0,31.1,Supported by a multi-academy trust,SWALE ACADEMIES TRUST,Linked to a sponsor,Swale Academies Trust,Not applicable,,10088876.0,,,,28-05-2024,Essella Road,,,Ashford,Kent,TN24 8AL,https://www.thenorthschool.org.uk/,1233614600.0,Mrs,Clair,Ellerby,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision,25.0,25.0,,,South East,Ashford,Furley,Ashford,(England/Wales) Urban city and town,E10000016,601952.0,142258.0,Ashford 005,Ashford 005C,Not applicable,,,,,South-East England and South London,,100062560863.0,,Not applicable,Not applicable,,,E02005000,E01024023,350.0, +148876,886,Kent,2133,Halstead Community Primary School,Academy converter,Academies,Open,Academy Converter,01-03-2022,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,175.0,No Special Classes,19-01-2023,54.0,33.0,21.0,25.9,Supported by a multi-academy trust,THE PIONEER ACADEMY,Linked to a sponsor,The Pioneer Academy,Not applicable,,10089754.0,,,,03-06-2024,Otford Lane,Halstead,,Sevenoaks,Kent,TN14 7EA,www.halstead.kent.sch.uk,1959532224.0,Mrs,Sue,Saheed,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,"Halstead, Knockholt and Badgers Mount",Sevenoaks,(England/Wales) Rural village,E10000016,548908.0,161035.0,Sevenoaks 008,Sevenoaks 008D,Not applicable,,,,,South-East England and South London,,50002001021.0,,Not applicable,Not applicable,,,E02005094,E01024440,14.0, +148918,886,Kent,2119,Shears Green Infant School,Academy converter,Academies,Open,Academy Converter,01-07-2022,,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,342.0,163.0,179.0,21.3,Supported by a multi-academy trust,HORNCHURCH ACADEMY TRUST,-,,Not applicable,,10089902.0,,,,04-05-2024,Packham Road,Northfleet,,Gravesend,Kent,DA11 7JF,http://www.shearsgreeninfantschool.co.uk,1474566700.0,Ms,Hayley,Kotze,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Gravesham,Coldharbour & Perry Street,Gravesham,(England/Wales) Urban major conurbation,E10000016,563816.0,172261.0,Gravesham 009,Gravesham 009A,Not applicable,,,,,South-East England and South London,,100062310733.0,,Not applicable,Not applicable,,,E02005063,E01024264,73.0, +148991,886,Kent,2272,East Stour Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2022,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,472.0,No Special Classes,19-01-2023,438.0,233.0,205.0,36.2,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10090164.0,,,,07-06-2024,Earlsworth Road,South Willesborough,,Ashford,Kent,TN24 0DW,www.east-stour.kent.sch.uk/,1233630820.0,Mrs,E,Law,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Aylesford & East Stour,Ashford,(England/Wales) Urban city and town,E10000016,601863.0,140767.0,Ashford 009,Ashford 009G,Not applicable,,,,,South-East England and South London,,100062560510.0,,Not applicable,Not applicable,,,E02005004,E01032817,147.0, +148992,886,Kent,2672,Palm Bay Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2022,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,428.0,No Special Classes,19-01-2023,424.0,218.0,206.0,26.9,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10090163.0,,,,07-06-2024,Palm Bay Avenue,Cliftonville,,Margate,Kent,CT9 3PP,http://www.palmbay.uk,1843290050.0,Miss,Lizzie,Williams,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Cliftonville East,South Thanet,(England/Wales) Urban city and town,E10000016,637983.0,171167.0,Thanet 002,Thanet 002D,Not applicable,,,,,South-East England and South London,,100062307578.0,,Not applicable,Not applicable,,,E02005133,E01024656,114.0, +149039,886,Kent,4028,Barton Manor School,Free schools,Free Schools,Open,New Provision,01-09-2022,,,Secondary,11.0,19,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1050.0,Not applicable,19-01-2023,149.0,71.0,78.0,36.9,Supported by a multi-academy trust,BARTON COURT ACADEMY TRUST,Linked to a sponsor,Barton Court Academy Trust,Not applicable,,10090861.0,,,,17-05-2024,Spring Lane,,,Canterbury,Kent,CT1 1SU,https://www.bartonmanor.org/,1227532140.0,Mr,Richard,Morgan,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Canterbury,Barton,Canterbury,(England/Wales) Urban city and town,E10000016,616290.0,157296.0,Canterbury 016,Canterbury 016B,,,,,,South-East England and South London,United Kingdom,10094587078.0,,Not applicable,Not applicable,,,E02005025,E01024045,55.0, +149123,886,Kent,3020,Sedley's Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-06-2022,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,105.0,No Special Classes,19-01-2023,99.0,57.0,42.0,9.1,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10090460.0,,,12-07-2023,07-06-2024,Church Street,Southfleet,,Gravesend,Kent,DA13 9NR,www.sedleys.kent.sch.uk/,1474833221.0,Mrs,T,Handley,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,"Longfield, New Barn & Southfleet",Dartford,(England/Wales) Rural village,E10000016,561345.0,171128.0,Dartford 013,Dartford 013A,Not applicable,,,,Good,South-East England and South London,,200000535394.0,,Not applicable,Not applicable,,,E02005040,E01024157,9.0, +149261,886,Kent,3718,St Augustine's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2022,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,203.0,111.0,92.0,13.8,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10090965.0,,,,23-04-2024,St John's Road,,,Hythe,Kent,CT21 4BE,www.st-augustines-hythe.kent.sch.uk,1303266578.0,Mrs,Nicola,Clarke,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,Hythe,Folkestone and Hythe,(England/Wales) Urban city and town,E10000016,615271.0,135480.0,Folkestone and Hythe 008,Folkestone and Hythe 008A,Not applicable,,,,,South-East England and South London,,50017101.0,,Not applicable,Not applicable,,,E02005109,E01024523,28.0, +149319,886,Kent,4029,Aylesford School,Academy sponsor led,Academies,Open,New Provision,01-09-2022,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1044.0,No Special Classes,19-01-2023,873.0,453.0,420.0,20.7,Supported by a multi-academy trust,CHARACTER EDUCATION TRUST,Linked to a sponsor,Wrotham School,Not applicable,,10091142.0,,,,06-06-2024,Teapot Lane,,,Aylesford,Kent,ME20 7JU,www.aylesford.kent.sch.uk,1622717341.0,Miss,Tanya,Kelvie,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford South & Ditton,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,571899.0,158410.0,Tonbridge and Malling 005,Tonbridge and Malling 005B,,,,,,South-East England and South London,United Kingdom,200000966311.0,,Not applicable,Not applicable,,,E02005153,E01024718,163.0, +149554,886,Kent,4030,The Royal Harbour Academy,Academy sponsor led,Academies,Open,New Provision,01-04-2023,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,916.0,No Special Classes,,,,,,Supported by a multi-academy trust,COASTAL ACADEMIES TRUST,Linked to a sponsor,Coastal Academies Trust,Not applicable,,10091882.0,,,,13-05-2024,Newlands Lane,,,Ramsgate,Kent,CT12 6RH,www.rha.kent.sch.uk,1843572500.0,Mr,Simon,Pullen,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Thanet,Northwood,South Thanet,(England/Wales) Urban city and town,E10000016,637611.0,166588.0,Thanet 011,Thanet 011A,,,,,,South-East England and South London,United Kingdom,10022962106.0,,Not applicable,Not applicable,,,E02005142,E01024686,, +149623,886,Kent,3134,John Mayne Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2023,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Not applicable,140.0,No Special Classes,,,,,,Supported by a multi-academy trust,TENTERDEN SCHOOLS TRUST,-,,Not applicable,,10092108.0,,,,18-04-2024,High Street,Biddenden,,Ashford,Kent,TN27 8AL,www.john-mayne.kent.sch.uk/,1580291424.0,Mrs,Helen,Tester,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Biddenden,Ashford,(England/Wales) Rural town and fringe,E10000016,584986.0,138375.0,Ashford 011,Ashford 011B,Not applicable,,,,,South-East England and South London,,100062563751.0,,Not applicable,Not applicable,,,E02005006,E01023979,, +149666,886,Kent,2144,St. Clement's CofE Primary School,Academy converter,Academies,Open,Split school,01-04-2023,,,Primary,4.0,11,Not applicable,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Canterbury,Non-selective,210.0,Not applicable,,,,,,Supported by a multi-academy trust,THE DIOCESE OF CANTERBURY ACADEMIES TRUST,Linked to a sponsor,"Aquila, The Diocese of Canterbury Academies Trust",Not applicable,,10092363.0,,,,30-04-2024,Leysdown Road,,,Sheerness,Kent,ME12 4AB,www.stclementscep.co.uk,1795506910.0,Mrs,Kelly,Lockwood,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheppey East,Sittingbourne and Sheppey,(England/Wales) Rural village,E10000016,,,Swale 006,Swale 006A,,,,,,South-East England and South London,United Kingdom,100061077964.0,,Not applicable,Not applicable,,,E02005120,E01024580,, +149840,886,Kent,4032,Chilmington Green School,Free schools,Free Schools,Open,Academy Free School,01-09-2023,,,Secondary,11.0,19,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,1140.0,Not applicable,,,,,,Supported by a multi-academy trust,UNITED LEARNING TRUST,Linked to a sponsor,United Learning Trust,Not applicable,,10093045.0,,,,01-05-2024,Jemmett Road,,,Ashford,Kent,TN23 4QE,https://chilmingtongreenschool.org.uk/,1233438800.0,Mr,Jonathan,Rutland,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Ashford,Beaver,Ashford,(England/Wales) Urban city and town,E10000016,,,Ashford 007,Ashford 007B,,,,,,South-East England and South London,United Kingdom,10012841381.0,,Not applicable,Not applicable,,,E02005002,E01023975,, +149893,886,Kent,4033,The Abbey School,Academy converter,Academies,Open,Fresh Start,01-04-2023,,,Secondary,11.0,19,,No Nursery Classes,Has a sixth form,Mixed,None,None,,,1226.0,Not applicable,,,,,,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10092583.0,,,,22-05-2024,London Road,,,Faversham,Kent,ME13 8RZ,www.abbeyschoolfaversham.co.uk,1795532633.0,Dr,Rowland,Speller,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision and SEN unit,44.0,44.0,44.0,44.0,South East,Swale,Watling,Faversham and Mid Kent,(England/Wales) Urban city and town,E10000016,,,Swale 014,Swale 014E,,,,,,South-East England and South London,United Kingdom,100062379926.0,,Not applicable,Not applicable,,,E02005128,E01024626,, +149975,886,Kent,2066,Maypole Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2023,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,,,,,,Supported by a multi-academy trust,INSPIRE ACADEMY MOVEMENT TRUST,-,,Not applicable,,10092968.0,,,,08-05-2024,Franklin Road,,,Dartford,Kent,DA2 7UZ,www.maypole.kent.sch.uk/,1322523830.0,Miss,Katie,McCann,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Dartford,Maypole & Leyton Cross,Dartford,(England/Wales) Urban major conurbation,E10000016,551223.0,172486.0,Dartford 010,Dartford 010C,Not applicable,,,,,South-East England and South London,,10002021970.0,,Not applicable,Not applicable,,,E02005037,E01024152,, +149976,886,Kent,2134,Four Elms Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2023,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,108.0,No Special Classes,,,,,,Supported by a multi-academy trust,INSPIRE ACADEMY MOVEMENT TRUST,-,,Not applicable,,10092967.0,,,,15-05-2024,Bough Beech Road,Four Elms,,Edenbridge,Kent,TN8 6NE,www.four-elms.kent.sch.uk,1732700274.0,Mrs,Liz,Mitchell,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Cowden and Hever,Tonbridge and Malling,(England/Wales) Rural village,E10000016,546847.0,148273.0,Sevenoaks 015,Sevenoaks 015A,Not applicable,,,,,South-East England and South London,,10035181444.0,,Not applicable,Not applicable,,,E02005101,E01024420,, +149978,886,Kent,3035,Seal Church of England Voluntary Controlled Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2023,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,444.0,No Special Classes,,,,,,Supported by a multi-academy trust,INSPIRE ACADEMY MOVEMENT TRUST,-,,Not applicable,,10092966.0,,,,09-05-2024,Zambra Way,Seal,,Sevenoaks,Kent,TN15 0DJ,www.sealprimary.com,1732762388.0,Mrs,Tamsin,Jones,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Sevenoaks,Seal and Weald,Sevenoaks,(England/Wales) Urban city and town,E10000016,554704.0,157107.0,Sevenoaks 010,Sevenoaks 010A,Not applicable,,,,,South-East England and South London,,10035185574.0,,Not applicable,Not applicable,,,E02005096,E01024457,, +150156,886,Kent,5208,Ditton Church of England Junior School,Academy converter,Academies,Open,Academy Converter,01-11-2023,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,256.0,No Special Classes,,,,,,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10093280.0,,,,21-05-2024,New Road,Ditton,,Aylesford,Kent,ME20 6AE,http://www.ditton-jun.kent.sch.uk,1732843446.0,Mr,Graham,Ward,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Aylesford South & Ditton,Chatham and Aylesford,(England/Wales) Urban city and town,E10000016,571105.0,158080.0,Tonbridge and Malling 005,Tonbridge and Malling 005D,Not applicable,,,,,South-East England and South London,,10002907508.0,,Not applicable,Not applicable,,,E02005153,E01024736,, +150255,886,Kent,2692,The Churchill School,Academy converter,Academies,Open,Academy Converter,01-12-2023,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,,,,,,Supported by a multi-academy trust,EKC SCHOOLS TRUST LIMITED,-,,Not applicable,,10093663.0,,,,23-05-2024,Haven Drive,Hawkinge,,Folkestone,Kent,CT18 7RH,http://www.thechurchillschool.co.uk,1303893892.0,Mrs,Zoe,Stone,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Folkestone and Hythe,North Downs East,Folkestone and Hythe,(England/Wales) Rural town and fringe,E10000016,621237.0,139587.0,Folkestone and Hythe 002,Folkestone and Hythe 002F,Not applicable,,,,,South-East England and South London,,50102503.0,,Not applicable,Not applicable,,,E02005103,E01033214,, +150278,886,Kent,4034,Dover Christ Church Academy,Academy sponsor led,Academies,Open,Fresh Start,01-09-2023,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,,,990.0,Not applicable,,,,,,Supported by a multi-academy trust,TURNER SCHOOLS,Linked to a sponsor,Turner Schools,Not applicable,,10093628.0,,,,14-05-2024,Melbourne Avenue,,,Dover,,CT16 2EG,,,Mr,Jamie,MacLean,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,PMLD - Profound and Multiple Learning Difficulty,,,,,,,,,,,,,SEN unit,,,69.0,72.0,South East,Dover,Whitfield,Dover,(England/Wales) Urban city and town,E10000016,630895.0,144193.0,Dover 010,Dover 010E,,,,,,South-East England and South London,United Kingdom,100062289416.0,,Not applicable,Not applicable,,,E02005050,E01024254,, +150743,886,Kent,4036,Leigh Academy Hugh Christie,Academy sponsor led,Academies,Open,New Provision,01-04-2024,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1226.0,Has Special Classes,,,,,,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10094758.0,,,,01-04-2024,White Cottage Road,,,Tonbridge,Kent,TN10 4PU,,1732353544.0,Mr,Palak,Shah,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Tonbridge and Malling,Trench,Tonbridge and Malling,(England/Wales) Urban city and town,E10000016,559444.0,148580.0,Tonbridge and Malling 009,Tonbridge and Malling 009A,,,,,,South-East England and South London,United Kingdom,10002906320.0,,Not applicable,Not applicable,,,E02005157,E01024730,, +150934,886,Kent,4037,EKC Sheppey Secondary,Academy sponsor led,Academies,Proposed to open,Split school,01-09-2024,,,Secondary,11.0,16,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,,None,,,750.0,Not applicable,,,,,,Not supported by a trust,,-,,Not applicable,,10095470.0,,,,27-04-2024,Marine Parade,,,Sheerness,,ME12 2BE,,,,January,Lorman,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Sheerness,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,593060.0,174642.0,Swale 001,Swale 001B,,,,,,South-East England and South London,United Kingdom,100062378138.0,,Not applicable,Not applicable,,,E02005115,E01024610,, +150935,886,Kent,4038,Leigh Academy Minster,Academy sponsor led,Academies,Proposed to open,Split school,01-09-2024,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,,None,,,1150.0,Not applicable,,,,,,Not supported by a trust,,-,,Not applicable,,10095469.0,,,,27-04-2024,Minster Road,,,Sheerness,,ME12 3JQ,,,,Mathieu,Stevens,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Swale,Queenborough and Halfway,Sittingbourne and Sheppey,(England/Wales) Urban city and town,E10000016,593740.0,172606.0,Swale 004,Swale 004A,,,,,,South-East England and South London,United Kingdom,200002529794.0,,Not applicable,Not applicable,,,E02005118,E01024595,, diff --git a/data/init/kevlarai_data/kevlarai_curriculum.xlsx b/data/init/kevlarai_data/kevlarai_curriculum.xlsx new file mode 100644 index 0000000..ad428f8 Binary files /dev/null and b/data/init/kevlarai_data/kevlarai_curriculum.xlsx differ diff --git a/data/init/kevlarai_data/kevlarai_curriculum_lessons_df.csv b/data/init/kevlarai_data/kevlarai_curriculum_lessons_df.csv new file mode 100644 index 0000000..6466dd7 --- /dev/null +++ b/data/init/kevlarai_data/kevlarai_curriculum_lessons_df.csv @@ -0,0 +1,21 @@ +SyllabusSubject,SyllabusKeyStage,SyllabusStageID,SyllabusYearID,TopicStage,TopicSubjectCode,Topic,Lesson,TopicID,LessonID,LessonTitle,LessonTitleEnter,LessonType,NumberOfLearningOutcomes,SuggestedNumberOfPeriodsForLesson,LessonLearningObjectiveEnter,SuggestedActivities,WebLinks,SkillsLearned,TopicTitle,LessonTitleAuto,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,1,1,7B1,7B1.1,"Living, dead and never been alive","Living, dead and never been alive",Standard,3,1,Describe the features of living organisms,Class Prac: Have student carry out mini actions - use these to deduce MRS GRENDiscuss: What determines whether something is alive or not? Use objects/images to help with this.Worksheets: 7Aa1,,,Life Processes,,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,1,2,7B1,7B1.2,Plant and animal cells,Plant and animal cells,Standard,3,1,Identify the main parts of animal cells and describe their functions.,Class Prac: Use bio-viewers to look at simple animal and plants cells. Sketch cells. Label cells. Describe their functions. Venn diagram - Compare plant and animal cells. Highlight differences.Worksheet: 7Ad1,,,Life Processes,,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,1,R,7B1,7B1.R,Revision of topic 7B1 Life Processes,Null,Review,1,1,,,,,Life Processes,Revision of topic 7B1 Life Processes,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,1,T,7B1,7B1.T,Topic test: 7B1 Life Processes,Null,Assessment,1,1,,,,,Life Processes,Topic test: 7B1 Life Processes,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,2,1,7B2,7B2.1,Aerobic Respiration,Aerobic Respiration,Standard,3,1,Be able to describe what happens during respiration,Brain dump: Students write down prior knowledge – opportunity to deal with misconceptions (see starter slide). Demo: Screaming jelly baby,,,Respiration And Circulation,,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,2,2,7B2,7B2.2,Anaerobic respiration,Anaerobic respiration,Standard,3,1,Describe what happens in anaerobic respiration in humans,Explain how and why anaerobic respiration happens in humans. Extend into fermentation. Class Prac: Investigate the rate of anaerobic respiration in yeast. Compare pros and cons of aerobic and anaerobic respiration Worksheets: 8Db-2,,,Respiration And Circulation,,,,,,,, + +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,12-1,AP.PAG,AP.PAG.12-1,PAG 12 Research,PAG 12 Research,Focus,1,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,12-2,AP.PAG,AP.PAG.12-2,PAG 12 Research,PAG 12 Research,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,12-3,AP.PAG,AP.PAG.12-3,PAG 12 Research,PAG 12 Research,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,2-1,AP.PAG,AP.PAG.2-1,PAG 2 Investigating Properties of Materials,PAG 2 Investigating Properties of Materials,Focus,1,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,2-2,AP.PAG,AP.PAG.2-2,PAG 2 Investigating Properties of Materials,PAG 2 Investigating Properties of Materials,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,2-3,AP.PAG,AP.PAG.2-3,PAG 2 Investigating Properties of Materials,PAG 2 Investigating Properties of Materials,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,3-1,AP.PAG,AP.PAG.3-1,PAG 3 Investigating Electrical Properties: Resistivity of a metal,PAG 3 Investigating Electrical Properties: Resistivity of a metal,Focus,1,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,3-2,AP.PAG,AP.PAG.3-2,PAG 3 Investigating Electrical Properties: Investigating electrical characteristics,PAG 3 Investigating Electrical Properties: Investigating electrical characteristics,Focus,1,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,3-3,AP.PAG,AP.PAG.3-3,PAG 3 Investigating Electrical Properties: Determining the maximum power from a cell,PAG 3 Investigating Electrical Properties: Determining the maximum power from a cell,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,1-1,AP.PAG,AP.PAG.1-1,PAG 4 Investigating Electrical Circuits: Investigating electrical circuits,PAG 4 Investigating Electrical Circuits: Investigating electrical circuits,Focus,1,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,1-2,AP.PAG,AP.PAG.1-2,PAG 4 Investigating Electrical Circuits: Electrical circuits with more than one source of emf,PAG 4 Investigating Electrical Circuits: Electrical circuits with more than one source of emf,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,1-3,AP.PAG,AP.PAG.1-3,PAG 4 Investigating Electrical Circuits: Using non-ohmic devices as sensors,PAG 4 Investigating Electrical Circuits: Using non-ohmic devices as sensors,Focus,0,1,,,,,A-level Physics PAG,,,,,,,, +Physics,5,KS5.Physics,KS5.Physics.Special,A,P,PAG,1-1,AP.PAG,AP.PAG.1-1,PAG 5 Investigating Waves: Determining wavelength with diffraction grating,PAG 5 Investigating Waves: Determining wavelength with diffraction grating,Focus,1,1,,,,,A-level Physics PAG,,,,,,,, diff --git a/data/init/kevlarai_data/kevlarai_curriculum_statements_df.csv b/data/init/kevlarai_data/kevlarai_curriculum_statements_df.csv new file mode 100644 index 0000000..922fe1d --- /dev/null +++ b/data/init/kevlarai_data/kevlarai_curriculum_statements_df.csv @@ -0,0 +1,8 @@ +SyllabusSubject,SyllabusKeyStage,SyllabusStageID,SyllabusYearID,TopicStage,TopicSubject,Topic,Lesson,Statement,TopicID,LessonID,StatementID,TopicTitle,LessonTitle,LearningStatement,LearningStatementEnter,StatementType,StatementLevel,LearningStatementAuto,,,,,,,,,,, +Science,3,KS3.Science,Y7.Science,7,B,1,1,1,7B1,7B1.1,7B1.1.1,Life Processes,"Living, dead and never been alive",Recall the different life processes,Recall the different life processes,Outcome,,Incomplete,,,,,,,,,,, +Physics,5,KS5.Physics,Y12.Physics,A,P,8,1,1,AP.8,AP.8.1,AP.8.1.1,Charge And Current,Current And Charge,Understand electric current as the rate of flow of charge using the equation I = ΔQ/Δt,Understand electric current as the rate of flow of charge using the equation I = ΔQ/Δt,Outcome,,Incomplete,,,,,,,,,,, +Physics,5,KS5.Physics,Y12.Physics,A,P,8,1,2,AP.8,AP.8.1,AP.8.1.2,Charge And Current,Current And Charge,Define the coulomb as the unit of electric charge,Define the coulomb as the unit of electric charge,Outcome,,Incomplete,,,,,,,,,,, +Physics,5,KS5.Physics,Y12.Physics,A,P,8,1,3,AP.8,AP.8.1,AP.8.1.3,Charge And Current,Current And Charge,State the elementary charge e as 1.6 x 10^-19 C,State the elementary charge e as 1.6 x 10^-19 C,Outcome,,Incomplete,,,,,,,,,,, +Physics,5,KS5.Physics,Y12.Physics,A,P,8,1,4,AP.8,AP.8.1,AP.8.1.4,Charge And Current,Current And Charge,Recognize that the net charge on a particle or object is quantized and is a multiple of e,Recognize that the net charge on a particle or object is quantized and is a multiple of e,Outcome,,Incomplete,,,,,,,,,,, +Physics,5,KS5.Physics,Y12.Physics,A,P,8,2,1,AP.8,AP.8.2,AP.8.2.1,Charge And Current,Moving Charges,Explain current as the movement of electrons in metals and the movement of ions in electrolytes,Explain current as the movement of electrons in metals and the movement of ions in electrolytes,Outcome,,Incomplete,,,,,,,,,,, +Physics,5,KS5.Physics,Y12.Physics,A,P,8,2,2,AP.8,AP.8.2,AP.8.2.2,Charge And Current,Moving Charges,Differentiate between conventional current and electron flow,Differentiate between conventional current and electron flow,Outcome,,Incomplete,,,,,,,,,,, \ No newline at end of file diff --git a/data/init/kevlarai_data/kevlarai_curriculum_topics_df.csv b/data/init/kevlarai_data/kevlarai_curriculum_topics_df.csv new file mode 100644 index 0000000..c77d5fc --- /dev/null +++ b/data/init/kevlarai_data/kevlarai_curriculum_topics_df.csv @@ -0,0 +1,15 @@ +SyllabusSubject,SyllabusKeyStage,SyllabusYear,SyllabusStageID,SyllabusYearID,TopicStage,TopicSubjectCode,Topic,TopicID,TopicTitle,TotalNumberOfLessonsForTopic,TopicType,TopicAssessmentType,, +Science,3,7,KS3.Science,Y7.Science,I,B,1,7B1,Life Processes,11,Standard,Test,, +Science,3,7,KS3.Science,Y7.Science,I,B,2,7B2,Respiration And Circulation,11,Standard,Test,, +Science,3,7,KS3.Science,Y7.Science,I,B,3,7B3,Variation,11,Standard,Test,, +Science,3,7,KS3.Science,Y7.Science,I,B,4,7B4,Food And Digestion,12,Standard,Test,, +Science,3,7,KS3.Science,Y7.Science,I,B,5,7B5,Reproduction,11,Standard,Test,, +Physics,Y13.Physics,A,P,15,AP.15,Ideal Gases,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,16,AP.16,Circular Motion,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,17,AP.17,Oscillations,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,18,AP.18,Gravitational Fields,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,19,AP.19,Stars,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,20,AP.20,Cosmology,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,21,AP.21,Capacitors,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,22,AP.22,Electric Fields,5,Standard,Test,, +Physics,5,13,KS5.Physics,Y13.Physics,A,P,23,AP.23,Magnetic Fields,5,Standard,Test,, \ No newline at end of file diff --git a/data/init/kevlarai_data/kevlarai_timetable.xlsx b/data/init/kevlarai_data/kevlarai_timetable.xlsx new file mode 100644 index 0000000..bf9a630 Binary files /dev/null and b/data/init/kevlarai_data/kevlarai_timetable.xlsx differ diff --git a/data/init/kevlarai_data/users/kcar_planning.xlsx b/data/init/kevlarai_data/users/kcar_planning.xlsx new file mode 100644 index 0000000..4044868 Binary files /dev/null and b/data/init/kevlarai_data/users/kcar_planning.xlsx differ diff --git a/data/init/kevlarai_data/users/kcar_timetable.xlsx b/data/init/kevlarai_data/users/kcar_timetable.xlsx new file mode 100644 index 0000000..794f128 Binary files /dev/null and b/data/init/kevlarai_data/users/kcar_timetable.xlsx differ diff --git a/data/init/sample_schools.csv b/data/init/sample_schools.csv new file mode 100644 index 0000000..9c69a73 --- /dev/null +++ b/data/init/sample_schools.csv @@ -0,0 +1,105 @@ +URN,LA (code),LA (name),EstablishmentNumber,EstablishmentName,TypeOfEstablishment (name),EstablishmentTypeGroup (name),EstablishmentStatus (name),ReasonEstablishmentOpened (name),OpenDate,ReasonEstablishmentClosed (name),CloseDate,PhaseOfEducation (name),StatutoryLowAge,StatutoryHighAge,Boarders (name),NurseryProvision (name),OfficialSixthForm (name),Gender (name),ReligiousCharacter (name),ReligiousEthos (name),Diocese (name),AdmissionsPolicy (name),SchoolCapacity,SpecialClasses (name),CensusDate,NumberOfPupils,NumberOfBoys,NumberOfGirls,PercentageFSM,TrustSchoolFlag (name),Trusts (name),SchoolSponsorFlag (name),SchoolSponsors (name),FederationFlag (name),Federations (name),UKPRN,FEHEIdentifier,FurtherEducationType (name),OfstedLastInsp,LastChangedDate,Street,Locality,Address3,Town,County (name),Postcode,SchoolWebsite,TelephoneNum,HeadTitle (name),HeadFirstName,HeadLastName,HeadPreferredJobTitle,BSOInspectorateName (name),InspectorateReport,DateOfLastInspectionVisit,NextInspectionVisit,TeenMoth (name),TeenMothPlaces,CCF (name),SENPRU (name),EBD (name),PlacesPRU,FTProv (name),EdByOther (name),Section41Approved (name),SEN1 (name),SEN2 (name),SEN3 (name),SEN4 (name),SEN5 (name),SEN6 (name),SEN7 (name),SEN8 (name),SEN9 (name),SEN10 (name),SEN11 (name),SEN12 (name),SEN13 (name),TypeOfResourcedProvision (name),ResourcedProvisionOnRoll,ResourcedProvisionCapacity,SenUnitOnRoll,SenUnitCapacity,GOR (name),DistrictAdministrative (name),AdministrativeWard (name),ParliamentaryConstituency (name),UrbanRural (name),GSSLACode (name),Easting,Northing,MSOA (name),LSOA (name),InspectorateName (name),SENStat,SENNoStat,PropsName,OfstedRating (name),RSCRegion (name),Country (name),UPRN,SiteName,QABName (name),EstablishmentAccredited (name),QABReport,CHNumber,MSOA (code),LSOA (code),FSM,AccreditationExpiryDate +118317,887,Medway,2198,Greenvale Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,120.0,No Special Classes,19-01-2023,110.0,58.0,52.0,39.1,Not applicable,,Not applicable,,Not under a federation,,10074076.0,,Not applicable,22-11-2023,22-04-2024,Symons Avenue,,,Chatham,Kent,ME4 5UP,www.greenvale.medway.sch.uk,1634409521.0,Mrs,Amanda,Allnutt,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Chatham Central & Brompton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576183.0,166676.0,Medway 022,Medway 022B,,,,,Good,South-East England and South London,,100062391942.0,,Not applicable,Not applicable,,,E02003335,E01016023,43.0, +118320,887,Medway,2202,New Road Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,360.0,187.0,173.0,38.6,Not applicable,,Not applicable,,Not under a federation,,10076230.0,,Not applicable,19-04-2023,04-06-2024,Bryant Street,,,Chatham,Kent,ME4 5QN,http://www.newroad.medway.sch.uk,1634843084.0,Mrs,Samantha,Cooper,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Chatham Central & Brompton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576251.0,167414.0,Medway 015,Medway 015C,,,,,Good,South-East England and South London,,44054185.0,,Not applicable,Not applicable,,,E02003328,E01016019,139.0, +118329,887,Medway,2215,Balfour Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,266.0,140.0,126.0,23.8,Not applicable,,Not applicable,,Not under a federation,,10074074.0,,Not applicable,04-06-2019,12-04-2024,Pattens Lane,,,Rochester,Kent,ME1 2QT,http://www.balfourinf.medway.sch.uk,1634338280.0,Ms,Donna,Atkinson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Rochester and Strood,(England/Wales) Urban city and town,E06000035,575083.0,166460.0,Medway 026,Medway 026C,,,,,Good,South-East England and South London,,44035930.0,,Not applicable,Not applicable,,,E02003339,E01016124,63.0, +118330,887,Medway,2216,Crest Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,177.0,83.0,94.0,34.5,Not applicable,,Not applicable,,Not under a federation,,10074073.0,,Not applicable,12-02-2020,29-04-2024,Fleet Road,,,Rochester,Kent,ME1 2QA,www.crestinfants.co.uk,1634844127.0,Mrs,Kerry,Seales,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574568.0,166523.0,Medway 024,Medway 024A,,,,,Good,South-East England and South London,,200000909496.0,,Not applicable,Not applicable,,,E02003337,E01016116,60.0, +118423,887,Medway,2403,Hempstead Junior School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,360.0,No Special Classes,19-01-2023,353.0,193.0,160.0,7.9,Not applicable,,Not applicable,,Not under a federation,,10079033.0,,Not applicable,20-06-2018,06-06-2024,Birch Grove,Hempstead,,Gillingham,Kent,ME7 3HJ,www.hempsteadjnr.medway.sch.uk/,1634336963.0,Mr,Paul,Cross,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Hempstead & Wigmore,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579226.0,164022.0,Medway 035,Medway 035A,,,,,Good,South-East England and South London,,100062394567.0,,Not applicable,Not applicable,,,E02003348,E01016050,28.0, +118442,887,Medway,2439,Horsted Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,177.0,93.0,84.0,10.2,Not applicable,,Not applicable,,Supported by a federation,The Bluebell Federation,10074065.0,,Not applicable,11-10-2023,03-06-2024,Barberry Avenue,,,Chatham,Kent,ME5 9TF,www.horstedschool.co.uk/,1634335400.0,Mrs,Sarah,Steer,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Horsted,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574928.0,164210.0,Medway 033,Medway 033C,,,,,Good,South-East England and South London,,200000909501.0,,Not applicable,Not applicable,,,E02003346,E01016123,18.0, +118472,887,Medway,2494,Parkwood Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,540.0,No Special Classes,19-01-2023,546.0,270.0,276.0,16.8,Not applicable,,Not applicable,,Not under a federation,,10074063.0,,Not applicable,08-06-2023,07-05-2024,Deanwood Drive,Rainham,,Gillingham,Kent,ME8 9LP,www.parkwoodprimary.org.uk,1634234699.0,Headteacher,Lee,McCormack,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South East,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,581085.0,164262.0,Medway 036,Medway 036D,,,,,Requires improvement,South-East England and South London,,100062396933.0,,Not applicable,Not applicable,,,E02003349,E01016105,92.0, +118477,887,Medway,2506,Horsted Junior School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,248.0,No Special Classes,19-01-2023,249.0,130.0,119.0,16.5,Not applicable,,Not applicable,,Supported by a federation,The Bluebell Federation,10079031.0,,Not applicable,13-09-2023,03-06-2024,Barberry Avenue,,,Chatham,Kent,ME5 9TF,www.horstedschool.co.uk,1634335400.0,Mrs,Sarah,Steer,Executive Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Horsted,Rochester and Strood,(England/Wales) Urban city and town,E06000035,575006.0,164184.0,Medway 033,Medway 033C,,,,,Good,South-East England and South London,,44018867.0,,Not applicable,Not applicable,,,E02003346,E01016123,41.0, +118509,887,Medway,2549,Swingate Primary School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,659.0,335.0,324.0,12.0,Not applicable,,Not applicable,,Not under a federation,,10076224.0,,Not applicable,08-11-2018,21-05-2024,Sultan Road,Lordswood,,Chatham,Kent,ME5 8TJ,www.swingate.medway.sch.uk/,1634863778.0,Mr,Steven,Geary,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Lordswood & Walderslade,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,577377.0,162361.0,Medway 038,Medway 038B,,,,,Good,South-East England and South London,,44058696.0,,Not applicable,Not applicable,,,E02003351,E01016056,76.0, +118555,887,Medway,2638,Hempstead Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,270.0,149.0,121.0,5.2,Not applicable,,Not applicable,,Not under a federation,,10074059.0,,Not applicable,13-03-2024,22-05-2024,Hempstead Road,Hempstead,,Gillingham,Kent,ME7 3QG,www.hempsteadschoolsfederation.org.uk,1634336963.0,Mr,Paul,Cross,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Hempstead & Wigmore,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579161.0,164155.0,Medway 035,Medway 035B,,,,,Good,South-East England and South London,,200000901137.0,,Not applicable,Not applicable,,,E02003348,E01016051,14.0, +118576,887,Medway,2665,St Peter's Infant School,Community school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,100.0,No Special Classes,19-01-2023,94.0,53.0,41.0,32.3,Not applicable,,Not applicable,,Not under a federation,,10074058.0,,Not applicable,13-12-2018,02-06-2024,Holcombe Road,,,Rochester,Kent,ME1 2HU,http://www.stpetersinfants.co.uk/,1634843590.0,Mrs,Joanna,Worrall (Interim),Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574398.0,167163.0,Medway 017,Medway 017D,,,,,Good,South-East England and South London,,44025555.0,,Not applicable,Not applicable,,,E02003330,E01016118,30.0, +118641,887,Medway,3096,"St Helen's Church of England Primary School, Cliffe",Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,211.0,107.0,104.0,18.5,Not applicable,,Not applicable,,Not under a federation,,10069109.0,,Not applicable,04-06-2019,23-04-2024,Church Street,Cliffe,,Rochester,Kent,ME3 7PU,www.sthelens.medway.sch.uk/,1634220246.0,Mrs,Stephanie,Jarvis,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood Rural,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,573629.0,176192.0,Medway 002,Medway 002A,,,,,Good,South-East England and South London,,44018290.0,,Not applicable,Not applicable,,,E02003315,E01016142,39.0, +118643,887,Medway,3102,St Nicholas CEVC Primary School,Voluntary controlled school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,120.0,No Special Classes,19-01-2023,93.0,46.0,47.0,28.0,Not applicable,,Not applicable,,Not under a federation,,10079669.0,,Not applicable,23-01-2013,26-04-2024,St Nicholas CEVC primary School,London Road,Strood,Rochester,Kent,ME2 3HU,http://www.st-nicholas.medway.sch.uk,1634717120.0,Mrs,Ruth,Gooch,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood North & Frindsbury,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573257.0,169332.0,Medway 006,Medway 006E,,,,,Outstanding,South-East England and South London,,200000896704.0,,Not applicable,Not applicable,,,E02003319,E01016141,26.0, +118756,887,Medway,3712,St Michael's RC Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,460.0,No Special Classes,19-01-2023,469.0,236.0,233.0,25.6,Not applicable,,Not applicable,,Not under a federation,,10076805.0,,Not applicable,06-11-2019,08-05-2024,Hills Terrace,,,Chatham,Kent,ME4 6PX,www.stmichaelsrcp.org,1634832578.0,Mrs,Nicola,Collins,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575579.0,167357.0,Medway 015,Medway 015B,,,,,Good,South-East England and South London,,44040971.0,,Not applicable,Not applicable,,,E02003328,E01016017,107.0, +118766,887,Medway,3729,English Martyrs' Catholic Primary School,Voluntary aided school,Local authority maintained schools,"Open, but proposed to close",Not applicable,,Academy Converter,30-06-2024,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,202.0,101.0,101.0,15.8,Not applicable,,Not applicable,,Not under a federation,,10072742.0,,Not applicable,13-07-2023,28-05-2024,Frindsbury Road,Strood,,Rochester,Kent,ME2 4JA,http://www.englishmartyrs.medway.sch.uk,1634718964.0,Ms,Catherine,Thacker,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood North & Frindsbury,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573987.0,169894.0,Medway 006,Medway 006A,,,,,Good,South-East England and South London,,100062388690.0,,Not applicable,Not applicable,,,E02003319,E01016135,32.0, +118767,887,Medway,3732,St Thomas of Canterbury RC Primary School,Voluntary aided school,Local authority maintained schools,"Open, but proposed to close",Not applicable,,Academy Converter,30-06-2024,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,236.0,No Special Classes,19-01-2023,258.0,125.0,133.0,12.1,Not applicable,,Not applicable,,Not under a federation,,10072741.0,,Not applicable,08-06-2023,28-05-2024,Romany Road,Rainham,,Gillingham,Kent,ME8 6JH,www.stthomascanterbury.org.uk/,1634234677.0,Mrs,Vicki-Louise,Gallagher,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Twydall,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579863.0,166652.0,Medway 018,Medway 018F,,,,,Good,South-East England and South London,,44087220.0,,Not applicable,Not applicable,,,E02003331,E01016165,29.0, +118769,887,Medway,3736,St Thomas More Roman Catholic Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,420.0,No Special Classes,19-01-2023,417.0,188.0,229.0,11.3,Not applicable,,Not applicable,,Not under a federation,,10072739.0,,Not applicable,08-02-2013,09-02-2024,Bleakwood Road,,,Chatham,Kent,ME5 0NF,www.st-thomasmore.medway.sch.uk/,1634864701.0,Mrs,Victoria,Ebdon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Wayfield & Weeds Wood,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576043.0,164020.0,Medway 033,Medway 033D,,,,,Outstanding,South-East England and South London,,44045642.0,,Not applicable,Not applicable,,,E02003346,E01016173,47.0, +118775,887,Medway,3746,St William of Perth Roman Catholic Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,208.0,106.0,102.0,7.7,Not applicable,,Not applicable,,Not under a federation,,10072738.0,,Not applicable,18-10-2023,10-05-2024,Canon Close,Maidstone Road,,Rochester,Kent,ME1 3EN,www.stwilliamofperth.org.uk/,1634404267.0,Mr,James,Willis,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573973.0,166819.0,Medway 024,Medway 024D,,,,,Good,South-East England and South London,,44010893.0,,Not applicable,Not applicable,,,E02003337,E01016132,16.0, +118779,887,Medway,3752,St Augustine of Canterbury Catholic Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,19-01-2023,199.0,101.0,98.0,13.6,Not applicable,,Not applicable,,Not under a federation,,10072736.0,,Not applicable,07-02-2024,20-05-2024,Deanwood Drive,Rainham,,Gillingham,Kent,ME8 9NP,www.staccp.org.uk/,1634371892.0,Mrs,Louise,Prestidge,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South East,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580898.0,163428.0,Medway 036,Medway 036C,,,,,Good,South-East England and South London,,200000901485.0,,Not applicable,Not applicable,,,E02003349,E01016100,27.0, +118782,887,Medway,3755,St Mary's Catholic Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,480.0,No Special Classes,19-01-2023,433.0,235.0,198.0,15.6,Not applicable,,Not applicable,,Not under a federation,,10072734.0,,Not applicable,26-04-2023,11-03-2024,Greenfield Road,,Https://Stmarysrcp.Medway.Sch.Uk/,Gillingham,Kent,ME7 1YH,www.stmarysrcp.medway.sch.uk/,1634855783.0,Mr,Joseph,Pomeroy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Gillingham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,577917.0,168572.0,Medway 010,Medway 010B,,,,,Good,South-East England and South London,,44069382.0,,Not applicable,Not applicable,,,E02003323,E01016032,61.0, +118908,887,Medway,5436,St John Fisher Catholic Comprehensive School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,,Not applicable,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Non-selective,1185.0,No Special Classes,19-01-2023,1024.0,550.0,474.0,30.1,Not applicable,,Not applicable,,Not under a federation,,10006189.0,,Not applicable,22-05-2019,23-05-2024,City Way,,,Rochester,,ME1 2FA,http://www.stjohnfisher.school,1634543123.0,Mrs,Dympna,Lennon,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575287.0,167305.0,Medway 021,Medway 021A,,,,,Good,South-East England and South London,United Kingdom,44082945.0,,Not applicable,Not applicable,,,E02003334,E01016020,262.0, +118948,887,Medway,6000,"King's School, Rochester",Other independent school,Independent schools,Open,Not applicable,01-01-1909,Not applicable,,Not applicable,3.0,18,Boarding school,Has Nursery Classes,Has a sixth form,Mixed,Church of England,Church of England,Not applicable,Non-selective,800.0,No Special Classes,20-01-2022,660.0,384.0,276.0,0.0,Not applicable,,Not applicable,,Not applicable,,10003660.0,,Not applicable,,06-06-2024,Satis House,Boley Hill,,Rochester,Kent,ME1 1TE,www.kings-rochester.co.uk,1634888555.0,Mr,Benjamin,Charles,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574095.0,168504.0,Medway 014,Medway 014C,ISI,4.0,89.0,The Governors of King's School,,South-East England and South London,,44026826.0,,Not applicable,Not applicable,,,E02003327,E01016130,0.0, +118979,887,Medway,6001,Bryony School,Other independent school,Independent schools,Open,Not applicable,02-04-1958,Not applicable,,Not applicable,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Non-selective,232.0,No Special Classes,20-01-2022,128.0,60.0,68.0,0.0,Not applicable,,Not applicable,,Not applicable,,10071117.0,,Not applicable,24-11-2022,28-05-2024,157 Marshall Road,Rainham,Marshall Road,Rainham,,ME8 0AJ,www.bryonyschool.org.uk,1634231511.0,Mrs,Natalie,Gee,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South West,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580169.0,165278.0,Medway 030,Medway 030C,Ofsted,,1.0,Mr & Mrs Edmunds,Good,South-East England and South London,,100061264465.0,,Not applicable,Not applicable,,,E02003343,E01016091,0.0, +118985,887,Medway,6002,St Andrew's School (Rochester),Other independent school,Independent schools,Open,Not applicable,18-03-1958,Not applicable,,Not applicable,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,360.0,No Special Classes,20-01-2022,362.0,172.0,190.0,0.0,Not applicable,,Not applicable,,Not applicable,,10080533.0,,Not applicable,,24-05-2024,24 - 26 Watts Avenue,,,Rochester,Kent,ME1 1SA,www.st-andrews.rochester.sch.uk,1634843479.0,Mrs,Emma,Steinmann-Gilbert,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574044.0,167996.0,Medway 014,Medway 014C,ISI,,8.0,Education Development Trust,,South-East England and South London,,44022882.0,,Not applicable,Not applicable,,,E02003327,E01016130,0.0, +119006,887,Medway,6004,Rochester Independent College,Other independent school,Independent schools,Open,Not applicable,26-09-1986,Not applicable,,Not applicable,11.0,21,Boarding school,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,380.0,No Special Classes,20-01-2022,358.0,197.0,161.0,0.0,Not applicable,,Not applicable,,Not applicable,,10005511.0,,Not applicable,,21-03-2024,254 St Margaret's Banks,,,Rochester,Kent,ME1 1HY,www.rochester-college.org.uk,1634828115.0,Mr,Alistair,Brownlow,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not approved,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574653.0,167978.0,Medway 015,Medway 015F,ISI,43.0,102.0,Dukes Education,,South-East England and South London,United Kingdom,100062373676.0,,Not applicable,Not applicable,,,E02003328,E01035296,0.0, +131527,887,Medway,3760,Burnt Oak Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,01-09-2006,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,453.0,238.0,215.0,40.6,Not applicable,,Not applicable,,Not under a federation,,10075241.0,,Not applicable,18-05-2022,20-05-2024,Richmond Road,,,Gillingham,Kent,ME7 1LS,www.burntoak.medway.sch.uk/,1634334344.0,Mrs,Maureen,Grabski,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Gillingham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,577499.0,169057.0,Medway 009,Medway 009C,,,,,Good,South-East England and South London,,44027271.0,,Not applicable,Not applicable,,,E02003322,E01016037,183.0, +132056,887,Medway,3756,St Mary's Island Church of England (Aided) Primary School,Voluntary aided school,Local authority maintained schools,Open,Not applicable,01-09-1999,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,630.0,Not applicable,19-01-2023,671.0,329.0,342.0,10.3,Not applicable,,Not applicable,,Not under a federation,,10075517.0,,Not applicable,15-11-2023,23-04-2024,Island Way West,St Mary's Island,,Chatham,Kent,ME4 3ST,http://www.st-marys-island-cofe-primary-school.co.uk,1634891050.0,Mrs,Christine,Easton,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Medway,St Mary's Island,Rochester and Strood,(England/Wales) Urban city and town,E06000035,576620.0,170684.0,Medway 007,Medway 007F,,,,,Good,South-East England and South London,,200003623259.0,,Not applicable,Not applicable,,,E02003320,E01035290,69.0, +134904,887,Medway,3759,Fairview Community Primary School,Community school,Local authority maintained schools,Open,Result of Amalgamation,12-09-2005,Not applicable,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,690.0,Not applicable,19-01-2023,668.0,340.0,328.0,7.0,Not applicable,,Not applicable,,Not under a federation,,10071739.0,,Not applicable,02-04-2019,21-05-2024,Drewery Drive,Wigmore,,Gillingham,Kent,ME8 0NU,www.fairviewprimary.co.uk,1634338710.0,Mrs,Karin,Tillett,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Hempstead & Wigmore,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580272.0,164185.0,Medway 030,Medway 030A,,,,,Good,South-East England and South London,,44013431.0,,Not applicable,Not applicable,,,E02003343,E01016085,44.0, +135964,887,Medway,6905,Strood Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2009,Not applicable,,Secondary,11.0,19,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1500.0,No Special Classes,19-01-2023,1273.0,626.0,647.0,26.8,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10027834.0,,Not applicable,02-12-2021,17-05-2024,Carnation Road,Strood,,Rochester,Kent,ME2 2SX,http://www.stroodacademy.org.uk,1634717121.0,Mr,Jon,Richardson,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision and SEN unit,5.0,25.0,5.0,25.0,South East,Medway,Strood West,Rochester and Strood,(England/Wales) Urban city and town,E06000035,571638.0,169261.0,Medway 008,Medway 008D,,,,,Good,South-East England and South London,,100062388557.0,,Not applicable,Not applicable,,,E02003321,E01016155,310.0, +136107,887,Medway,6906,Brompton Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2010,Not applicable,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,Does not apply,None,Not applicable,Non-selective,1400.0,Has Special Classes,19-01-2023,1403.0,736.0,667.0,30.4,Supported by a multi-academy trust,THE UNIVERSITY OF KENT ACADEMIES TRUST,Linked to a sponsor,University of Kent (Brompton Academy),Not applicable,,10030223.0,,Not applicable,22-09-2022,21-05-2024,Marlborough Road,,,Gillingham,Kent,ME7 5HT,http://www.bromptonacademy.org.uk,1634852341.0,Mr,Dan,Walters,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,SpLD - Specific Learning Difficulty,OTH - Other Difficulty/Disability,"SLCN - Speech, language and Communication",,,,,,,,,,,SEN unit,,,94.0,100.0,South East,Medway,Gillingham South,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,576827.0,167905.0,Medway 012,Medway 012C,,,,,Requires improvement,South-East England and South London,,44054862.0,,Not applicable,Not applicable,,,E02003325,E01016045,366.0, +136108,887,Medway,6907,The Victory Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2010,Not applicable,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,None,,Non-selective,1500.0,No Special Classes,19-01-2023,1188.0,598.0,590.0,40.1,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10030225.0,,Not applicable,03-02-2023,07-06-2024,Magpie Hall Road,,,Chatham,Kent,ME4 5JB,http://www.thevictoryacademy.org.uk/,3333602140.0,Mr,Oliver,Owen,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Luton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576368.0,166387.0,Medway 022,Medway 022D,,,,,Good,South-East England and South London,,44051900.0,,Not applicable,Not applicable,,,E02003335,E01016062,428.0, +136313,887,Medway,5445,The Rochester Grammar School,Academy converter,Academies,Open,Academy Converter,01-11-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,1110.0,No Special Classes,19-01-2023,1182.0,9.0,1173.0,7.0,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10032200.0,,Not applicable,18-01-2023,21-05-2024,Maidstone Road,,,Rochester,Kent,ME1 3BY,http://www.rochestergrammar.org.uk,3333602120.0,Mrs,Clare,Brinklow,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574171.0,166566.0,Medway 017,Medway 017C,,,,,Good,South-East England and South London,,44022498.0,,Not applicable,Not applicable,,,E02003330,E01016115,70.0, +136337,887,Medway,4069,Fort Pitt Grammar School,Academy converter,Academies,Open,Academy Converter,01-11-2010,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,800.0,No Special Classes,19-01-2023,895.0,20.0,875.0,11.6,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10032192.0,,Not applicable,05-10-2022,05-03-2024,Fort Pitt Hill,,,Chatham,Kent,ME4 6TJ,http://www.fortpitt.medway.sch.uk,1634842359.0,Ms,Salena,Hirons,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Rochester and Strood,(England/Wales) Urban city and town,E06000035,575049.0,167596.0,Medway 015,Medway 015G,,,,,Outstanding,South-East England and South London,,100062392022.0,,Not applicable,Not applicable,,,E02003328,E01035297,83.0, +136456,887,Medway,4199,Rainham School for Girls,Academy converter,Academies,Open,Academy Converter,01-02-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,Does not apply,Does not apply,Not applicable,Non-selective,1558.0,No Special Classes,19-01-2023,1668.0,36.0,1632.0,17.6,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10032961.0,,Not applicable,21-04-2022,20-05-2024,Derwent Way,Rainham,,Gillingham,Kent,ME8 0BX,http://www.rainhamgirls-tkat.org/,1634362746.0,Mrs,Vicki,Shaw,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South West,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580560.0,165785.0,Medway 029,Medway 029B,,,,,Good,South-East England and South London,,100062395978.0,,Not applicable,Not applicable,,,E02003342,E01016088,243.0, +136594,887,Medway,4068,Holcombe Grammar School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,Does not apply,Does not apply,Not applicable,Selective,1000.0,No Special Classes,19-01-2023,1067.0,955.0,112.0,9.9,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10033242.0,,Not applicable,24-04-2018,07-06-2024,Holcombe,Maidstone Road,,Chatham,Kent,ME4 6JB,http://www.holcombegrammar.org.uk,3333602130.0,Mr,Lee,Preston,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575486.0,166035.0,Medway 027,Medway 027A,,,,,Good,South-East England and South London,,100062392018.0,,Not applicable,Not applicable,,,E02003340,E01016025,72.0, +136662,887,Medway,4530,Sir Joseph Williamson's Mathematical School,Academy converter,Academies,Open,Academy Converter,01-04-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Boys,None,Does not apply,Not applicable,Selective,1280.0,No Special Classes,19-01-2023,1479.0,1386.0,93.0,8.7,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10033431.0,,Not applicable,22-03-2023,07-06-2024,Maidstone Road,,,Rochester,Kent,ME1 3EL,http://www.sjwms.org.uk,1634844008.0,Mr,Eliot,Hodges,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574020.0,166430.0,Medway 024,Medway 024C,,,,,Outstanding,South-East England and South London,,44022499.0,,Not applicable,Not applicable,,,E02003337,E01016128,92.0, +136859,887,Medway,2588,Cliffe Woods Primary School,Academy converter,Academies,Open,Academy Converter,01-07-2011,,,Primary,4.0,11,No boarders,No Nursery Classes,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,415.0,197.0,218.0,5.1,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10034067.0,,Not applicable,18-03-2015,07-05-2024,View Road,Cliffe Woods,,Rochester,Kent,ME3 8UJ,www.cliffewoods.medway.sch.uk,1634220822.0,Mrs,Karen,Connolly,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood Rural,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,574188.0,173690.0,Medway 002,Medway 002C,,,,,Outstanding,South-East England and South London,,44030622.0,,Not applicable,Not applicable,,,E02003315,E01016145,21.0, +136864,887,Medway,5420,Rainham Mark Grammar School,Academy converter,Academies,Open,Academy Converter,01-07-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Selective,1242.0,No Special Classes,19-01-2023,1567.0,839.0,728.0,7.3,Supported by a multi-academy trust,RMET,Linked to a sponsor,RMET (Rainham Mark Education Trust),Not applicable,,10034121.0,,Not applicable,25-05-2022,29-05-2024,Pump Lane,Rainham,,Gillingham,Kent,ME8 7AJ,http://rainhammark.com/,1634364151.0,Mrs,Agnes,Hart,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580729.0,166755.0,Medway 023,Medway 023C,,,,,Good,South-East England and South London,,44096394.0,,Not applicable,Not applicable,,,E02003336,E01016166,88.0, +137119,887,Medway,4000,The Hundred of Hoo Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2011,Not applicable,,All-through,4.0,19,No boarders,Not applicable,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1900.0,Not applicable,19-01-2023,1702.0,844.0,858.0,22.3,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10034985.0,,Not applicable,05-07-2018,08-05-2024,Main Road,Hoo,,Rochester,Kent,ME3 9HH,https://www.hundredofhooacademy.org.uk/,1634251443.0,Mr,Carl,Guerin-Hassett,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,SEN unit,,,43.0,48.0,South East,Medway,Hoo St Werburgh & High Halstow,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,577407.0,172277.0,Medway 003,Medway 003D,,,,,Good,South-East England and South London,,200000900015.0,,Not applicable,Not applicable,,,E02003316,E01016077,353.0, +137376,887,Medway,5451,The Thomas Aveling School,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,Does not apply,Not applicable,Non-selective,1204.0,No Special Classes,19-01-2023,1205.0,599.0,606.0,25.1,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10035084.0,,Not applicable,14-09-2022,14-05-2024,Arethusa Road,,,Rochester,Kent,ME1 2UW,http://www.thomasaveling.co.uk,1634844809.0,Mr,Paul,Jackson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,,,,,,,,,,,,,Resourced provision,20.0,20.0,,,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574327.0,165761.0,Medway 026,Medway 026B,,,,,Good,South-East England and South London,,44018933.0,,Not applicable,Not applicable,,,E02003339,E01016120,259.0, +137389,887,Medway,5429,Chatham Grammar,Academy converter,Academies,Open,Academy Converter,01-09-2011,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Girls,None,Does not apply,Not applicable,Selective,976.0,No Special Classes,19-01-2023,945.0,18.0,927.0,10.9,Supported by a multi-academy trust,THE UNIVERSITY OF KENT ACADEMIES TRUST,Linked to a sponsor,University of Kent (Brompton Academy),Not applicable,,10035160.0,,Not applicable,18-10-2023,22-05-2024,Rainham Road,,,Chatham,Kent,ME5 7EH,http://www.chathamgirlsgrammar.medway.sch.uk/,1634851262.0,Ms,Wendy,Walters,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Luton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,577460.0,166933.0,Medway 020,Medway 020A,,,,,Good,South-East England and South London,,200000899518.0,,Not applicable,Not applicable,,,E02003333,E01016061,80.0, +137990,887,Medway,2421,High Halstow Primary Academy,Academy converter,Academies,Open,Academy Converter,01-04-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,211.0,111.0,100.0,9.5,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10036980.0,,Not applicable,21-06-2023,14-03-2024,Harrison Drive,High Halstow,,Rochester,Kent,ME3 8TF,http://www.highhalstowprimaryacademy.org.uk,1634251098.0,Mrs,Gemma,Stangroom,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Hoo St Werburgh & High Halstow,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,578093.0,175447.0,Medway 003,Medway 003A,,,,,Outstanding,South-East England and South London,,44083531.0,,Not applicable,Not applicable,,,E02003316,E01016073,20.0, +138182,887,Medway,2600,All Faiths Children's Academy,Academy converter,Academies,Open,Academy Converter,01-06-2012,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,Has Special Classes,19-01-2023,226.0,101.0,125.0,28.4,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10037460.0,,Not applicable,29-01-2020,23-04-2024,Gun Lane,Strood,,Rochester,Kent,ME2 4UF,www.allfaithschildrensacademy.org.uk,3333602100.0,Mrs,Kirstie,Jones,Acting Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,HI - Hearing Impairment,,,,,,,,,,,,,Resourced provision and SEN unit,15.0,15.0,15.0,15.0,South East,Medway,Strood North & Frindsbury,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573564.0,169506.0,Medway 006,Medway 006E,,,,,Good,South-East England and South London,,200000896787.0,,Not applicable,Not applicable,,,E02003319,E01016141,64.0, +138328,887,Medway,2209,Chattenden Primary School,Academy converter,Academies,Open,Academy Converter,01-07-2012,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,No Special Classes,19-01-2023,209.0,101.0,108.0,34.8,Supported by a multi-academy trust,PENINSULA GATEWAY ACADEMY TRUST,-,,Not applicable,,10037769.0,,Not applicable,06-11-2018,03-05-2024,Chattenden Lane,Chattenden,,Rochester,Kent,ME3 8LF,http://www.chattenden.medway.sch.uk/,1634250861.0,Miss,Julie,North,Principal,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Hoo St Werburgh & High Halstow,Rochester and Strood,(England/Wales) Rural village,E06000035,575792.0,171843.0,Medway 003,Medway 003E,,,,,Good,South-East England and South London,,200000909509.0,,Not applicable,Not applicable,,,E02003316,E01016143,72.0, +138510,887,Medway,2001,Phoenix Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2012,,,Primary,5.0,11,,Not applicable,Not applicable,Mixed,None,None,Not applicable,Not applicable,360.0,Not applicable,19-01-2023,359.0,179.0,180.0,49.3,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10038455.0,,Not applicable,21-06-2023,25-04-2024,Glencoe Road,,,Chatham,Kent,ME4 5QD,www.phoenixprimary.com,1634829009.0,Mrs,Melissa,Ireland-Hubbert,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Chatham Central & Brompton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576075.0,166910.0,Medway 022,Medway 022A,,,,,Good,South-East England and South London,,44051781.0,,Not applicable,Not applicable,,,E02003335,E01016018,176.0, +138511,887,Medway,4001,The Robert Napier School,Academy sponsor led,Academies,Open,New Provision,01-09-2012,,,Secondary,11.0,18,No boarders,Not applicable,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1080.0,Not applicable,19-01-2023,1037.0,561.0,476.0,43.0,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10038456.0,,Not applicable,31-01-2019,21-05-2024,Third Avenue,,,Gillingham,Kent,ME7 2LX,http://www.robertnapier.org.uk/,1634851157.0,Mrs,Jenny,Tomkins,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Medway,Watling,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,578331.0,167405.0,Medway 019,Medway 019D,,,,,Good,South-East England and South London,,44076749.0,,Not applicable,Not applicable,,,E02003332,E01016179,407.0, +138974,887,Medway,2002,St James Church of England Primary Academy,Academy sponsor led,Academies,Open,New Provision,01-12-2012,,,Primary,3.0,11,,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,None,Diocese of Rochester,Not applicable,210.0,Not applicable,19-01-2023,199.0,104.0,95.0,31.7,Supported by a multi-academy trust,MEDWAY ANGLICAN SCHOOLS TRUST,-,,Not applicable,,10039506.0,,Not applicable,24-05-2023,23-02-2024,High Street,Isle of Grain,,Rochester,Kent,ME3 0BS,http://www.stjamesisleofgrain.org.uk,1634270341.0,Miss,Fay,Cordingley,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,All Saints,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,588751.0,176727.0,Medway 001,Medway 001B,,,,,Good,South-East England and South London,,44004798.0,,Not applicable,Not applicable,,,E02003314,E01016071,58.0, +139493,887,Medway,2412,The Academy of Woodlands,Academy converter,Academies,Open,Academy Converter,01-04-2013,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,758.0,386.0,372.0,32.1,Supported by a multi-academy trust,RIVERMEAD INCLUSIVE TRUST,Linked to a sponsor,Rivermead Inclusive Trust,Not applicable,,10041035.0,,Not applicable,11-12-2019,01-05-2024,Woodlands Road,,,Gillingham,Kent,ME7 2DU,www.theacademyofwoodlands.co.uk,3000658200.0,Mrs,Chloe,Brown,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Watling,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,578807.0,167818.0,Medway 013,Medway 013C,,,,,Good,South-East England and South London,,100062394529.0,,Not applicable,Not applicable,,,E02003326,E01016043,243.0, +139927,887,Medway,2003,Kingfisher Community Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,219.0,103.0,116.0,40.6,Supported by a multi-academy trust,THE GRIFFIN SCHOOLS TRUST,Linked to a sponsor,The Griffin Schools Trust,Not applicable,,10042689.0,,Not applicable,12-09-2019,28-05-2024,Kingfisher Drive,Princes Park,Walderslade,Chatham,Kent,ME5 7NX,www.kingfisher-gst.org,1634661540.0,Ms,Fiona,Armstrong,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Princes Park,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,577072.0,165267.0,Medway 031,Medway 031E,,,,,Good,South-East England and South London,,100062392674.0,,Not applicable,Not applicable,,,E02003344,E01016084,89.0, +139928,887,Medway,2004,Saxon Way Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,425.0,229.0,196.0,47.2,Supported by a multi-academy trust,THE GRIFFIN SCHOOLS TRUST,Linked to a sponsor,The Griffin Schools Trust,Not applicable,,10042702.0,,Not applicable,22-01-2020,04-06-2024,,Ingram Road,,Gillingham,Kent,ME7 1SJ,http://www.saxonway-gst.org/,1634336720.0,Mrs,Jennifer,Vidler-Ironmonger,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Gillingham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,578169.0,168848.0,Medway 010,Medway 010B,,,,,Good,South-East England and South London,,200000900397.0,,Not applicable,Not applicable,,,E02003323,E01016032,200.0, +140040,887,Medway,2006,Oasis Academy Skinner Street,Academy sponsor led,Academies,Open,New Provision,01-09-2013,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,410.0,198.0,212.0,51.7,Supported by a multi-academy trust,OASIS COMMUNITY LEARNING,Linked to a sponsor,Oasis Community Learning,Not applicable,,10042952.0,,Not applicable,22-09-2021,19-04-2024,Skinner Street,,,Gillingham,Kent,ME7 1LG,www.oasisacademyskinnerstreet.org/,1634850213.0,Mrs,Victoria,Richmond,Interim Principal,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Gillingham South,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,577257.0,168594.0,Medway 012,Medway 012A,,,,,Good,South-East England and South London,,100062394010.0,,Not applicable,Not applicable,,,E02003325,E01016031,212.0, +140186,887,Medway,2007,Lordswood School,Academy sponsor led,Academies,Open,New Provision,01-11-2013,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,410.0,220.0,190.0,26.6,Supported by a multi-academy trust,THE GRIFFIN SCHOOLS TRUST,Linked to a sponsor,The Griffin Schools Trust,Not applicable,,10043317.0,,Not applicable,16-01-2019,28-05-2024,Lordswood Lane,,,Chatham,Kent,ME5 8NN,www.lordswood-gst.org,1634336767.0,Mrs,Jayne,Lusinski,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Lordswood & Walderslade,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576856.0,163264.0,Medway 038,Medway 038D,,,,,Good,South-East England and South London,,200000909500.0,,Not applicable,Not applicable,,,E02003351,E01016060,109.0, +140215,887,Medway,2008,New Horizons Children's Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2014,,,Primary,4.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,,Not applicable,630.0,Not applicable,19-01-2023,639.0,318.0,321.0,19.4,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10047226.0,,Not applicable,25-05-2022,03-05-2024,Park Crescent,,,Chatham,Kent,ME4 6NR,http://www.newhorizonschildrensacademy.org.uk,3333602115.0,Mr,Cormac,Murphy,Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Chatham Central & Brompton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575624.0,165866.0,Medway 027,Medway 027A,,,,,Good,South-East England and South London,,44036212.0,,Not applicable,Not applicable,,,E02003340,E01016025,124.0, +140606,887,Medway,2009,"Gordons Children's Academy, Junior",Academy sponsor led,Academies,Open,New Provision,01-03-2014,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,400.0,Not applicable,19-01-2023,336.0,180.0,156.0,22.9,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10044931.0,,Not applicable,28-09-2022,16-01-2024,Gordon Road,Strood,,Rochester,Kent,ME2 3HQ,https://www.gordonchildrensacademy.org.uk/,3333602110.0,Mrs,Kirstie,Jones,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood North & Frindsbury,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573169.0,169601.0,Medway 006,Medway 006E,,,,,Good,South-East England and South London,United Kingdom,100062388599.0,,Not applicable,Not applicable,,,E02003319,E01016141,77.0, +140607,887,Medway,2010,"Gordons Children's Academy, Infant",Academy sponsor led,Academies,Open,New Provision,01-03-2014,,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,189.0,Not applicable,19-01-2023,164.0,83.0,81.0,25.0,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10044932.0,,Not applicable,28-09-2022,16-01-2024,Gordon Road,Strood,,Rochester,Kent,ME2 3HQ,www.gordonchildrensacademy.org.uk,3333602110.0,Mrs,Kirstie,Jones,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood North & Frindsbury,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573169.0,169601.0,Medway 006,Medway 006E,,,,,Good,South-East England and South London,,100062388599.0,,Not applicable,Not applicable,,,E02003319,E01016141,41.0, +140989,887,Medway,2011,Warren Wood Primary School,Academy sponsor led,Academies,Open,New Provision,01-07-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Not applicable,411.0,Has Special Classes,19-01-2023,486.0,258.0,228.0,37.8,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10046284.0,,Not applicable,16-10-2019,16-04-2024,Arethusa Road,,,Rochester,Kent,ME1 2UR,www.warrenwoodprimary.co.uk,1634401401.0,Mrs,Lucinda,Woodroof,Acting Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,"SLCN - Speech, language and Communication",,,,,,,,,,,,,SEN unit,,,30.0,34.0,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574546.0,165725.0,Medway 026,Medway 026B,,,,,Good,South-East England and South London,,44020102.0,,Not applicable,Not applicable,,,E02003339,E01016120,178.0, +141199,887,Medway,2012,Napier Community Primary and Nursery Academy,Academy sponsor led,Academies,Open,New Provision,01-09-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,585.0,282.0,303.0,34.9,Supported by a multi-academy trust,THE KEMNAL ACADEMIES TRUST,Linked to a sponsor,The Kemnal Academies Trust,Not applicable,,10046908.0,,Not applicable,04-12-2019,11-04-2024,Napier Community Primary and Nursery Academy,Napier Road,,Gillingham,Kent,ME7 4HG,www.napierprimary.org.uk,1634574920.0,Mr,Ciaran,Mc Cann,Executive Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Gillingham South,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,577675.0,167833.0,Medway 016,Medway 016B,,,,,Good,South-East England and South London,,200000909503.0,,Not applicable,Not applicable,,,E02003329,E01016046,204.0, +141224,887,Medway,2013,Cuxton Community Junior School,Academy sponsor led,Academies,Open,New Provision,01-09-2014,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,240.0,Not applicable,19-01-2023,228.0,112.0,116.0,14.9,Supported by a multi-academy trust,THE PRIMARY FIRST TRUST,Linked to a sponsor,The Primary First Trust,Not applicable,,10047013.0,,Not applicable,07-07-2022,21-05-2024,Bush Road,Cuxton,,Rochester,Kent,ME2 1EY,www.cuxtonschools.co.uk,1634337720.0,Mrs,Charlotte,Aldham Breary,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,"Cuxton, Halling & Riverside",Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,570725.0,166676.0,Medway 028,Medway 028C,,,,,Good,South-East England and South London,,44027917.0,,Not applicable,Not applicable,,,E02003341,E01016028,34.0, +141276,887,Medway,2208,Cuxton Community Infant School,Academy converter,Academies,Open,Academy Converter,01-09-2014,,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,180.0,No Special Classes,19-01-2023,180.0,97.0,83.0,8.3,Supported by a multi-academy trust,THE PRIMARY FIRST TRUST,Linked to a sponsor,The Primary First Trust,Not applicable,,10047167.0,,Not applicable,30-03-2022,21-05-2024,Bush Road,Cuxton,,Rochester,Kent,ME2 1EY,www.cuxtonschools.co.uk,1634337720.0,Mrs,Charlotte,Aldham Breary,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,"Cuxton, Halling & Riverside",Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,570781.0,166686.0,Medway 028,Medway 028C,,,,,Good,South-East England and South London,,44027917.0,,Not applicable,Not applicable,,,E02003341,E01016028,15.0, +141466,887,Medway,5457,The Howard School,Academy converter,Academies,Open,Academy Converter,01-10-2014,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,Not applicable,Non-selective,1725.0,No Special Classes,19-01-2023,1508.0,1486.0,22.0,15.3,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10047641.0,,Not applicable,24-11-2021,30-04-2024,Derwent Way,Rainham,,Gillingham,Kent,ME8 0BX,https://www.thehoward-that.org.uk/,1634388765.0,Mr,Jasbinder,Johal,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South West,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580562.0,165928.0,Medway 029,Medway 029B,,,,,Good,South-East England and South London,,100062395978.0,,Not applicable,Not applicable,,,E02003342,E01016088,193.0, +141467,887,Medway,2646,Brompton-Westbrook Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,450.0,227.0,223.0,33.5,Supported by a multi-academy trust,THE WESTBROOK TRUST,Linked to a sponsor,The Westbrook Trust,Not applicable,,10047618.0,,Not applicable,22-01-2019,21-05-2024,Kings Bastion,Brompton,,Gillingham,Kent,ME7 5DQ,http://www.bromptonwestbrook.medway.sch.uk,1634844152.0,Mrs,Sue,Mason,Head Teacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Medway,Chatham Central & Brompton,Rochester and Strood,(England/Wales) Urban city and town,E06000035,576422.0,168177.0,Medway 015,Medway 015E,,,,,Good,South-East England and South London,,44056069.0,,Not applicable,Not applicable,,,E02003328,E01016111,146.0, +141553,887,Medway,2194,Peninsula East Primary Academy,Academy converter,Academies,Open,Academy Converter,01-11-2014,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,250.0,No Special Classes,19-01-2023,248.0,127.0,121.0,26.9,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10047879.0,,Not applicable,15-01-2020,23-05-2024,Avery Way,Allhallows,,Rochester,,ME3 9HR,www.pepa.org.uk,1634270428.0,Mrs,Lorna,Rimmer,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Medway,All Saints,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,583168.0,176220.0,Medway 001,Medway 001A,,,,,Good,South-East England and South London,United Kingdom,44108207.0,,Not applicable,Not applicable,,,E02003314,E01016070,66.0, +142137,887,Medway,3093,All Saints Church of England Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2015,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,379.0,No Special Classes,19-01-2023,353.0,176.0,177.0,33.2,Supported by a multi-academy trust,MEDWAY ANGLICAN SCHOOLS TRUST,-,,Not applicable,,10053760.0,,Not applicable,21-02-2024,20-05-2024,Magpie Hall Road,,,Chatham,Kent,ME4 5JY,http://www.allsaints.medway.sch.uk,1634338922.0,,Joanne,Strachan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Chatham Central & Brompton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576479.0,167253.0,Medway 015,Medway 015C,,,,,Good,South-East England and South London,,44054186.0,,Not applicable,Not applicable,,,E02003328,E01016019,111.0, +142157,887,Medway,3095,St John's Church of England Infant School,Academy converter,Academies,Open,Academy Converter,01-09-2015,,,Primary,5.0,7,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,90.0,No Special Classes,19-01-2023,46.0,21.0,25.0,34.8,Supported by a multi-academy trust,MEDWAY ANGLICAN SCHOOLS TRUST,-,,Not applicable,,10053759.0,,Not applicable,13-06-2018,12-04-2024,4 New Street,,,Chatham,Kent,ME4 6RH,https://www.stjohns.medway.sch.uk/,1634844135.0,Miss,J,Strachan,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575450.0,167393.0,Medway 015,Medway 015B,,,,,Good,South-East England and South London,,44029362.0,,Not applicable,Not applicable,,,E02003328,E01016017,16.0, +142160,887,Medway,3195,St Margaret's Church of England Junior School,Academy converter,Academies,Open,Academy Converter,01-09-2015,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,363.0,No Special Classes,19-01-2023,364.0,189.0,175.0,19.0,Supported by a multi-academy trust,MEDWAY ANGLICAN SCHOOLS TRUST,-,,Not applicable,,10053758.0,,Not applicable,21-09-2023,26-03-2024,Orchard Street,Rainham,,Gillingham,Kent,ME8 9AE,http://www.stmargaretsonline.net,1634230998.0,Mr,Lenny,Williams,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South West,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,581405.0,165749.0,Medway 029,Medway 029C,,,,,Good,South-East England and South London,,200000901856.0,,Not applicable,Not applicable,,,E02003342,E01016089,69.0, +142393,887,Medway,2014,Twydall Primary School and Nursery,Academy sponsor led,Academies,Open,New Provision,01-02-2016,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,255.0,Not applicable,19-01-2023,304.0,169.0,135.0,42.7,Supported by a multi-academy trust,RMET,Linked to a sponsor,RMET (Rainham Mark Education Trust),Not applicable,,10054150.0,,,17-05-2023,29-05-2024,Twydall Lane,,,Gillingham,Kent,ME8 6JS,www.twydallprimary.org.uk,1634231761.0,Mrs,Louise,Hardie,Headteacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,PD - Physical Disability,,,,,,,,,,,,,Resourced provision,7.0,8.0,,,South East,Medway,Twydall,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579908.0,166888.0,Medway 018,Medway 018B,,,,,Good,South-East England and South London,,44087223.0,,Not applicable,Not applicable,,,E02003331,E01016161,120.0, +142394,887,Medway,2015,Temple Mill Primary School,Academy sponsor led,Academies,Open,New Provision,01-12-2015,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,247.0,126.0,121.0,21.7,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10054151.0,,,17-10-2018,11-04-2024,Cliffe Road,Strood,,Rochester,Kent,ME2 3NL,https://www.templemill-that.org.uk/,1634629079.0,Mrs,Lisa,Lewis,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood Rural,Rochester and Strood,(England/Wales) Urban city and town,E06000035,573642.0,170455.0,Medway 004,Medway 004C,,,,,Good,South-East England and South London,,100062388732.0,,Not applicable,Not applicable,,,E02003317,E01016148,51.0, +142399,887,Medway,2016,Byron Primary School,Academy sponsor led,Academies,Open,New Provision,01-01-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,None,Not applicable,Not applicable,525.0,Not applicable,19-01-2023,520.0,251.0,269.0,27.5,Supported by a multi-academy trust,THE WESTBROOK TRUST,Linked to a sponsor,The Westbrook Trust,Not applicable,,10054156.0,,,15-06-2022,21-05-2024,Byron Road,,,Gillingham,Kent,ME7 5XX,,1634852981.0,Mr,Jon,Carthy,Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Gillingham South,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,577482.0,167330.0,Medway 016,Medway 016C,,,,,Requires improvement,South-East England and South London,,44067058.0,,Not applicable,Not applicable,,,E02003329,E01016047,143.0, +142568,887,Medway,6010,The GFC School,Other independent school,Independent schools,Open,New Provision,07-12-2015,,,Not applicable,11.0,16,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,None,Not applicable,Selective,50.0,Not applicable,20-01-2022,28.0,20.0,8.0,0.0,Not applicable,,Not applicable,,Not applicable,,10066496.0,,,02-05-2019,15-04-2024,Priestfield Stadium,Redfern Avenue,,Gillingham,Kent,ME7 4DD,www.thegfcschool.co.uk,1634623420.0,Mrs,Sue,Wade,Head of School,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not approved,SpLD - Specific Learning Difficulty,"SEMH - Social, Emotional and Mental Health",MLD - Moderate Learning Difficulty,,,,,,,,,,,SEN unit,,,15.0,15.0,South East,Medway,Watling,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,578373.0,168139.0,Medway 013,Medway 013B,Ofsted,16.0,12.0,Paul Scally,Good,South-East England and South London,,44007001.0,,Not applicable,Not applicable,,,E02003326,E01016042,0.0, +142817,887,Medway,2017,Cedar Children's Academy,Academy sponsor led,Academies,Open,New Provision,01-06-2016,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,630.0,Not applicable,19-01-2023,668.0,352.0,316.0,26.9,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10056531.0,,,09-05-2019,08-01-2024,Cedar Road,Strood,,Rochester,Kent,ME2 2JP,www.cedarchildrensacademy.org.uk,3333602105.0,Ms,Tracey,Baillie,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood West,Rochester and Strood,(England/Wales) Urban city and town,E06000035,572439.0,168467.0,Medway 011,Medway 011D,,,,,Good,South-East England and South London,,200000895826.0,,Not applicable,Not applicable,,,E02003324,E01016157,180.0, +143261,887,Medway,2203,Walderslade Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2016,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,240.0,No Special Classes,19-01-2023,235.0,110.0,125.0,16.6,Supported by a multi-academy trust,RIVERMEAD INCLUSIVE TRUST,Linked to a sponsor,Rivermead Inclusive Trust,Not applicable,,10057635.0,,Not applicable,21-11-2018,30-04-2024,Dargets Road,Walderslade,,Chatham,Kent,ME5 8BJ,www.walderslade-pri.medway.sch.uk/,1634337766.0,Mrs,Amy,Rowley-Jones,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Lordswood & Walderslade,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576195.0,163447.0,Medway 037,Medway 037B,,,,,Good,South-East England and South London,,44045350.0,,Not applicable,Not applicable,,,E02003350,E01016169,39.0, +143262,887,Medway,2213,Hoo St Werburgh Primary School and the Marlborough,Academy converter,Academies,Open,Academy Converter,01-09-2016,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,525.0,Has Special Classes,19-01-2023,573.0,308.0,265.0,19.9,Supported by a multi-academy trust,RIVERMEAD INCLUSIVE TRUST,Linked to a sponsor,Rivermead Inclusive Trust,Not applicable,,10057859.0,,Not applicable,06-03-2024,22-05-2024,Pottery Road,Hoo St Werburgh,,Rochester,Kent,ME3 9BS,www.hoo-st-werburgh.medway.sch.uk,1634338040.0,Mr,Simon,McLean,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,,Resourced provision and SEN unit,80.0,80.0,80.0,80.0,South East,Medway,Hoo St Werburgh & High Halstow,Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,577642.0,172283.0,Medway 003,Medway 003D,,,,,Good,South-East England and South London,,200000900025.0,,Not applicable,Not applicable,,,E02003316,E01016077,114.0, +143458,887,Medway,2684,Deanwood Primary School,Academy converter,Academies,Open,Academy Converter,01-10-2016,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,210.0,Not applicable,19-01-2023,207.0,102.0,105.0,18.4,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10061378.0,,Not applicable,02-11-2018,23-05-2024,"Deanwood Primary School, Long Catlis Road",Parkwood,,Gillingham,Kent,ME8 9TX,https://www.deanwood-that.org.uk/,1634231901.0,Mrs,Jane,Wright,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South East,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,580615.0,163584.0,Medway 036,Medway 036B,,,,,Good,South-East England and South London,,200000909507.0,,Not applicable,Not applicable,,,E02003349,E01016099,38.0, +143832,887,Medway,3758,The Pilgrim School (A Church of England Primary With Nursery),Academy converter,Academies,Open,Academy Converter,01-12-2016,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,Not applicable,19-01-2023,229.0,102.0,127.0,18.6,Supported by a multi-academy trust,THE PILGRIM MULTI ACADEMY TRUST,-,,Not applicable,,10062013.0,,Not applicable,,01-06-2024,Warwick Crescent,Borstal,,Rochester,Kent,ME1 3LF,www.thepilgrimschool.co.uk,16343975555.0,Mrs,Alison,Mepsted,,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,572771.0,166724.0,Medway 024,Medway 024B,,,,,,South-East England and South London,,44010044.0,,Not applicable,Not applicable,,,E02003337,E01016127,39.0, +143880,887,Medway,2214,Balfour Junior School,Academy converter,Academies,Open,Academy Converter,01-01-2017,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,480.0,No Special Classes,19-01-2023,480.0,257.0,223.0,29.0,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10062374.0,,Not applicable,05-12-2018,23-04-2024,Balfour Road,,,Chatham,Kent,ME4 6QX,www.balfourjuniorschool.org.uk,1634843833.0,Mrs,Zoe,Mayston,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Fort Pitt,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575169.0,166802.0,Medway 021,Medway 021D,,,,,Good,South-East England and South London,,44038129.0,,Not applicable,Not applicable,,,E02003334,E01016125,139.0, +143909,887,Medway,2018,Wayfield Primary School,Academy converter,Academies,Open,Fresh Start,01-09-2016,,,Primary,2.0,11,,Has Nursery Classes,Not applicable,Mixed,None,Does not apply,,,360.0,Not applicable,19-01-2023,392.0,212.0,180.0,39.0,Supported by a multi-academy trust,THE PRIMARY FIRST TRUST,Linked to a sponsor,The Primary First Trust,Not applicable,,10062232.0,,,22-05-2019,25-04-2024,Wayfield Road,,,Chatham,Kent,ME5 0HH,www.wayfield.medway.sch.uk,3000658230.0,Miss,Ria,Henry,Head Teacher,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Wayfield & Weeds Wood,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576192.0,165414.0,Medway 027,Medway 027E,,,,,Good,South-East England and South London,,,,Not applicable,Not applicable,,,E02003340,E01016068,135.0, +144132,887,Medway,2592,Thames View Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2017,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,450.0,No Special Classes,19-01-2023,449.0,224.0,225.0,18.0,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10063038.0,,Not applicable,17-05-2023,23-05-2024,Bloors Lane,Rainham,,Gillingham,Kent,ME8 7DX,https://www.thamesview-that.org.uk/,1634629080.0,Mrs,Leanna,Rogers,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,581008.0,166754.0,Medway 023,Medway 023C,,,,,Good,South-East England and South London,,44103856.0,,Not applicable,Not applicable,,,E02003336,E01016166,74.0, +144133,887,Medway,2479,St Margaret's Infant School,Academy converter,Academies,Open,Academy Converter,01-04-2017,,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,300.0,155.0,145.0,10.6,Supported by a multi-academy trust,THE WESTBROOK TRUST,Linked to a sponsor,The Westbrook Trust,Not applicable,,10063037.0,,Not applicable,13-02-2020,21-05-2024,"St Margaret's Infant School, Orchard Street",Orchard Street,Rainham,Gillingham,Kent,ME8 9AE,http://www.stmargaretsinf.medway.sch.uk,1634231327.0,Mrs,Paula,Fewtrell,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South West,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,581405.0,165749.0,Medway 029,Medway 029C,,,,,Good,South-East England and South London,,100062396976.0,,Not applicable,Not applicable,,,E02003342,E01016089,30.0, +144134,887,Medway,1107,The Rowans,Academy alternative provision converter,Academies,Open,Academy Converter,01-06-2017,,,Not applicable,5.0,16,No boarders,Not applicable,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,65.0,Not applicable,19-01-2023,26.0,18.0,8.0,65.4,Supported by a multi-academy trust,THE INSPIRING CHANGE MULTI-ACADEMY TRUST,-,,Not applicable,,10063925.0,,Not applicable,10-11-2021,19-03-2024,Silverbank,Churchill Avenue,,Chatham,Kent,ME5 0LB,www.therowans.org,1634338803.0,Mrs,Fiona,May,Headteacher,Not applicable,,,,Not applicable,,Does not have child care facilities,PRU Does have Provision for SEN,PRU Does have EBD provision,66.0,PRU offers full time provision,Does not offer tuition by another provider,Not applicable,"SEMH - Social, Emotional and Mental Health",,,,,,,,,,,,,Resourced provision,65.0,65.0,,,South East,Medway,Wayfield & Weeds Wood,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576044.0,164885.0,Medway 027,Medway 027E,,,,,Outstanding,South-East England and South London,,44112005.0,,Not applicable,Not applicable,,,E02003340,E01016068,17.0, +144135,887,Medway,3757,Riverside Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2017,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,390.0,Not applicable,19-01-2023,406.0,220.0,186.0,32.5,Supported by a multi-academy trust,RMET,Linked to a sponsor,RMET (Rainham Mark Education Trust),Not applicable,,10063036.0,,Not applicable,13-11-2019,29-05-2024,St Edmunds Way,Rainham,,Gillingham,Kent,ME8 8ET,www.riverside.medway.sch.uk/,1634623500.0,Mrs,Helen,Robson,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,VI - Visual Impairment,ASD - Autistic Spectrum Disorder,,,,,,,,,,,,Resourced provision,17.0,15.0,,,South East,Medway,Rainham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,582416.0,166196.0,Medway 025,Medway 025C,,,,,Good,South-East England and South London,,44110167.0,,Not applicable,Not applicable,,,E02003338,E01016097,117.0, +144423,887,Medway,2396,Barnsole Primary School,Academy converter,Academies,Open,Academy Converter,01-05-2017,,,Primary,2.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,19-01-2023,671.0,336.0,335.0,25.9,Supported by a multi-academy trust,MARITIME ACADEMY TRUST,Linked to a sponsor,Maritime Academy Trust,Not applicable,,10063725.0,,Not applicable,29-02-2024,22-05-2024,Barnsole Road,,,Gillingham,Kent,ME7 2JG,www.barnsoleprimary.medway.sch.uk,1634333400.0,Mr,Jonathan,Smales,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Watling,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,578041.0,167976.0,Medway 013,Medway 013D,,,,,Good,South-East England and South London,,44076748.0,,Not applicable,Not applicable,,,E02003326,E01016177,168.0, +144566,887,Medway,2492,Bligh Primary School (Juniors),Academy converter,Academies,Open,Academy Converter,01-10-2017,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,330.0,No Special Classes,19-01-2023,328.0,159.0,169.0,35.7,Supported by a multi-academy trust,MARITIME ACADEMY TRUST,Linked to a sponsor,Maritime Academy Trust,Not applicable,,10064767.0,,Not applicable,22-06-2023,04-06-2024,Bligh Way,Strood,,Rochester,Kent,ME2 2XJ,www.blighprimaryschool.co.uk,1634336220.0,Mr,Christian,Markham,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood West,Rochester and Strood,(England/Wales) Urban city and town,E06000035,571407.0,169082.0,Medway 008,Medway 008D,,,,,Good,South-East England and South London,,100062388554.0,,Not applicable,Not applicable,,,E02003321,E01016155,117.0, +144639,887,Medway,2623,Miers Court Primary School,Academy converter,Academies,Open,Academy Converter,01-08-2017,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,394.0,200.0,194.0,14.2,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10064690.0,,Not applicable,03-11-2021,17-04-2024,Silverspot Close,,,Rainham,Kent,ME8 8JR,https://www.mierscourt-that.org.uk/,1634388943.0,Mrs,Susan,Chapman,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham South East,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,581647.0,165162.0,Medway 032,Medway 032C,,,,,Good,South-East England and South London,,44101008.0,,Not applicable,Not applicable,,,E02003345,E01016104,56.0, +144914,887,Medway,3293,St Margaret's at Troy Town CofE Voluntary Controlled Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2017,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Does not apply,Diocese of Rochester,Not applicable,210.0,No Special Classes,19-01-2023,225.0,119.0,106.0,37.3,Supported by a multi-academy trust,THE PILGRIM MULTI ACADEMY TRUST,-,,Not applicable,,10065034.0,,Not applicable,02-12-2021,05-02-2024,King Street,,,Rochester,Kent,ME1 1YF,http://www.stmargaretsattroytown.co.uk,1634843843.0,Miss,Katie,Willis,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,Not applicable,,,,,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574390.0,167980.0,Medway 014,Medway 014A,,,,,Good,South-East England and South London,,44023621.0,,Not applicable,Not applicable,,,E02003327,E01016117,76.0, +144915,887,Medway,2537,Bligh Primary School (Infants),Academy converter,Academies,Open,Academy Converter,01-10-2017,,,Primary,2.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,325.0,157.0,168.0,27.9,Supported by a multi-academy trust,MARITIME ACADEMY TRUST,Linked to a sponsor,Maritime Academy Trust,Not applicable,,10065035.0,,Not applicable,,04-06-2024,Bligh Way,Strood,,Rochester,Kent,ME2 2XJ,www.blighprimaryschool.co.uk,1634336220.0,Mr,Christian,Markham,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood West,Rochester and Strood,(England/Wales) Urban city and town,E06000035,571407.0,169082.0,Medway 008,Medway 008D,,,,,,South-East England and South London,,100062388554.0,,Not applicable,Not applicable,,,E02003321,E01016155,80.0, +144969,887,Medway,2019,Featherby Junior School,Academy sponsor led,Academies,Open,New Provision,01-09-2017,,,Primary,7.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,360.0,Not applicable,19-01-2023,339.0,165.0,174.0,34.2,Supported by a multi-academy trust,MARITIME ACADEMY TRUST,Linked to a sponsor,Maritime Academy Trust,Not applicable,,10065102.0,,,29-06-2022,07-05-2024,Chilham Road,,Featherby Junior School,Gillingham,Kent,ME8 6BT,www.featherby-jun.medway.sch.uk,1634231984.0,Mrs,Emma,Pape,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Twydall,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579316.0,167170.0,Medway 018,Medway 018D,,,,,Good,South-East England and South London,,44088972.0,,Not applicable,Not applicable,,,E02003331,E01016163,116.0, +145042,887,Medway,2401,Featherby Infant and Nursery School,Academy converter,Academies,Open,Academy Converter,01-09-2017,,,Primary,3.0,7,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,270.0,No Special Classes,19-01-2023,297.0,155.0,142.0,32.1,Supported by a multi-academy trust,MARITIME ACADEMY TRUST,Linked to a sponsor,Maritime Academy Trust,Not applicable,,10065161.0,,Not applicable,17-11-2021,07-05-2024,Allington Road,,,Gillingham,Kent,ME8 6PD,http://www.featherby-inf.medway.sch.uk,1634231072.0,Mrs,Emma,Pape,Head of School,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Twydall,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579330.0,167352.0,Medway 018,Medway 018D,,,,,Good,South-East England and South London,,44088976.0,,Not applicable,Not applicable,,,E02003331,E01016163,93.0, +145112,887,Medway,2020,Maundene School,Academy sponsor led,Academies,Open,New Provision,01-01-2018,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,,Not applicable,Not applicable,420.0,Not applicable,19-01-2023,395.0,187.0,208.0,22.0,Supported by a multi-academy trust,INSPIRE PARTNERSHIP ACADEMY TRUST,Linked to a sponsor,Inspire Partnership Academy Trust,Not applicable,,10065460.0,,,15-06-2022,02-05-2024,Swallow Rise,Walderslade,,Chatham,Kent,ME5 7QB,https://www.maundene.medway.sch.uk/,1634864721.0,Miss,Joanne,Capes,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Princes Park,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576557.0,164396.0,Medway 034,Medway 034A,,,,,Good,South-East England and South London,,44047176.0,,Not applicable,Not applicable,,,E02003347,E01016078,87.0, +145440,887,Medway,2499,Hilltop Primary School,Academy converter,Academies,Open,Academy Converter,01-02-2018,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,448.0,No Special Classes,19-01-2023,436.0,242.0,194.0,14.3,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10066627.0,,Not applicable,29-06-2022,23-04-2024,Hilltop Road,Frindsbury,,Rochester,Kent,ME2 4QN,www.hilltopprimary.co.uk,1634710312.0,Mrs,Ewa,Eddy,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood Rural,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574043.0,170333.0,Medway 004,Medway 004C,,,,,Good,South-East England and South London,,100062389240.0,,Not applicable,Not applicable,,,E02003317,E01016148,61.0, +145926,887,Medway,2021,Elaine Primary School,Academy sponsor led,Academies,Open,Fresh Start,01-05-2018,,,Primary,3.0,11,No boarders,Has Nursery Classes,Not applicable,Mixed,None,None,,,350.0,Has Special Classes,19-01-2023,301.0,148.0,153.0,56.1,Supported by a multi-academy trust,INSPIRE PARTNERSHIP ACADEMY TRUST,Linked to a sponsor,Inspire Partnership Academy Trust,Not applicable,,10067319.0,,,22-09-2022,16-04-2024,Elaine Avenue,,,Rochester,,ME2 2YN,http://www.elaine.medway.sch.uk/,1634294817.0,Mrs,Sarah,Martin,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,"SEMH - Social, Emotional and Mental Health",,,,,,,,,,,,,SEN unit,,,20.0,11.0,South East,Medway,Strood West,Rochester and Strood,(England/Wales) Urban city and town,E06000035,572443.0,168984.0,Medway 011,Medway 011A,,,,,Good,South-East England and South London,United Kingdom,200000895803.0,,Not applicable,Not applicable,,,E02003324,E01016150,169.0, +146648,887,Medway,4003,Waterfront UTC,University technical college,Free Schools,Open,,01-04-2019,,,Secondary,14.0,19,,No Nursery Classes,Has a sixth form,Mixed,Does not apply,Does not apply,,Non-selective,250.0,Not applicable,19-01-2023,267.0,190.0,77.0,31.3,Supported by a multi-academy trust,THE HOWARD ACADEMY TRUST,Linked to a sponsor,The Howard Academy Trust,Not applicable,,10082095.0,,,11-01-2023,14-09-2023,South Side Three Road,,,Chatham,Kent,ME4 4FQ,www.waterfront-that.org.uk,1634505800.0,Mrs,Fiona,McLean,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,St Mary's Island,Rochester and Strood,(England/Wales) Urban city and town,E06000035,577382.0,169573.0,Medway 007,Medway 007H,,,,,Good,South-East England and South London,United Kingdom,44039551.0,,Not applicable,Not applicable,,,E02003320,E01035292,66.0, +146872,887,Medway,2211,Halling Primary School,Academy converter,Academies,Open,Academy Converter,01-04-2019,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,420.0,No Special Classes,19-01-2023,376.0,188.0,188.0,17.3,Supported by a multi-academy trust,ALETHEIA ACADEMIES TRUST,Linked to a sponsor,Aletheia Academies Trust,Not applicable,,10082988.0,,Not applicable,07-06-2023,14-05-2024,Howlsmere Close,Halling,,Rochester,Kent,ME2 1ER,http://www.halling.medway.sch.uk/,1634240258.0,Ms,Lisa,Taylor,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,"Cuxton, Halling & Riverside",Rochester and Strood,(England/Wales) Rural town and fringe,E06000035,570607.0,163387.0,Medway 028,Medway 028A,,,,,Good,South-East England and South London,,44000585.0,,Not applicable,Not applicable,,,E02003341,E01016026,65.0, +147446,887,Medway,2022,Wainscott Primary School,Academy sponsor led,Academies,Open,New Provision,01-09-2019,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,446.0,No Special Classes,19-01-2023,455.0,235.0,220.0,17.6,Supported by a multi-academy trust,THE PRIMARY FIRST TRUST,Linked to a sponsor,The Primary First Trust,Not applicable,,10084146.0,,,,21-05-2024,Wainscott Road,Wainscott,,Rochester,Kent,ME2 4JX,www.wainscott.medway.sch.uk,1634332550.0,Mrs,Monique,Clark,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Strood Rural,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574754.0,170678.0,Medway 004,Medway 004E,,,,,,South-East England and South London,United Kingdom,44028647.0,,Not applicable,Not applicable,,,E02003317,E01035289,80.0, +147769,887,Medway,2023,Delce Academy,Academy converter,Academies,Open,Fresh Start,01-03-2020,,,Primary,5.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,None,Does not apply,,,360.0,Has Special Classes,19-01-2023,359.0,193.0,166.0,44.0,Supported by a multi-academy trust,INSPIRE PARTNERSHIP ACADEMY TRUST,Linked to a sponsor,Inspire Partnership Academy Trust,Not applicable,,10085699.0,,,,28-05-2024,The Tideway,,,Rochester,Kent,ME1 2NJ,www.delceacademy.co.uk,1634845242.0,Miss,Loni,Stevens,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,"SEMH - Social, Emotional and Mental Health",,,,,,,,,,,,,SEN unit,,,15.0,15.0,South East,Medway,Rochester East & Warren Wood,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574455.0,166524.0,Medway 017,Medway 017C,,,,,,South-East England and South London,United Kingdom,44022435.0,,Not applicable,Not applicable,,,E02003330,E01016115,158.0, +148117,887,Medway,2433,Oaklands School,Academy converter,Academies,Open,Academy Converter,01-09-2020,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,450.0,No Special Classes,19-01-2023,430.0,208.0,222.0,42.2,Supported by a multi-academy trust,THE WESTBROOK TRUST,Linked to a sponsor,The Westbrook Trust,Not applicable,,10086851.0,,,04-07-2023,21-05-2024,Weedswood Road,Walderslade,,Chatham,Kent,ME5 0QS,http://www.oaklands.medway.sch.uk,1634333820.0,Mrs,Louisa,Jones,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Wayfield & Weeds Wood,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575761.0,163898.0,Medway 033,Medway 033D,Not applicable,,,,Good,South-East England and South London,,44033912.0,,Not applicable,Not applicable,,,E02003346,E01016173,175.0, +148577,887,Medway,4004,Leigh Academy Rainham,Free schools,Free Schools,Open,New Provision,01-09-2021,,,Secondary,11.0,19,,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1150.0,Not applicable,19-01-2023,405.0,223.0,182.0,16.8,Supported by a multi-academy trust,LEIGH ACADEMIES TRUST,Linked to a sponsor,Leigh Academies Trust,Not applicable,,10088768.0,,,28-02-2024,22-05-2024,Otterham Quay Lane,Rainham,,Gillingham,Kent,ME8 8GS,https://leighacademyrainham.org.uk/,1634412440.0,Miss,Alexandra,Millward,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rainham North,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,582774.0,165824.0,Medway 025,Medway 025E,,,,,Good,South-East England and South London,United Kingdom,44073878.0,,Not applicable,Not applicable,,,E02003338,E01016102,68.0, +149009,887,Medway,1108,Will Adams Academy,Academy alternative provision converter,Academies,Open,Academy Converter,01-04-2022,,,Not applicable,14.0,17,No boarders,Not applicable,Not applicable,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,,Not applicable,19-01-2023,39.0,21.0,18.0,39.5,Supported by a multi-academy trust,ALTERNATIVE LEARNING TRUST,Linked to a sponsor,Alternative Learning Trust,Not applicable,,10090191.0,,,,16-04-2024,Woodlands Road,,,Gillingham,Kent,ME7 2BX,www.willadamsacademy.org.uk,1634337111.0,Ms,Marie,Woolston,HeadTeacher,Not applicable,,,,Not applicable,,Not applicable,PRU Does not have Provision for SEN,Not applicable,,Not applicable,Not applicable,Not applicable,"SEMH - Social, Emotional and Mental Health",,,,,,,,,,,,,Resourced provision,45.0,45.0,,,South East,Medway,Watling,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,578774.0,167623.0,Medway 019,Medway 019A,Not applicable,,,,,South-East England and South London,,200003623419.0,,Not applicable,Not applicable,,,E02003332,E01016159,15.0, +149069,887,Medway,2025,Rochester Riverside Church of England Primary School,Free schools,Free Schools,Open,Academy Free School,01-09-2022,,,Primary,3.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Church of England,Christian,,Non-selective,446.0,Not applicable,19-01-2023,26.0,16.0,10.0,0.0,Supported by a multi-academy trust,THE PILGRIM MULTI ACADEMY TRUST,-,,Not applicable,,10090885.0,,,,29-05-2024,Gas House Road,,,Rochester,Kent,ME1 1US,rrcoe.medway.sch.uk,1634471697.0,Mrs,Alison,Mepsted,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Rochester West & Borstal,Rochester and Strood,(England/Wales) Urban city and town,E06000035,574334.0,168802.0,Medway 015,Medway 015F,,,,,,South-East England and South London,United Kingdom,44076556.0,,Not applicable,Not applicable,,,E02003328,E01035296,0.0, +149075,887,Medway,4005,Maritime Academy,Free schools,Free Schools,Open,Academy Free School,01-09-2022,,,Secondary,11.0,19,No boarders,No Nursery Classes,Has a sixth form,Mixed,None,None,Not applicable,Non-selective,1150.0,Not applicable,19-01-2023,142.0,77.0,65.0,27.5,Supported by a multi-academy trust,THE THINKING SCHOOLS ACADEMY TRUST,Linked to a sponsor,The Thinking Schools Academy Trust,Not applicable,,10090894.0,,,,24-05-2024,Frindsbury Hill,Rochester,Kent,Medway,,,https://www.maritimeacademy.org.uk/,3333602150.0,,Jody,Murphy,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Twydall,Gillingham and Rainham,(England/Wales) Urban city and town,E06000035,579947.0,166841.0,Medway 018,Medway 018B,,,,,,South-East England and South London,,,,Not applicable,Not applicable,,,E02003331,E01016161,39.0, +149979,887,Medway,3753,St Benedict's Catholic Primary School,Academy converter,Academies,Open,Academy Converter,01-09-2023,,,Primary,4.0,11,No boarders,No Nursery Classes,Does not have a sixth form,Mixed,Roman Catholic,Does not apply,Archdiocese of Southwark,Not applicable,210.0,No Special Classes,,,,,,Supported by a multi-academy trust,KENT CATHOLIC SCHOOLS' PARTNERSHIP,Linked to a sponsor,Kent Catholic Schools' Partnership,Not applicable,,10092965.0,,,,06-03-2024,Lambourn Way,Lordswood,,Chatham,Kent,ME5 8PU,www.st-benedicts.medway.sch.uk/,1634669700.0,Mrs,Sarah,McAlpine,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Lordswood & Walderslade,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,577291.0,162859.0,Medway 038,Medway 038D,Not applicable,,,,,South-East England and South London,,44058733.0,,Not applicable,Not applicable,,,E02003351,E01016060,, +150154,887,Medway,4007,Oasis Restore,Academy secure 16 to 19,Academies,Open,New Provision,15-04-2024,,,16 plus,12.0,17,,Not applicable,Not applicable,Mixed,,,,,49.0,Not applicable,,,,,,Not applicable,OASIS RESTORE TRUST,Not applicable,,Not applicable,,10094342.0,,,,15-04-2024,1 Kennington Road,,,London,,SE1 7QP,https://www.oasisrestore.org/,2079214200.0,Mr,Andrew,Willetts,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Lambeth,Waterloo & South Bank,Vauxhall,(England/Wales) Urban major conurbation,E06000035,531156.0,179383.0,Lambeth 036,Lambeth 036B,,,,,,,United Kingdom,100023225318.0,,Not applicable,Not applicable,,,E02006801,E01003014,, +150218,887,Medway,2199,Luton Primary School,Academy converter,Academies,Open,Academy Converter,01-11-2023,,,Primary,4.0,11,No boarders,Has Nursery Classes,Does not have a sixth form,Mixed,Does not apply,Does not apply,Not applicable,Not applicable,630.0,No Special Classes,,,,,,Supported by a multi-academy trust,RIVERMEAD INCLUSIVE TRUST,Linked to a sponsor,Rivermead Inclusive Trust,Not applicable,,10093456.0,,,,03-06-2024,Luton Road,,,Chatham,Kent,ME4 5AW,www.lutonprimaryschool.co.uk,1634336900.0,Mrs,Karen,Major,Headteacher,Not applicable,,,,Not applicable,,Not applicable,Not applicable,Not applicable,,Not applicable,Not applicable,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Luton,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,577119.0,166655.0,Medway 020,Medway 020A,Not applicable,,,,,South-East England and South London,,100062391815.0,,Not applicable,Not applicable,,,E02003333,E01016061,, +150869,887,Medway,4008,Walderslade School,Academy converter,Academies,Open,Fresh Start,01-04-2024,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Girls,None,Does not apply,,,949.0,Not applicable,,,,,,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10095249.0,,,,04-04-2024,Bradfields Avenue,Walderslade,,Chatham,Kent,ME5 0LE,,,Mrs,Louise,Campbell,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Wayfield & Weeds Wood,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,575885.0,164367.0,Medway 027,Medway 027D,,,,,,South-East England and South London,United Kingdom,44033058.0,,Not applicable,Not applicable,,,E02003340,E01016067,, +150870,887,Medway,4009,Greenacre School,Academy converter,Academies,Open,Fresh Start,01-04-2024,,,Secondary,11.0,18,No boarders,No Nursery Classes,Has a sixth form,Boys,None,Does not apply,,,969.0,Not applicable,,,,,,Supported by a multi-academy trust,BEYOND SCHOOLS TRUST,Linked to a sponsor,FPTA Academies (Fort Pitt Grammar School and The Thomas Aveling School),Not applicable,,10095250.0,,,,04-04-2024,157 Walderslade Road,Walderslade,,Chatham,Kent,ME5 0LP,,,Mrs,Louise,Campbell,,Not applicable,,,,Not applicable,,,Not applicable,Not applicable,,,,Not applicable,,,,,,,,,,,,,,,,,,,South East,Medway,Wayfield & Weeds Wood,Chatham and Aylesford,(England/Wales) Urban city and town,E06000035,576000.0,164283.0,Medway 034,Medway 034C,,,,,,South-East England and South London,United Kingdom,44026624.0,,Not applicable,Not applicable,,,E02003347,E01016081,, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..47929cc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + backend: + container_name: ccapi + build: + context: . + dockerfile: Dockerfile + env_file: .env + ports: + - "${PORT_BACKEND}:${PORT_BACKEND}" + networks: + - kevlarai-network + +networks: + kevlarai-network: + name: kevlarai-network + driver: bridge diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..3599552 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +# Add backend to Python path +export PYTHONPATH="/app:${PYTHONPATH}" + +# Create init directories +mkdir -p /init + +# Function to check if initialization is needed +check_init_needed() { + if [ ! -f "/init/status.json" ]; then + return 0 + fi + + # Check if any status is incomplete + incomplete=$(python -c " +import json +try: + with open('/init/status.json', 'r') as f: + status = json.load(f) + print(not all(v for k, v in status.items() if k != 'timestamp')) +except (FileNotFoundError, json.JSONDecodeError): + print('True') +") + + if [ "$incomplete" = "True" ]; then + return 0 + else + return 1 + fi +} + +# Run initialization if needed +if check_init_needed; then + echo "Running initialization..." + python -c "from run.initialization import initialize_system; initialize_system()" +else + echo "System already initialized, skipping..." +fi + +# Execute the main command +exec "$@" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7f4d46f --- /dev/null +++ b/main.py @@ -0,0 +1,133 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +from fastapi import FastAPI, HTTPException +import uvicorn +import requests +from typing import Dict, Any +from modules.database.tools.neo4j_driver_tools import get_driver +from run.initialization.initialization import InitializationSystem +import time +import ssl + +from run.setup import setup_cors +from run.routers import register_routes +from run.initialization import initialize_system + +# FastAPI App Setup +app = FastAPI() +setup_cors(app) + +# Health check endpoint +@app.get("/health") +async def health_check() -> Dict[str, Any]: + """Health check endpoint that verifies all service dependencies""" + health_status = { + "status": "healthy", + "services": { + "neo4j": {"status": "healthy", "message": "Connected"}, + "supabase": {"status": "healthy", "message": "Connected"} + } + } + + try: + # Check Neo4j + driver = get_driver() + if not driver: + health_status["services"]["neo4j"] = { + "status": "unhealthy", + "message": "Failed to connect to Neo4j" + } + health_status["status"] = "unhealthy" + except Exception as e: + health_status["services"]["neo4j"] = { + "status": "unhealthy", + "message": f"Error checking Neo4j: {str(e)}" + } + health_status["status"] = "unhealthy" + + try: + # Minimal check to confirm Supabase is responsive (e.g., pinging auth or storage endpoint) + supabase_url = os.getenv("SUPABASE_URL") + service_role_key = os.getenv("SERVICE_ROLE_KEY") + response = requests.get( + f"{supabase_url}/auth/v1/health", + headers={"apikey": service_role_key}, + timeout=5 + ) + if response.status_code != 200: + health_status["services"]["supabase"] = { + "status": "unhealthy", + "message": f"Supabase Auth API returned status {response.status_code}" + } + health_status["status"] = "unhealthy" + except Exception as e: + health_status["services"]["supabase"] = { + "status": "unhealthy", + "message": f"Error checking Supabase Auth API: {str(e)}" + } + health_status["status"] = "unhealthy" + + if health_status["status"] == "unhealthy": + raise HTTPException(status_code=503, detail=health_status) + + return health_status + +# Register routes +register_routes(app) + +# Initialize system with retry logic +def initialize_with_retry(max_attempts: int = 3, initial_delay: int = 5) -> bool: + """Initialize the system with retry logic""" + attempt = 0 + delay = initial_delay + + while attempt < max_attempts: + try: + logger.info(f"Attempting system initialization (attempt {attempt + 1}/{max_attempts})") + initialize_system() + logger.info("System initialization completed successfully") + return True + except Exception as e: + attempt += 1 + if attempt == max_attempts: + logger.error(f"System initialization failed after {max_attempts} attempts: {str(e)}") + return False + + logger.warning(f"Initialization attempt {attempt} failed: {str(e)}. Retrying in {delay} seconds...") + time.sleep(delay) + delay *= 2 # Exponential backoff + + return False + +if __name__ == "__main__": + import uvicorn + import os + + # Run initialization with retry logic + if not initialize_with_retry(): + logger.error("Failed to initialize system after multiple attempts") + # Continue anyway to allow the API to start and handle health checks + + if os.getenv('BACKEND_DEV_MODE') == 'true': + logger.info("Running with Reload") + uvicorn.run( + "main:app", + host="0.0.0.0", + port=int(os.getenv('PORT_BACKEND', 8000)), + log_level="info", + proxy_headers=True, + timeout_keep_alive=10, + reload=True + ) + else: + logger.info("Running without Reload and without SSL (behind reverse proxy)") + uvicorn.run( + "main:app", + host="0.0.0.0", + port=int(os.getenv('PORT_BACKEND', 8000)), # <-- not 443 + log_level="info", + proxy_headers=True, + timeout_keep_alive=10, + workers=int(os.getenv('UVICORN_WORKERS', '1')) + ) diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/__pycache__/__init__.cpython-311.pyc b/modules/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..3ead5b3 Binary files /dev/null and b/modules/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/__pycache__/document_processor.cpython-311.pyc b/modules/__pycache__/document_processor.cpython-311.pyc new file mode 100644 index 0000000..757c672 Binary files /dev/null and b/modules/__pycache__/document_processor.cpython-311.pyc differ diff --git a/modules/__pycache__/logger_tool.cpython-311.pyc b/modules/__pycache__/logger_tool.cpython-311.pyc new file mode 100644 index 0000000..b2bb540 Binary files /dev/null and b/modules/__pycache__/logger_tool.cpython-311.pyc differ diff --git a/modules/__pycache__/pdf_utils.cpython-311.pyc b/modules/__pycache__/pdf_utils.cpython-311.pyc new file mode 100644 index 0000000..7573fad Binary files /dev/null and b/modules/__pycache__/pdf_utils.cpython-311.pyc differ diff --git a/modules/__pycache__/redis_config.cpython-311.pyc b/modules/__pycache__/redis_config.cpython-311.pyc new file mode 100644 index 0000000..49aec9d Binary files /dev/null and b/modules/__pycache__/redis_config.cpython-311.pyc differ diff --git a/modules/__pycache__/test_analyzer.cpython-311.pyc b/modules/__pycache__/test_analyzer.cpython-311.pyc new file mode 100644 index 0000000..ec4a7bd Binary files /dev/null and b/modules/__pycache__/test_analyzer.cpython-311.pyc differ diff --git a/modules/auth/__init__.py b/modules/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/auth/supabase_bearer.py b/modules/auth/supabase_bearer.py new file mode 100644 index 0000000..8cab779 --- /dev/null +++ b/modules/auth/supabase_bearer.py @@ -0,0 +1,81 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +from fastapi import Request, HTTPException, Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +import jwt +from jwt.exceptions import InvalidTokenError +import os + +class SupabaseBearer(HTTPBearer): + def __init__(self, auto_error: bool = True): + super().__init__(auto_error=auto_error) + + async def __call__(self, credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer())): + if not credentials: + raise HTTPException(status_code=403, detail="Invalid authorization code.") + + try: + token = credentials.credentials + payload = verify_supabase_token(token) + return payload + except Exception as e: + logger.error(f"Token verification failed: {str(e)}") + raise HTTPException(status_code=403, detail="Invalid token or expired token.") + +def verify_supabase_token(token: str) -> dict: + """Verify a Supabase JWT token and return its payload.""" + try: + jwt_secret = os.getenv("JWT_SECRET") + if not jwt_secret: + raise ValueError("JWT_SECRET not configured") + + # Decode the token with proper audience check + payload = jwt.decode( + token, + jwt_secret, + algorithms=["HS256"], + audience="authenticated" + ) + + logger.debug(f"Token payload: {payload}") + + return payload + except jwt.ExpiredSignatureError: + logger.error("Token has expired") + raise HTTPException(status_code=401, detail="Token has expired") + except jwt.InvalidTokenError as e: + logger.error(f"Invalid token: {str(e)}") + raise HTTPException(status_code=401, detail="Invalid token") + except Exception as e: + logger.error(f"Token verification failed: {str(e)}") + raise HTTPException(status_code=401, detail="Token verification failed") + +def decodeSupabaseJWT(token: str) -> dict: + try: + jwt_secret = os.getenv('SUPABASE_JWT_SECRET') + payload = jwt.decode(token, jwt_secret, algorithms=["HS256"], audience="authenticated") + return payload + except Exception: + return None + +# Initialize the security instance +security = HTTPBearer() + +async def verify_supabase_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + try: + token = credentials.credentials + # Verify token using your Supabase JWT secret + decoded_token = jwt.decode( + token, + os.getenv('SUPABASE_JWT_SECRET'), + algorithms=["HS256"], + audience="authenticated" + ) + return decoded_token + except InvalidTokenError as e: + raise HTTPException( + status_code=401, + detail=f"Invalid authentication token: {str(e)}" + ) \ No newline at end of file diff --git a/modules/database/__init__.py b/modules/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/__pycache__/__init__.cpython-311.pyc b/modules/database/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7587478 Binary files /dev/null and b/modules/database/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/admin/__init__.py b/modules/database/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/admin/__pycache__/__init__.cpython-311.pyc b/modules/database/admin/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e013d90 Binary files /dev/null and b/modules/database/admin/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/admin/__pycache__/graph_provider.cpython-311.pyc b/modules/database/admin/__pycache__/graph_provider.cpython-311.pyc new file mode 100644 index 0000000..49d60fc Binary files /dev/null and b/modules/database/admin/__pycache__/graph_provider.cpython-311.pyc differ diff --git a/modules/database/admin/__pycache__/neontology_provider.cpython-311.pyc b/modules/database/admin/__pycache__/neontology_provider.cpython-311.pyc new file mode 100644 index 0000000..04a59dc Binary files /dev/null and b/modules/database/admin/__pycache__/neontology_provider.cpython-311.pyc differ diff --git a/modules/database/admin/calendar_provider.py b/modules/database/admin/calendar_provider.py new file mode 100644 index 0000000..bbd1b2b --- /dev/null +++ b/modules/database/admin/calendar_provider.py @@ -0,0 +1,280 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +import modules.database.schemas.nodes.calendars as calendar_schemas +import modules.database.schemas.entities as entities +import modules.database.schemas.relationships.calendars as cal_rels +import modules.database.tools.neontology_tools as neon +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +from datetime import timedelta, datetime + +def create_calendar(db_name, start_date, end_date, attach_to_calendar_node=False, entity_node=None, time_chunk_node=None): + logger.info(f"Creating calendar for {start_date} to {end_date}") + + logger.info("Initializing Neontology connection") + + neon.init_neontology_connection() + + filesystem = ClassroomCopilotFilesystem(db_name, init_run_type="school") + + def create_tldraw_file_for_node(node, node_path): + node_data = { + "unique_id": node.unique_id, + "type": node.__class__.__name__, + "name": node.name if hasattr(node, 'name') else 'Unnamed Node' + } + logger.debug(f"Creating tldraw file for node: {node_data}") + filesystem.create_default_tldraw_file(node_path, node_data) + + created_years = {} + created_months = {} + created_weeks = {} + created_days = {} + + last_year_node = None + last_month_node = None + last_week_node = None + last_day_node = None + + calendar_nodes = { + 'calendar_node': None, + 'calendar_year_nodes': [], + 'calendar_month_nodes': [], + 'calendar_week_nodes': [], + 'calendar_day_nodes': [] + } + + calendar_type = None + if attach_to_calendar_node and entity_node: + calendar_type = "entity_calendar" + logger.info(f"Attaching calendar to entity node: {entity_node.unique_id}") + entity_unique_id = entity_node.unique_id + calendar_unique_id = f"Calendar_{entity_unique_id}" + calendar_name = f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}" + calendar_path = os.path.join(entity_node.path, "calendar") + calendar_node = calendar_schemas.CalendarNode( + unique_id=calendar_unique_id, + name=calendar_name, + start_date=start_date, + end_date=end_date, + path=calendar_path + ) + neon.create_or_merge_neontology_node(calendar_node, database=db_name, operation='merge') + calendar_nodes['calendar_node'] = calendar_node + logger.info(f"Calendar node created: {calendar_node.unique_id}") + + # Create a node tldraw file for the calendar node + create_tldraw_file_for_node(calendar_node, calendar_path) + + import backend.modules.database.schemas.relationships.owner_relationships as entity_cal_rels + + neon.create_or_merge_neontology_relationship( + entity_cal_rels.EntityHasCalendar(source=entity_node, target=calendar_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {entity_node.unique_id} to {calendar_node.unique_id}") + if entity_node and not attach_to_calendar_node: + calendar_type = "time_entity" + else: + logger.error("Invalid combination of parameters for calendar creation.") + raise ValueError("Invalid combination of parameters for calendar creation.") + + current_date = start_date + while current_date <= end_date: + year = current_date.year + month = current_date.month + day = current_date.day + iso_year, iso_week, iso_weekday = current_date.isocalendar() + + # Create directories for year, month, week, and day + _, year_path = filesystem.create_year_directory(year, calendar_path) + _, month_path = filesystem.create_month_directory(year, month, calendar_path) + _, week_path = filesystem.create_week_directory(year, iso_week, calendar_path) + _, day_path = filesystem.create_day_directory(year, month, day, calendar_path) + + calendar_year_unique_id = f"CalendarYear_{year}" + + if year not in created_years: + year_node = calendar_schemas.CalendarYearNode( + unique_id=calendar_year_unique_id, + year=str(year), + path=year_path + ) + neon.create_or_merge_neontology_node(year_node, database=db_name, operation='merge') + calendar_nodes['calendar_year_nodes'].append(year_node) + created_years[year] = year_node + create_tldraw_file_for_node(year_node, year_path) + logger.info(f"Year node created: {year_node.unique_id}") + + if attach_to_calendar_node: + neon.create_or_merge_neontology_relationship( + cal_rels.CalendarIncludesYear(source=calendar_node, target=year_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {calendar_node.unique_id} to {year_node.unique_id}") + if last_year_node: + neon.create_or_merge_neontology_relationship( + cal_rels.YearFollowsYear(source=last_year_node, target=year_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_year_node.unique_id} to {year_node.unique_id}") + last_year_node = year_node + + calendar_month_unique_id = f"CalendarMonth_{year}_{month}" + + month_key = f"{year}-{month}" + if month_key not in created_months: + month_node = calendar_schemas.CalendarMonthNode( + unique_id=calendar_month_unique_id, + year=str(year), + month=str(month), + month_name=datetime(year, month, 1).strftime('%B'), + path=month_path + ) + neon.create_or_merge_neontology_node(month_node, database=db_name, operation='merge') + calendar_nodes['calendar_month_nodes'].append(month_node) + created_months[month_key] = month_node + create_tldraw_file_for_node(month_node, month_path) + logger.info(f"Month node created: {month_node.unique_id}") + + # Check for the end of year transition for months + if last_month_node: + if int(month) == 1 and int(last_month_node.month) == 12 and int(last_month_node.year) == year - 1: + neon.create_or_merge_neontology_relationship( + cal_rels.MonthFollowsMonth(source=last_month_node, target=month_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}") + elif int(month) == int(last_month_node.month) + 1: + neon.create_or_merge_neontology_relationship( + cal_rels.MonthFollowsMonth(source=last_month_node, target=month_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}") + last_month_node = month_node + + neon.create_or_merge_neontology_relationship( + cal_rels.YearIncludesMonth(source=year_node, target=month_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {year_node.unique_id} to {month_node.unique_id}") + + calendar_week_unique_id = f"CalendarWeek_{iso_year}_{iso_week}" + + week_key = f"{iso_year}-W{iso_week}" + if week_key not in created_weeks: + # Get the date of the first monday of the week + week_start_date = current_date - timedelta(days=current_date.weekday()) + week_node = calendar_schemas.CalendarWeekNode( + unique_id=calendar_week_unique_id, + start_date=week_start_date, + week_number=str(iso_week), + iso_week=f"{iso_year}-W{iso_week:02}", + path=week_path + ) + neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge') + calendar_nodes['calendar_week_nodes'].append(week_node) + created_weeks[week_key] = week_node + create_tldraw_file_for_node(week_node, week_path) + logger.info(f"Week node created: {week_node.unique_id}") + + if last_week_node and ((last_week_node.iso_week.split('-')[0] == str(iso_year) and int(last_week_node.week_number) == int(iso_week) - 1) or + (last_week_node.iso_week.split('-')[0] != str(iso_year) and int(last_week_node.week_number) == 52 and int(iso_week) == 1)): + neon.create_or_merge_neontology_relationship( + cal_rels.WeekFollowsWeek(source=last_week_node, target=week_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_week_node.unique_id} to {week_node.unique_id}") + last_week_node = week_node + + neon.create_or_merge_neontology_relationship( + cal_rels.YearIncludesWeek(source=year_node, target=week_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {year_node.unique_id} to {week_node.unique_id}") + + calendar_day_unique_id = f"CalendarDay_{year}_{month}_{day}" + + day_key = f"{year}-{month}-{day}" + day_node = calendar_schemas.CalendarDayNode( + unique_id=calendar_day_unique_id, + date=current_date, + day_of_week=current_date.strftime('%A'), + iso_day=f"{year}-{month:02}-{day:02}", + path=day_path + ) + neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge') + calendar_nodes['calendar_day_nodes'].append(day_node) + created_days[day_key] = day_node + create_tldraw_file_for_node(day_node, day_path) + logger.info(f"Day node created: {day_node.unique_id}") + + if last_day_node: + neon.create_or_merge_neontology_relationship( + cal_rels.DayFollowsDay(source=last_day_node, target=day_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_day_node.unique_id} to {day_node.unique_id}") + last_day_node = day_node + + neon.create_or_merge_neontology_relationship( + cal_rels.MonthIncludesDay(source=month_node, target=day_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {month_node.unique_id} to {day_node.unique_id}") + neon.create_or_merge_neontology_relationship( + cal_rels.WeekIncludesDay(source=week_node, target=day_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {week_node.unique_id} to {day_node.unique_id}") + current_date += timedelta(days=1) + + if time_chunk_node: + time_chunk_interval = time_chunk_node + # Get every calendar day node and create time chunks of length time_chunk_node minutes for the whole day + for day_node in calendar_nodes['calendar_day_nodes']: + day_path = day_node.path + total_time_chunks_in_day = (24 * 60) / time_chunk_interval + for i in range(total_time_chunks_in_day): + time_chunk_unique_id = f"CalendarTimeChunk_{day_node.unique_id}_{i}" + time_chunk_start_time = day_node.date.time() + timedelta(minutes=i * time_chunk_interval) + time_chunk_end_time = time_chunk_start_time + timedelta(minutes=time_chunk_interval) + time_chunk_node = calendar_schemas.CalendarTimeChunkNode( + unique_id=time_chunk_unique_id, + start_time=time_chunk_start_time, + end_time=time_chunk_end_time, + path=day_path + ) + neon.create_or_merge_neontology_node(time_chunk_node, database=db_name, operation='merge') + calendar_nodes['calendar_time_chunk_nodes'].append(time_chunk_node) + logger.info(f"Time chunk node created: {time_chunk_node.unique_id}") + # Create a relationship between the time chunk node and the day node + neon.create_or_merge_neontology_relationship( + cal_rels.DayIncludesTimeChunk(source=day_node, target=time_chunk_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {day_node.unique_id} to {time_chunk_node.unique_id}") + # Create sequential relationship between the time chunk nodes + if i > 0: + neon.create_or_merge_neontology_relationship( + cal_rels.TimeChunkFollowsTimeChunk(source=calendar_nodes['calendar_time_chunk_nodes'][i-1], target=time_chunk_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {calendar_nodes['calendar_time_chunk_nodes'][i-1].unique_id} to {time_chunk_node.unique_id}") + + logger.info(f'Created calendar: {calendar_nodes["calendar_node"].unique_id}') + return calendar_nodes \ No newline at end of file diff --git a/modules/database/admin/graph_provider.py b/modules/database/admin/graph_provider.py new file mode 100644 index 0000000..2b2e036 --- /dev/null +++ b/modules/database/admin/graph_provider.py @@ -0,0 +1,401 @@ +from enum import Enum +from typing import Optional, List, Dict, Any +import logging + +from modules.database.admin.neontology_provider import NeontologyProvider + +class NodeLabels(Enum): + SCHOOL = "School" + DEPARTMENT_STRUCTURE = "DepartmentStructure" + CURRICULUM_STRUCTURE = "CurriculumStructure" + PASTORAL_STRUCTURE = "PastoralStructure" + DEPARTMENT = "Department" + KEY_STAGE = "KeyStage" + YEAR_GROUP = "YearGroup" + +class RelationshipTypes(Enum): + HAS_DEPARTMENT_STRUCTURE = "HAS_DEPARTMENT_STRUCTURE" + HAS_CURRICULUM_STRUCTURE = "HAS_CURRICULUM_STRUCTURE" + HAS_PASTORAL_STRUCTURE = "HAS_PASTORAL_STRUCTURE" + HAS_DEPARTMENT = "HAS_DEPARTMENT" + INCLUDES_KEY_STAGE = "INCLUDES_KEY_STAGE" + INCLUDES_YEAR_GROUP = "INCLUDES_YEAR_GROUP" + +class PropertyKeys(Enum): + UNIQUE_ID = "unique_id" + PATH = "path" + URN = "urn" + ESTABLISHMENT_NUMBER = "establishment_number" + ESTABLISHMENT_NAME = "establishment_name" + ESTABLISHMENT_TYPE = "establishment_type" + ESTABLISHMENT_STATUS = "establishment_status" + PHASE_OF_EDUCATION = "phase_of_education" + STATUTORY_LOW_AGE = "statutory_low_age" + STATUTORY_HIGH_AGE = "statutory_high_age" + RELIGIOUS_CHARACTER = "religious_character" + SCHOOL_CAPACITY = "school_capacity" + SCHOOL_WEBSITE = "school_website" + OFSTED_RATING = "ofsted_rating" + DEPARTMENT_NAME = "department_name" + KEY_STAGE = "key_stage" + KEY_STAGE_NAME = "key_stage_name" + YEAR_GROUP = "year_group" + YEAR_GROUP_NAME = "year_group_name" + CREATED = "created" + MERGED = "merged" + +class SchemaDefinition: + """Class to hold schema definition queries and information""" + + @staticmethod + def get_schema_info() -> Dict[str, List[Dict]]: + """Returns a dictionary containing the schema definition for nodes and relationships.""" + return { + "nodes": [ + { + "label": "School", + "description": "Represents a school entity", + "required_properties": ["unique_id", "urn", "name"], + "optional_properties": ["address", "postcode", "phone", "email", "website"] + }, + { + "label": "DepartmentStructure", + "description": "Represents the department structure of a school", + "required_properties": ["unique_id", "name"], + "optional_properties": ["description", "head_of_department"] + }, + { + "label": "CurriculumStructure", + "description": "Represents the curriculum structure of a school", + "required_properties": ["unique_id", "name"], + "optional_properties": ["description", "key_stage", "subject"] + }, + { + "label": "PastoralStructure", + "description": "Represents the pastoral structure of a school", + "required_properties": ["unique_id", "name"], + "optional_properties": ["description", "year_group", "form_group"] + } + ], + "relationships": [ + { + "type": "HAS_DEPARTMENT_STRUCTURE", + "description": "Links a school to its department structure", + "source": "School", + "target": "DepartmentStructure", + "properties": ["created_at"] + }, + { + "type": "HAS_CURRICULUM_STRUCTURE", + "description": "Links a school to its curriculum structure", + "source": "School", + "target": "CurriculumStructure", + "properties": ["created_at"] + }, + { + "type": "HAS_PASTORAL_STRUCTURE", + "description": "Links a school to its pastoral structure", + "source": "School", + "target": "PastoralStructure", + "properties": ["created_at"] + } + ] + } + + @staticmethod + def get_schema_creation_queries() -> List[str]: + """Returns a list of Cypher queries to create the schema.""" + return [ + # Node Uniqueness Constraints + f"CREATE CONSTRAINT school_unique_id IF NOT EXISTS FOR (n:{NodeLabels.SCHOOL.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE", + f"CREATE CONSTRAINT department_unique_id IF NOT EXISTS FOR (n:{NodeLabels.DEPARTMENT_STRUCTURE.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE", + f"CREATE CONSTRAINT curriculum_unique_id IF NOT EXISTS FOR (n:{NodeLabels.CURRICULUM_STRUCTURE.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE", + f"CREATE CONSTRAINT pastoral_unique_id IF NOT EXISTS FOR (n:{NodeLabels.PASTORAL_STRUCTURE.value}) REQUIRE n.{PropertyKeys.UNIQUE_ID.value} IS UNIQUE", + + # Indexes for Performance + f"CREATE INDEX school_urn IF NOT EXISTS FOR (n:{NodeLabels.SCHOOL.value}) ON (n.{PropertyKeys.URN.value})", + f"CREATE INDEX school_name IF NOT EXISTS FOR (n:{NodeLabels.SCHOOL.value}) ON (n.{PropertyKeys.ESTABLISHMENT_NAME.value})", + f"CREATE INDEX department_name IF NOT EXISTS FOR (n:{NodeLabels.DEPARTMENT_STRUCTURE.value}) ON (n.{PropertyKeys.DEPARTMENT_NAME.value})", + f"CREATE INDEX curriculum_name IF NOT EXISTS FOR (n:{NodeLabels.CURRICULUM_STRUCTURE.value}) ON (n.name)", + f"CREATE INDEX pastoral_name IF NOT EXISTS FOR (n:{NodeLabels.PASTORAL_STRUCTURE.value}) ON (n.name)", + ] + + @staticmethod + def get_schema_verification_queries() -> Dict[str, str]: + """Returns a dictionary of queries to verify the schema state.""" + return { + "constraints": "SHOW CONSTRAINTS", + "indexes": "SHOW INDEXES", + "labels": "CALL db.labels()" + } + +class GraphNamingProvider: + @staticmethod + def get_school_unique_id(urn: str) -> str: + """Generate unique ID for a school node.""" + return f"School_{urn}" + + @staticmethod + def get_department_structure_unique_id(school_unique_id: str) -> str: + """Generate unique ID for a department structure node.""" + return f"DepartmentStructure_{school_unique_id}" + + @staticmethod + def get_curriculum_structure_unique_id(school_unique_id: str) -> str: + """Generate unique ID for a curriculum structure node.""" + return f"CurriculumStructure_{school_unique_id}" + + @staticmethod + def get_pastoral_structure_unique_id(school_unique_id: str) -> str: + """Generate unique ID for a pastoral structure node.""" + return f"PastoralStructure_{school_unique_id}" + + @staticmethod + def get_department_unique_id(school_unique_id: str, department_name: str) -> str: + """Generate unique ID for a department node.""" + return f"Department_{school_unique_id}_{department_name.replace(' ', '_')}" + + @staticmethod + def get_key_stage_unique_id(curriculum_structure_unique_id: str, key_stage: str) -> str: + """Generate unique ID for a key stage node.""" + return f"KeyStage_{curriculum_structure_unique_id}_KStg{key_stage}" + + @staticmethod + def get_year_group_unique_id(school_unique_id: str, year_group: int) -> str: + """Generate unique ID for a year group node.""" + return f"YearGroup_{school_unique_id}_YGrp{year_group}" + + @staticmethod + def get_school_path(database_name: str, urn: str) -> str: + """Generate path for a school node.""" + return f"/schools/{database_name}/{urn}" + + @staticmethod + def get_department_path(school_path: str, department_name: str) -> str: + """Generate path for a department node.""" + return f"{school_path}/departments/{department_name}" + + @staticmethod + def get_department_structure_path(school_path: str) -> str: + """Generate path for a department structure node.""" + return f"{school_path}/departments" + + @staticmethod + def get_curriculum_path(school_path: str) -> str: + """Generate path for a curriculum structure node.""" + return f"{school_path}/curriculum" + + @staticmethod + def get_pastoral_path(school_path: str) -> str: + """Generate path for a pastoral structure node.""" + return f"{school_path}/pastoral" + + @staticmethod + def get_key_stage_path(curriculum_path: str, key_stage: str) -> str: + """Generate path for a key stage node.""" + return f"{curriculum_path}/key_stage_{key_stage}" + + @staticmethod + def get_year_group_path(pastoral_path: str, year_group: int) -> str: + """Generate path for a year group node.""" + return f"{pastoral_path}/year_{year_group}" + + @staticmethod + def get_cypher_match_school(unique_id: str) -> str: + """Generate Cypher MATCH clause for finding a school node.""" + return f"MATCH (s:{NodeLabels.SCHOOL.value} {{{PropertyKeys.UNIQUE_ID.value}: $school_id}})" + + @staticmethod + def get_cypher_check_basic_structure() -> str: + """Generate Cypher query for checking basic structure existence and validity.""" + return """ + // Find the school node + MATCH (s:{school}) + + // Check for department structure with any relationship + OPTIONAL MATCH (s)-[r1]-(dept_struct:{dept_struct}) + + // Check for curriculum structure with any relationship + OPTIONAL MATCH (s)-[r2]-(curr_struct:{curr_struct}) + + // Check for pastoral structure with any relationship + OPTIONAL MATCH (s)-[r3]-(past_struct:{past_struct}) + + // Return structure information + RETURN {{ + has_basic: + dept_struct IS NOT NULL AND r1 IS NOT NULL AND + curr_struct IS NOT NULL AND r2 IS NOT NULL AND + past_struct IS NOT NULL AND r3 IS NOT NULL, + department_structure: {{ + exists: dept_struct IS NOT NULL AND r1 IS NOT NULL + }}, + curriculum_structure: {{ + exists: curr_struct IS NOT NULL AND r2 IS NOT NULL + }}, + pastoral_structure: {{ + exists: past_struct IS NOT NULL AND r3 IS NOT NULL + }} + }} as status + """.format( + school=NodeLabels.SCHOOL.value, + dept_struct=NodeLabels.DEPARTMENT_STRUCTURE.value, + curr_struct=NodeLabels.CURRICULUM_STRUCTURE.value, + past_struct=NodeLabels.PASTORAL_STRUCTURE.value + ) + + @staticmethod + def get_cypher_check_detailed_structure() -> str: + """Generate Cypher query for checking detailed structure existence and validity.""" + return """ + // Find the school node + MATCH (s:{school} {{unique_id: $school_id}}) + + // Check for department structure and departments + OPTIONAL MATCH (s)-[r1]-(dept_struct:{dept_struct}) + WHERE dept_struct.unique_id = 'DepartmentStructure_' + s.unique_id + WITH s, dept_struct, r1, + CASE WHEN dept_struct IS NOT NULL + THEN [(dept_struct)-[r]-(d:{dept}) | d] + ELSE [] + END as departments + + // Check for curriculum structure and key stages + OPTIONAL MATCH (s)-[r2]-(curr_struct:{curr_struct}) + WHERE curr_struct.unique_id = 'CurriculumStructure_' + s.unique_id + WITH s, dept_struct, r1, departments, curr_struct, r2, + CASE WHEN curr_struct IS NOT NULL + THEN [(curr_struct)-[r]-(k:{key_stage}) | k] + ELSE [] + END as key_stages + + // Check for pastoral structure and year groups + OPTIONAL MATCH (s)-[r3]-(past_struct:{past_struct}) + WHERE past_struct.unique_id = 'PastoralStructure_' + s.unique_id + WITH dept_struct, r1, departments, curr_struct, r2, key_stages, past_struct, r3, + CASE WHEN past_struct IS NOT NULL + THEN [(past_struct)-[r]-(y:{year_group}) | y] + ELSE [] + END as year_groups + + // Return structure information + RETURN {{ + has_detailed: + dept_struct IS NOT NULL AND r1 IS NOT NULL AND size(departments) > 0 AND + curr_struct IS NOT NULL AND r2 IS NOT NULL AND size(key_stages) > 0 AND + past_struct IS NOT NULL AND r3 IS NOT NULL AND size(year_groups) > 0, + department_structure: {{ + exists: dept_struct IS NOT NULL AND r1 IS NOT NULL, + has_departments: size(departments) > 0, + department_count: size(departments), + node_id: dept_struct.unique_id + }}, + curriculum_structure: {{ + exists: curr_struct IS NOT NULL AND r2 IS NOT NULL, + has_key_stages: size(key_stages) > 0, + key_stage_count: size(key_stages), + node_id: curr_struct.unique_id + }}, + pastoral_structure: {{ + exists: past_struct IS NOT NULL AND r3 IS NOT NULL, + has_year_groups: size(year_groups) > 0, + year_group_count: size(year_groups), + node_id: past_struct.unique_id + }} + }} as status + """.format( + school=NodeLabels.SCHOOL.value, + dept_struct=NodeLabels.DEPARTMENT_STRUCTURE.value, + curr_struct=NodeLabels.CURRICULUM_STRUCTURE.value, + past_struct=NodeLabels.PASTORAL_STRUCTURE.value, + dept=NodeLabels.DEPARTMENT.value, + key_stage=NodeLabels.KEY_STAGE.value, + year_group=NodeLabels.YEAR_GROUP.value + ) + + @staticmethod + def get_schema_definition() -> SchemaDefinition: + """Get the schema definition instance""" + return SchemaDefinition() + + @staticmethod + def get_schema_creation_queries() -> List[str]: + """Get queries to create the schema""" + return SchemaDefinition.get_schema_creation_queries() + + @staticmethod + def get_schema_verification_queries() -> Dict[str, str]: + """Get queries to verify schema state""" + return SchemaDefinition.get_schema_verification_queries() + + @staticmethod + def get_schema_info() -> Dict[str, List[Dict]]: + """Get human-readable schema information""" + return SchemaDefinition.get_schema_info() + +class GraphProvider: + def __init__(self): + """Initialize the graph provider with Neo4j connection.""" + self.neontology = NeontologyProvider() + self.graph_naming = GraphNamingProvider() + self.logger = logging.getLogger(__name__) + + def check_schema_status(self, database_name: str) -> Dict[str, Any]: + """ + Checks the current state of the schema in the specified database. + Returns a dictionary containing information about constraints, indexes, and labels. + """ + try: + verification_queries = SchemaDefinition.get_schema_verification_queries() + expected_schema = SchemaDefinition.get_schema_info() + + # Get current schema state + constraints = self.neontology.run_query(verification_queries["constraints"], {}, database_name) + indexes = self.neontology.run_query(verification_queries["indexes"], {}, database_name) + labels = self.neontology.run_query(verification_queries["labels"], {}, database_name) + + # Process results + current_constraints = [c["name"] for c in constraints] + current_indexes = [i["name"] for i in indexes] + current_labels = [l["label"] for l in labels] + + # Expected values + expected_labels = [node["label"] for node in expected_schema["nodes"]] + + return { + "constraints": current_constraints, + "constraints_valid": len(current_constraints) >= 4, # We expect at least 4 unique constraints + "indexes": current_indexes, + "indexes_valid": len(current_indexes) >= 5, # We expect at least 5 indexes + "labels": current_labels, + "labels_valid": all(label in current_labels for label in expected_labels) + } + except Exception as e: + self.logger.error(f"Error checking schema status: {str(e)}") + return { + "constraints": [], "constraints_valid": False, + "indexes": [], "indexes_valid": False, + "labels": [], "labels_valid": False + } + + def initialize_schema(self, database_name: str) -> None: + """ + Initializes the schema for the specified database by creating all necessary + constraints and indexes. + """ + try: + creation_queries = SchemaDefinition.get_schema_creation_queries() + + for query in creation_queries: + self.neontology.cypher_write(query, {}, database_name) + + self.logger.info(f"Schema initialized successfully for database {database_name}") + except Exception as e: + self.logger.error(f"Error initializing schema: {str(e)}") + raise + + def get_schema_info(self) -> Dict[str, Any]: + """ + Returns the schema definition information. + """ + return SchemaDefinition.get_schema_info() diff --git a/modules/database/admin/neontology_provider.py b/modules/database/admin/neontology_provider.py new file mode 100644 index 0000000..9782993 --- /dev/null +++ b/modules/database/admin/neontology_provider.py @@ -0,0 +1,212 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +from modules.database.tools.neontology.graphconnection import GraphConnection, init_neontology +from modules.database.tools.neontology.basenode import BaseNode +from modules.database.tools.neontology.baserelationship import BaseRelationship +from typing import Optional, Dict, Any, List +from neo4j import Record as Neo4jRecord +import re + +log_name = 'api_modules_database_admin_neontology_provider' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +class NeontologyProvider: + """Provider class for managing Neontology connections and operations.""" + + def __init__(self): + """Initialize the provider with Neo4j connection details from environment.""" + self.bolt_url = os.getenv("APP_BOLT_URL") + self.user = os.getenv("USER_NEO4J") + self.password = os.getenv("PASSWORD_NEO4J") + self.connection = None + self.current_database = None + + def _validate_database_name(self, database: str) -> str: + """ + Validate and format database name to handle special characters. + + Args: + database: The database name to validate + + Returns: + str: The validated database name + + Raises: + ValueError: If database name is invalid + """ + if not database: + raise ValueError("Database name cannot be empty") + + # Check for valid database name pattern + # Allow letters, numbers, underscores, and dots + if not re.match(r'^[a-zA-Z0-9_\.]+$', database): + raise ValueError("Database name contains invalid characters") + + # For database names with multiple dots, we need to handle them specially + # Neo4j treats dots as special characters in some contexts + if database.count('.') > 1: + # Replace dots with underscores except for the first one + parts = database.split('.') + if len(parts) > 2: + # Keep the first dot, replace others with underscore + formatted_name = f"{parts[0]}.{'.'.join(parts[1:])}" + logging.info(f"Reformatted database name from {database} to {formatted_name}") + return formatted_name + + return database + + def connect(self, database: str = 'neo4j') -> None: + """Establish connection to Neo4j using Neontology.""" + try: + # Validate and format database name + formatted_database = self._validate_database_name(database) + + # If we're switching databases, ensure we close the old connection + if self.current_database != formatted_database and self.connection is not None: + self.close() + + # Initialize Neontology connection if needed + if self.connection is None: + init_neontology( + neo4j_uri=self.bolt_url, + neo4j_username=self.user, + neo4j_password=self.password + ) + # Get the GraphConnection instance + self.connection = GraphConnection() + self.current_database = formatted_database + logging.info(f"Neontology connection initialized with host: {self.host}, port: {self.port}, database: {formatted_database}") + + except Exception as e: + logging.error(f"Failed to initialize Neontology connection: {str(e)}") + raise + + def reset_connection(self) -> None: + """Reset the connection, forcing a new one to be created on next use.""" + if self.connection: + self.close() + + def create_or_merge_node(self, node: BaseNode, database: str = 'neo4j', operation: str = "merge") -> None: + """Create or merge a node in the Neo4j database.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + + if operation == "create": + node.create(database=database) + elif operation == "merge": + node.merge(database=database) + else: + logging.error(f"Invalid operation: {operation}") + raise ValueError(f"Invalid operation: {operation}") + except Exception as e: + logging.error(f"Error in processing node: {e}") + raise + + def create_or_merge_relationship(self, relationship: BaseRelationship, database: str = 'neo4j', operation: str = "merge") -> None: + """Create or merge a relationship in the Neo4j database.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + + if operation == "create": + relationship.create(database=database) + elif operation == "merge": + relationship.merge(database=database) + else: + logging.error(f"Invalid operation: {operation}") + raise ValueError(f"Invalid operation: {operation}") + except Exception as e: + logging.error(f"Error in processing relationship: {e}") + raise + + def cypher_write(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> None: + """Execute a write transaction.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + self.connection.cypher_write(cypher, params) + except Exception as e: + logging.error(f"Error in cypher write: {e}") + raise + + def cypher_read(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> Optional[Neo4jRecord]: + """Execute a read transaction returning a single record.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + return self.connection.cypher_read(cypher, params) + except Exception as e: + logging.error(f"Error in cypher read: {e}") + raise + + def cypher_read_many(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Neo4jRecord]: + """Execute a read transaction returning multiple records.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + return self.connection.cypher_read_many(cypher, params) + except Exception as e: + logging.error(f"Error in cypher read many: {e}") + raise + + def run_query(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Dict[str, Any]]: + """ + Execute a Cypher query and return results as a list of dictionaries. + This is a convenience method that handles both single and multiple record results. + + Args: + cypher: The Cypher query to execute + params: Query parameters + database: Target database name + + Returns: + List[Dict[str, Any]]: Query results as a list of dictionaries + """ + try: + if not self.connection or self.current_database != database: + self.connect(database) + + # Use cypher_read_many for consistent return type + records = self.connection.cypher_read_many(cypher, params) + + # Convert Neo4j records to dictionaries + results = [] + for record in records: + # Handle both Record and dict types + if isinstance(record, Neo4jRecord): + results.append(dict(record)) + else: + results.append(record) + + return results + + except Exception as e: + logging.error(f"Error in run_query: {e}") + raise + + def close(self) -> None: + """Close the Neontology connection.""" + if self.connection: + # The connection will be closed when the GraphConnection instance is deleted + self.connection = None + self.current_database = None + logging.info("Neontology connection closed") + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.close() diff --git a/modules/database/admin/school_curriculum_provider.py b/modules/database/admin/school_curriculum_provider.py new file mode 100644 index 0000000..1da3748 --- /dev/null +++ b/modules/database/admin/school_curriculum_provider.py @@ -0,0 +1,797 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +import backend.modules.database.schemas.entities as neo_entity +import modules.database.schemas.curriculum_neo as neo_curriculum +import modules.database.schemas.relationships.curriculum_relationships as curriculum_relationships +import modules.database.schemas.relationships.entity_relationships as ent_rels +import modules.database.schemas.relationships.entity_curriculum_rels as ent_cur_rels +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +import modules.database.tools.neontology_tools as neon +import pandas as pd + +# Default values for nodes +default_topic_values = { + 'topic_assessment_type': 'Null', + 'topic_type': 'Null', + 'total_number_of_lessons_for_topic': '1', + 'topic_title': 'Null' +} + +default_topic_lesson_values = { + 'topic_lesson_title': 'Null', + 'topic_lesson_type': 'Null', + 'topic_lesson_length': '1', + 'topic_lesson_suggested_activities': 'Null', + 'topic_lesson_skills_learned': 'Null', + 'topic_lesson_weblinks': 'Null', +} + +default_learning_statement_values = { + 'lesson_learning_statement': 'Null', + 'lesson_learning_statement_type': 'Student learning outcome' +} + +# Helper function to sort year groups numerically where possible +def sort_year_groups(df): + df = df.copy() + df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce') + return df.sort_values(by='YearGroupNumeric') + +def create_curriculum(dataframes, school_db_name, curriculum_db_name, school_node): + + fs_handler = ClassroomCopilotFilesystem(school_db_name, init_run_type="school") + + logger.info(f"Initialising neo4j connection...") + neon.init_neontology_connection() + + keystagesyllabus_df = dataframes['keystagesyllabuses'] + yeargroupsyllabus_df = dataframes['yeargroupsyllabuses'] + topic_df = dataframes['topics'] + lesson_df = dataframes['lessons'] + statement_df = dataframes['statements'] + # resource_df = dataframes['resources'] # TODO + + node_library = {} + node_library['key_stage_nodes'] = {} + node_library['year_group_nodes'] = {} + node_library['key_stage_syllabus_nodes'] = {} + node_library['year_group_syllabus_nodes'] = {} + node_library['topic_nodes'] = {} + node_library['topic_lesson_nodes'] = {} + node_library['statement_nodes'] = {} + node_library['department_nodes'] = {} + node_library['subject_nodes'] = {} + curriculum_node = None + pastoral_node = None + key_stage_nodes_created = {} + year_group_nodes_created = {} + last_year_group_node = None + last_key_stage_node = None + + # Create Curriculum and Pastoral nodes and relationships with School in both databases + _, curriculum_path = fs_handler.create_school_curriculum_directory(school_node.path) + _, pastoral_path = fs_handler.create_school_pastoral_directory(school_node.path) + + # Create Department Structure node + department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}" + department_structure_node = neo_entity.DepartmentStructureNode( + unique_id=department_structure_node_unique_id, + path=os.path.join(school_node.path, "departments") + ) + # Create in school database only + neon.create_or_merge_neontology_node(department_structure_node, database=school_db_name, operation='merge') + fs_handler.create_default_tldraw_file(department_structure_node.path, department_structure_node.to_dict()) + node_library['department_structure_node'] = department_structure_node + + # Link Department Structure to School + neon.create_or_merge_neontology_relationship( + ent_rels.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created department structure node and linked to school") + + # Create Curriculum Structure node + curriculum_structure_node_unique_id = f"CurriculumStructure_{school_node.unique_id}" + curriculum_node = neo_curriculum.CurriculumStructureNode( + unique_id=curriculum_structure_node_unique_id, + path=curriculum_path + ) + # Create in school database only + neon.create_or_merge_neontology_node(curriculum_node, database=school_db_name, operation='merge') + fs_handler.create_default_tldraw_file(curriculum_node.path, curriculum_node.to_dict()) + node_library['curriculum_node'] = curriculum_node + + # Create relationship in school database only + neon.create_or_merge_neontology_relationship( + ent_cur_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created curriculum node and relationship with school") + + # Create Pastoral Structure node + pastoral_structure_node_unique_id = f"PastoralStructure_{school_node.unique_id}" + pastoral_node = neo_curriculum.PastoralStructureNode( + unique_id=pastoral_structure_node_unique_id, + path=pastoral_path + ) + neon.create_or_merge_neontology_node(pastoral_node, database=school_db_name, operation='merge') + fs_handler.create_default_tldraw_file(pastoral_node.path, pastoral_node.to_dict()) + node_library['pastoral_node'] = pastoral_node + neon.create_or_merge_neontology_relationship( + ent_cur_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created pastoral node and relationship with school") + + # Create departments and subjects + # First get unique departments + unique_departments = keystagesyllabus_df['Department'].dropna().unique() + + for department_name in unique_departments: + department_unique_id = f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}" + _, department_path = fs_handler.create_school_department_directory(school_node.path, department_name) + + department_node = neo_entity.DepartmentNode( + unique_id=department_unique_id, + department_name=department_name, + path=department_path + ) + # Create department in school database only + neon.create_or_merge_neontology_node(department_node, database=school_db_name, operation='merge') + fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict()) + node_library['department_nodes'][department_name] = department_node + + # Link department to department structure in school database + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created department node for {department_name} and linked to department structure") + + # Create subjects and link to departments + # First get unique subjects from key stage syllabuses (which have department info) + unique_subjects = keystagesyllabus_df[['Subject', 'SubjectCode', 'Department']].drop_duplicates() + + # Then add any additional subjects from year group syllabuses (without department info) + additional_subjects = yeargroupsyllabus_df[['Subject', 'SubjectCode']].drop_duplicates() + additional_subjects = additional_subjects[~additional_subjects['SubjectCode'].isin(unique_subjects['SubjectCode'])] + + # Process subjects from key stage syllabuses first (these have department info) + for _, subject_row in unique_subjects.iterrows(): + subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}" + department_node = node_library['department_nodes'].get(subject_row['Department']) + if not department_node: + logger.warning(f"No department found for subject {subject_row['Subject']} with code {subject_row['SubjectCode']}") + continue + + _, subject_path = fs_handler.create_department_subject_directory( + department_node.path, + subject_row['Subject'] # Use full subject name instead of SubjectCode + ) + logger.info(f"Created subject directory for {subject_path}") + + subject_node = neo_curriculum.SubjectNode( + unique_id=subject_unique_id, + subject_code=subject_row['SubjectCode'], + subject_name=subject_row['Subject'], + path=subject_path + ) + # Create subject in both databases + neon.create_or_merge_neontology_node(subject_node, database=school_db_name, operation='merge') + neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(subject_node.path, subject_node.to_dict()) + node_library['subject_nodes'][subject_row['Subject']] = subject_node + + # Link subject to department in school database only + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentManagesSubject(source=department_node, target=subject_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created subject node for {subject_row['Subject']} and linked to department {subject_row['Department']}") + + # Process any additional subjects from year group syllabuses (these won't have department info) + for _, subject_row in additional_subjects.iterrows(): + subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}" + # Create in a special "Unassigned" department + unassigned_dept_name = "Unassigned Department" + if unassigned_dept_name not in node_library['department_nodes']: + _, dept_path = fs_handler.create_school_department_directory(school_node.path, unassigned_dept_name) + department_node = neo_entity.DepartmentNode( + unique_id=f"Department_{school_node.unique_id}_Unassigned", + department_name=unassigned_dept_name, + path=dept_path + ) + neon.create_or_merge_neontology_node(department_node, database=school_db_name, operation='merge') + fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict()) + node_library['department_nodes'][unassigned_dept_name] = department_node + + # Link unassigned department to department structure + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created unassigned department node and linked to department structure") + + _, subject_path = fs_handler.create_department_subject_directory( + node_library['department_nodes'][unassigned_dept_name].path, + subject_row['Subject'] + ) + + subject_node = neo_curriculum.SubjectNode( + unique_id=subject_unique_id, + subject_code=subject_row['SubjectCode'], + subject_name=subject_row['Subject'], + path=subject_path + ) + # Create subject in both databases + neon.create_or_merge_neontology_node(subject_node, database=school_db_name, operation='merge') + neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(subject_node.path, subject_node.to_dict()) + node_library['subject_nodes'][subject_row['Subject']] = subject_node + + # Link subject to unassigned department in school database only + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentManagesSubject( + source=node_library['department_nodes'][unassigned_dept_name], + target=subject_node + ), + database=school_db_name, operation='merge' + ) + logger.warning(f"Created subject node for {subject_row['Subject']} in unassigned department") + + # Process key stages and syllabuses + logger.info(f"Processing key stages") + last_key_stage_node = None + # Track last syllabus nodes per subject + last_key_stage_syllabus_nodes = {} # Dictionary to track last key stage syllabus node per subject + last_year_group_syllabus_nodes = {} # Dictionary to track last year group syllabus node per subject + topics_processed = set() # Track which topics have been processed + lessons_processed = set() # Track which lessons have been processed + statements_processed = set() # Track which statements have been processed + + # First create all key stage nodes and key stage syllabus nodes + for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows(): + key_stage = str(ks_row['KeyStage']) + logger.debug(f"Processing key stage syllabus row - Subject: {ks_row['Subject']}, Key Stage: {key_stage}") + + subject_node = node_library['subject_nodes'].get(ks_row['Subject']) + if not subject_node: + logger.warning(f"No subject node found for subject {ks_row['Subject']}") + continue + + if key_stage not in key_stage_nodes_created: + key_stage_node_unique_id = f"KeyStage_{curriculum_node.unique_id}_KStg{key_stage}" + key_stage_node = neo_curriculum.KeyStageNode( + unique_id=key_stage_node_unique_id, + key_stage_name=f"Key Stage {key_stage}", + key_stage=str(key_stage), + path=os.path.join(curriculum_node.path, "key_stages", f"KS{key_stage}") + ) + # Create key stage node in both databases + neon.create_or_merge_neontology_node(key_stage_node, database=school_db_name, operation='merge') + neon.create_or_merge_neontology_node(key_stage_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(key_stage_node.path, key_stage_node.to_dict()) + key_stage_nodes_created[key_stage] = key_stage_node + node_library['key_stage_nodes'][key_stage] = key_stage_node + + # Create relationship with curriculum structure in school database only + neon.create_or_merge_neontology_relationship( + curriculum_relationships.CurriculumStructureIncludesKeyStage(source=curriculum_node, target=key_stage_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created key stage node {key_stage_node_unique_id} and relationship with curriculum structure") + + # Create sequential relationship between key stages in both databases + if last_key_stage_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between key stages {last_key_stage_node.unique_id} and {key_stage_node.unique_id}") + last_key_stage_node = key_stage_node + + # Create key stage syllabus under the subject's curriculum directory + _, key_stage_syllabus_path = fs_handler.create_curriculum_key_stage_syllabus_directory( + curriculum_node.path, + key_stage, + ks_row['Subject'], + ks_row['ID'] + ) + logger.debug(f"Creating key stage syllabus node for {ks_row['Subject']} KS{key_stage} with ID {ks_row['ID']}") + + key_stage_syllabus_node_unique_id = f"KeyStageSyllabus_{curriculum_node.unique_id}_{ks_row['Title'].replace(' ', '')}" + key_stage_syllabus_node = neo_curriculum.KeyStageSyllabusNode( + unique_id=key_stage_syllabus_node_unique_id, + ks_syllabus_id=ks_row['ID'], + ks_syllabus_name=ks_row['Title'], + ks_syllabus_key_stage=str(ks_row['KeyStage']), + ks_syllabus_subject=ks_row['Subject'], + ks_syllabus_subject_code=ks_row['Subject'], + path=key_stage_syllabus_path + ) + # Create key stage syllabus node in both databases + neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=school_db_name, operation='merge') + neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(key_stage_syllabus_node.path, key_stage_syllabus_node.to_dict()) + node_library['key_stage_syllabus_nodes'][ks_row['ID']] = key_stage_syllabus_node + logger.debug(f"Created key stage syllabus node {key_stage_syllabus_node_unique_id} for {ks_row['Subject']} KS{key_stage}") + + # Link key stage syllabus to its subject in both databases + if subject_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between subject {subject_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}") + + # Link key stage syllabus to its key stage in both databases + key_stage_node = key_stage_nodes_created.get(key_stage) + if key_stage_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between key stage {key_stage_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}") + + # Create sequential relationship between key stage syllabuses in both databases + last_key_stage_syllabus_node = last_key_stage_syllabus_nodes.get(ks_row['Subject']) + if last_key_stage_syllabus_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between key stage syllabuses {last_key_stage_syllabus_node.unique_id} and {key_stage_syllabus_node.unique_id}") + last_key_stage_syllabus_nodes[ks_row['Subject']] = key_stage_syllabus_node + + # Now process year groups and their syllabuses + for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows(): + key_stage = str(ks_row['KeyStage']) + related_yeargroups = sort_year_groups(yeargroupsyllabus_df[yeargroupsyllabus_df['KeyStage'] == ks_row['KeyStage']]) + + logger.info(f"Processing year groups for key stage {key_stage}") + for yg_index, yg_row in related_yeargroups.iterrows(): + year_group = yg_row['YearGroup'] + subject_code = yg_row['SubjectCode'] + numeric_year_group = pd.to_numeric(year_group, errors='coerce') + + if pd.notna(numeric_year_group): + numeric_year_group = int(numeric_year_group) + if numeric_year_group not in year_group_nodes_created: + # Create year group directory under pastoral structure + _, year_group_path = fs_handler.create_pastoral_year_group_directory(pastoral_node.path, year_group) + logger.info(f"Created year group directory for {year_group_path}") + + year_group_node_unique_id = f"YearGroup_{school_node.unique_id}_YGrp{numeric_year_group}" + year_group_node = neo_curriculum.YearGroupNode( + unique_id=year_group_node_unique_id, + year_group=str(numeric_year_group), + year_group_name=f"Year {numeric_year_group}", + path=year_group_path + ) + # Create year group node in both databases but use same directory + neon.create_or_merge_neontology_node(year_group_node, database=school_db_name, operation='merge') + neon.create_or_merge_neontology_node(year_group_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(year_group_node.path, year_group_node.to_dict()) + + # Create sequential relationship between year groups in both databases + if last_year_group_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between year groups {last_year_group_node.unique_id} and {year_group_node.unique_id} across key stages") + last_year_group_node = year_group_node + + # Create relationship with Pastoral Structure in school database only + neon.create_or_merge_neontology_relationship( + curriculum_relationships.PastoralStructureIncludesYearGroup(source=pastoral_node, target=year_group_node), + database=school_db_name, operation='merge' + ) + logger.info(f"Created year group node {year_group_node_unique_id} and relationship with pastoral structure") + + year_group_nodes_created[numeric_year_group] = year_group_node + node_library['year_group_nodes'][str(numeric_year_group)] = year_group_node + + # Curriculum specific database initialisation begins here + # Create year group syllabus nodes in both databases + year_group_node = year_group_nodes_created.get(numeric_year_group) + if year_group_node: + # Create syllabus directory under curriculum structure + _, year_group_syllabus_path = fs_handler.create_curriculum_year_group_syllabus_directory( + curriculum_node.path, + yg_row['Subject'], + year_group, + yg_row['ID'] + ) + logger.info(f"Created year group syllabus directory for {year_group_syllabus_path}") + + year_group_syllabus_node_unique_id = f"YearGroupSyllabus_{school_node.unique_id}_{yg_row['ID']}" + year_group_syllabus_node = neo_curriculum.YearGroupSyllabusNode( + unique_id=year_group_syllabus_node_unique_id, + yr_syllabus_id=yg_row['ID'], + yr_syllabus_name=yg_row['Title'], + yr_syllabus_year_group=str(yg_row['YearGroup']), + yr_syllabus_subject=yg_row['Subject'], + yr_syllabus_subject_code=yg_row['Subject'], + path=year_group_syllabus_path + ) + + # Create year group syllabus node in both databases but use same directory + neon.create_or_merge_neontology_node(year_group_syllabus_node, database=school_db_name, operation='merge') + neon.create_or_merge_neontology_node(year_group_syllabus_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(year_group_syllabus_node.path, year_group_syllabus_node.to_dict()) + node_library['year_group_syllabus_nodes'][yg_row['ID']] = year_group_syllabus_node + + # Create sequential relationship between year group syllabuses in both databases + last_year_group_syllabus_node = last_year_group_syllabus_nodes.get(yg_row['Subject']) + # Only create sequential relationship if this year group is higher than the last one + if last_year_group_syllabus_node: + last_year = pd.to_numeric(last_year_group_syllabus_node.yr_syllabus_year_group, errors='coerce') + current_year = pd.to_numeric(year_group_syllabus_node.yr_syllabus_year_group, errors='coerce') + if pd.notna(last_year) and pd.notna(current_year) and current_year > last_year: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between year group syllabuses {last_year_group_syllabus_node.unique_id} and {year_group_syllabus_node.unique_id}") + last_year_group_syllabus_nodes[yg_row['Subject']] = year_group_syllabus_node + + # Create relationships in both databases using MATCH to avoid cartesian products + subject_node = node_library['subject_nodes'].get(yg_row['Subject']) + if subject_node: + # Link to subject + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between subject {subject_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Link to year group + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between year group {year_group_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Link to key stage syllabus if it exists for the same subject + key_stage_syllabus_node = node_library['key_stage_syllabus_nodes'].get(ks_row['ID']) + if key_stage_syllabus_node and yg_row['Subject'] == ks_row['Subject']: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node), + database=school_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between key stage syllabus {key_stage_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Processing of curriculum topic begins here + # Process topics for this year group syllabus only if not already processed + topics_for_syllabus = topic_df[topic_df['SyllabusYearID'] == yg_row['ID']] + for _, topic_row in topics_for_syllabus.iterrows(): + if topic_row['TopicID'] in topics_processed: + continue + topics_processed.add(topic_row['TopicID']) + + # Get the correct subject from the topic row + topic_subject = topic_row['SyllabusSubject'] + topic_key_stage = topic_row['SyllabusKeyStage'] + + logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage}") + logger.debug(f"Available key stage syllabus nodes: {[node.ks_syllabus_subject + '_KS' + node.ks_syllabus_key_stage for node in node_library['key_stage_syllabus_nodes'].values()]}") + + # Find the key stage syllabus node by iterating through all nodes + matching_syllabus_node = None + for syllabus_node in node_library['key_stage_syllabus_nodes'].values(): + logger.debug(f"Checking syllabus node - Subject: {syllabus_node.ks_syllabus_subject}, Key Stage: {syllabus_node.ks_syllabus_key_stage}") + logger.debug(f"Comparing with - Subject: {topic_subject}, Key Stage: {str(topic_key_stage)}") + logger.debug(f"Types - Node Subject: {type(syllabus_node.ks_syllabus_subject)}, Topic Subject: {type(topic_subject)}") + logger.debug(f"Types - Node Key Stage: {type(syllabus_node.ks_syllabus_key_stage)}, Topic Key Stage: {type(str(topic_key_stage))}") + + if (syllabus_node.ks_syllabus_subject == topic_subject and + syllabus_node.ks_syllabus_key_stage == str(topic_key_stage)): + matching_syllabus_node = syllabus_node + logger.debug(f"Found matching syllabus node: {syllabus_node.unique_id}") + break + + if not matching_syllabus_node: + logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation") + continue + + + _, topic_path = fs_handler.create_curriculum_topic_directory(matching_syllabus_node.path, topic_row['TopicID']) + logger.info(f"Created topic directory for {topic_path}") + + topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}" + topic_node = neo_curriculum.TopicNode( + unique_id=topic_node_unique_id, + topic_id=topic_row['TopicID'], + topic_title=topic_row.get('TopicTitle', default_topic_values['topic_title']), + total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])), + topic_type=topic_row.get('TopicType', default_topic_values['topic_type']), + topic_assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']), + path=topic_path + ) + # Create topic node in curriculum database only + neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(topic_node.path, topic_node.to_dict()) + node_library['topic_nodes'][topic_row['TopicID']] = topic_node + + # Link topic to key stage syllabus as well as year group syllabus + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node), + database=curriculum_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupSyllabusIncludesTopic(source=year_group_syllabus_node, target=topic_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationships between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Process lessons for this topic only if not already processed + lessons_for_topic = lesson_df[ + (lesson_df['TopicID'] == topic_row['TopicID']) & + (lesson_df['SyllabusSubject'] == topic_subject) + ].copy() + lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str) + lessons_for_topic = lessons_for_topic.sort_values('Lesson') + + previous_lesson_node = None + for _, lesson_row in lessons_for_topic.iterrows(): + if lesson_row['LessonID'] in lessons_processed: + continue + lessons_processed.add(lesson_row['LessonID']) + + _, lesson_path = fs_handler.create_curriculum_lesson_directory(topic_path, lesson_row['LessonID']) + logger.info(f"Created lesson directory for {lesson_path}") + + lesson_data = { + 'unique_id': f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}", + 'topic_lesson_id': lesson_row['LessonID'], + 'topic_lesson_title': lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']), + 'topic_lesson_type': lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']), + 'topic_lesson_length': str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])), + 'topic_lesson_suggested_activities': lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities']), + 'topic_lesson_skills_learned': lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned']), + 'topic_lesson_weblinks': lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks']), + 'path': lesson_path + } + for key, value in lesson_data.items(): + if pd.isna(value): + lesson_data[key] = default_topic_lesson_values.get(key, 'Null') + + lesson_node = neo_curriculum.TopicLessonNode(**lesson_data) + # Create lesson node in curriculum database only + neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(lesson_node.path, lesson_node.to_dict()) + node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node + + # Link lesson to topic + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}") + + # Create sequential relationships between lessons + if lesson_row['Lesson'].isdigit() and previous_lesson_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}") + previous_lesson_node = lesson_node + + # Process learning statements for this lesson only if not already processed + statements_for_lesson = statement_df[ + (statement_df['LessonID'] == lesson_row['LessonID']) & + (statement_df['SyllabusSubject'] == topic_subject) + ] + for _, statement_row in statements_for_lesson.iterrows(): + if statement_row['StatementID'] in statements_processed: + continue + statements_processed.add(statement_row['StatementID']) + + _, statement_path = fs_handler.create_curriculum_learning_statement_directory(lesson_path, statement_row['StatementID']) + + statement_data = { + 'unique_id': f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}", + 'lesson_learning_statement_id': statement_row['StatementID'], + 'lesson_learning_statement': statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']), + 'lesson_learning_statement_type': statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']), + 'path': statement_path + } + for key in statement_data: + if pd.isna(statement_data[key]): + statement_data[key] = default_learning_statement_values.get(key, 'Null') + + statement_node = neo_curriculum.LearningStatementNode(**statement_data) + # Create statement node in curriculum database only + neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(statement_node.path, statement_node.to_dict()) + node_library['statement_nodes'][statement_row['StatementID']] = statement_node + + # Link learning statement to lesson + neon.create_or_merge_neontology_relationship( + curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}") + else: + logger.warning(f"No year group node found for year group {year_group}, skipping syllabus creation") + + # After processing all year groups and their syllabuses, process any remaining topics + logger.info("Processing topics without year groups") + for _, topic_row in topic_df.iterrows(): + if topic_row['TopicID'] in topics_processed: + continue + + topic_subject = topic_row['SyllabusSubject'] + topic_key_stage = topic_row['SyllabusKeyStage'] + + logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage} without year group") + + # Find the key stage syllabus node + matching_syllabus_node = None + for syllabus_node in node_library['key_stage_syllabus_nodes'].values(): + if (syllabus_node.ks_syllabus_subject == topic_subject and + syllabus_node.ks_syllabus_key_stage == str(topic_key_stage)): + matching_syllabus_node = syllabus_node + break + + if not matching_syllabus_node: + logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation") + continue + + _, topic_path = fs_handler.create_curriculum_topic_directory(matching_syllabus_node.path, topic_row['TopicID']) + logger.info(f"Created topic directory for {topic_path}") + + topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}" + topic_node = neo_curriculum.TopicNode( + unique_id=topic_node_unique_id, + topic_id=topic_row['TopicID'], + topic_title=topic_row.get('TopicTitle', default_topic_values['topic_title']), + total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])), + topic_type=topic_row.get('TopicType', default_topic_values['topic_type']), + topic_assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']), + path=topic_path + ) + # Create topic node in curriculum database only + neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(topic_node.path, topic_node.to_dict()) + node_library['topic_nodes'][topic_row['TopicID']] = topic_node + topics_processed.add(topic_row['TopicID']) + + # Link topic to key stage syllabus + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id}") + + # Process lessons for this topic + lessons_for_topic = lesson_df[ + (lesson_df['TopicID'] == topic_row['TopicID']) & + (lesson_df['SyllabusSubject'] == topic_subject) + ].copy() + lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str) + lessons_for_topic = lessons_for_topic.sort_values('Lesson') + + previous_lesson_node = None + for _, lesson_row in lessons_for_topic.iterrows(): + if lesson_row['LessonID'] in lessons_processed: + continue + lessons_processed.add(lesson_row['LessonID']) + + _, lesson_path = fs_handler.create_curriculum_lesson_directory(topic_path, lesson_row['LessonID']) + logger.info(f"Created lesson directory for {lesson_path}") + + lesson_data = { + 'unique_id': f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}", + 'topic_lesson_id': lesson_row['LessonID'], + 'topic_lesson_title': lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']), + 'topic_lesson_type': lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']), + 'topic_lesson_length': str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])), + 'topic_lesson_suggested_activities': lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities']), + 'topic_lesson_skills_learned': lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned']), + 'topic_lesson_weblinks': lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks']), + 'path': lesson_path + } + for key, value in lesson_data.items(): + if pd.isna(value): + lesson_data[key] = default_topic_lesson_values.get(key, 'Null') + + lesson_node = neo_curriculum.TopicLessonNode(**lesson_data) + # Create lesson node in curriculum database only + neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(lesson_node.path, lesson_node.to_dict()) + node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node + + # Link lesson to topic + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}") + + # Create sequential relationships between lessons + if lesson_row['Lesson'].isdigit() and previous_lesson_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}") + previous_lesson_node = lesson_node + + # Process learning statements for this lesson + statements_for_lesson = statement_df[ + (statement_df['LessonID'] == lesson_row['LessonID']) & + (statement_df['SyllabusSubject'] == topic_subject) + ] + for _, statement_row in statements_for_lesson.iterrows(): + if statement_row['StatementID'] in statements_processed: + continue + statements_processed.add(statement_row['StatementID']) + + _, statement_path = fs_handler.create_curriculum_learning_statement_directory(lesson_path, statement_row['StatementID']) + + statement_data = { + 'unique_id': f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}", + 'lesson_learning_statement_id': statement_row['StatementID'], + 'lesson_learning_statement': statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']), + 'lesson_learning_statement_type': statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']), + 'path': statement_path + } + for key in statement_data: + if pd.isna(statement_data[key]): + statement_data[key] = default_learning_statement_values.get(key, 'Null') + + statement_node = neo_curriculum.LearningStatementNode(**statement_data) + # Create statement node in curriculum database only + neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge') + fs_handler.create_default_tldraw_file(statement_node.path, statement_node.to_dict()) + node_library['statement_nodes'][statement_row['StatementID']] = statement_node + + # Link learning statement to lesson + neon.create_or_merge_neontology_relationship( + curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}") + + return node_library \ No newline at end of file diff --git a/modules/database/admin/school_manager.py b/modules/database/admin/school_manager.py new file mode 100644 index 0000000..d7eb990 --- /dev/null +++ b/modules/database/admin/school_manager.py @@ -0,0 +1,512 @@ +import os +from modules.logger_tool import initialise_logger +from supabase import create_client +import json +import pandas as pd + +import modules.database.init.xl_tools as xl +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools +import modules.database.schemas.nodes.schools.schools as school_schemas +import modules.database.schemas.nodes.schools.curriculum as curriculum_schemas +import modules.database.schemas.nodes.schools.pastoral as pastoral_schemas +import modules.database.schemas.nodes.structures.schools as school_structures +import modules.database.schemas.entities as entities +from modules.database.admin.neontology_provider import NeontologyProvider +from modules.database.admin.graph_provider import GraphNamingProvider +from modules.database.schemas.relationships import curriculum_relationships, entity_relationships, entity_curriculum_rels + +class SchoolManager: + def __init__(self): + self.driver = driver_tools.get_driver() + self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + self.neontology = NeontologyProvider() + self.graph_naming = GraphNamingProvider() + + # Initialize Supabase client with correct URL and service role key + supabase_url = os.getenv("SUPABASE_URL") + service_role_key = os.getenv("SERVICE_ROLE_KEY") + + self.logger.info(f"Initializing Supabase client with URL: {supabase_url}") + self.supabase = create_client(supabase_url, service_role_key) + + # Set headers for admin operations + self.supabase.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json" + } + + # Set storage client headers explicitly + self.supabase.storage._client.headers.update({ + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json" + }) + + def create_schools_database(self): + """Creates the main cc.institutes database in Neo4j""" + try: + db_name = "cc.institutes" + with self.driver.session() as session: + return self._extracted_from_create_private_database( + session, db_name, f'Created database {db_name}' + ) + except Exception as e: + self.logger.error(f"Error creating schools database: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_school_node(self, school_data): + """Creates a school node in cc.institutes database and stores TLDraw file in Supabase""" + try: + # Convert Supabase school data to SchoolNode using GraphNamingProvider + school_unique_id = self.graph_naming.get_school_unique_id(school_data['urn']) + school_path = self.graph_naming.get_school_path("cc.institutes", school_data['urn']) + + school_node = entities.school_schemas.SchoolNode( + unique_id=school_unique_id, + path=school_path, + urn=school_data['urn'], + establishment_number=school_data['establishment_number'], + establishment_name=school_data['establishment_name'], + establishment_type=school_data['establishment_type'], + establishment_status=school_data['establishment_status'], + phase_of_education=school_data['phase_of_education'] if school_data['phase_of_education'] not in [None, ''] else None, + statutory_low_age=int(school_data['statutory_low_age']) if school_data.get('statutory_low_age') is not None else 0, + statutory_high_age=int(school_data['statutory_high_age']) if school_data.get('statutory_high_age') is not None else 0, + religious_character=school_data.get('religious_character') if school_data.get('religious_character') not in [None, ''] else None, + school_capacity=int(school_data['school_capacity']) if school_data.get('school_capacity') is not None else 0, + school_website=school_data.get('school_website', ''), + ofsted_rating=school_data.get('ofsted_rating') if school_data.get('ofsted_rating') not in [None, ''] else None + ) + + # Create default tldraw file data + tldraw_data = { + "document": { + "version": 1, + "id": school_data['urn'], + "name": school_data['establishment_name'], + "meta": { + "created_at": "", + "updated_at": "", + "creator_id": "", + "is_template": False, + "is_snapshot": False, + "is_draft": False, + "template_id": None, + "snapshot_id": None, + "draft_id": None + } + }, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": {} + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 22 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "cc-school-node": 1 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "store": { + "document:document": { + "gridSize": 10, + "name": school_data['establishment_name'], + "meta": {}, + "id": school_data['urn'], + "typeName": "document" + }, + "page:page": { + "meta": {}, + "id": "page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + "shape:school-node": { + "x": 0, + "y": 0, + "rotation": 0, + "type": "cc-school-node", + "id": school_unique_id, + "parentId": "page", + "index": "a1", + "props": school_node.to_dict(), + "typeName": "shape" + }, + "instance:instance": { + "id": "instance", + "currentPageId": "page", + "typeName": "instance" + }, + "camera:camera": { + "x": 0, + "y": 0, + "z": 1, + "id": "camera", + "typeName": "camera" + } + } + } + + # Store tldraw file in Supabase storage + file_path = f"{school_data['urn']}/tldraw.json" + file_options = { + "content-type": "application/json", + "x-upsert": "true", # Update if exists + "metadata": { + "establishment_urn": school_data['urn'], + "establishment_name": school_data['establishment_name'] + } + } + + try: + # Create a fresh service role client for storage operations + self.logger.info("Creating fresh service role client for storage operations") + service_client = create_client( + os.getenv("SUPABASE_URL"), + os.getenv("SERVICE_ROLE_KEY") + ) + + self.logger.debug(f"Service client created with URL: {os.getenv('SUPABASE_URL')}") + + service_client.headers = { + "apiKey": os.getenv("SERVICE_ROLE_KEY"), + "Authorization": f"Bearer {os.getenv('SERVICE_ROLE_KEY')}", + "Content-Type": "application/json" + } + service_client.storage._client.headers.update({ + "apiKey": os.getenv("SERVICE_ROLE_KEY"), + "Authorization": f"Bearer {os.getenv('SERVICE_ROLE_KEY')}", + "Content-Type": "application/json" + }) + + self.logger.debug("Headers set for service client and storage client") + + # Upload to Supabase storage using service role client + self.logger.info(f"Uploading tldraw file for school {school_data['urn']}") + self.logger.debug(f"File path: {file_path}") + self.logger.debug(f"File options: {file_options}") + + # First, ensure the bucket exists + self.logger.info("Checking if bucket cc.institutes exists") + try: + bucket = service_client.storage.get_bucket("cc.institutes") + self.logger.info("Bucket cc.institutes exists") + except Exception as bucket_error: + self.logger.error(f"Error checking bucket: {str(bucket_error)}") + if hasattr(bucket_error, 'response'): + self.logger.error(f"Bucket error response: {bucket_error.response.text if hasattr(bucket_error.response, 'text') else bucket_error.response}") + raise bucket_error + + # Attempt the upload + self.logger.info("Attempting file upload") + result = service_client.storage.from_("cc.institutes").upload( + path=file_path, + file=json.dumps(tldraw_data).encode(), + file_options=file_options + ) + self.logger.info(f"Upload successful. Result: {result}") + + except Exception as upload_error: + self.logger.error(f"Error uploading tldraw file: {str(upload_error)}") + if hasattr(upload_error, 'response'): + self.logger.error(f"Upload error response: {upload_error.response.text if hasattr(upload_error.response, 'text') else upload_error.response}") + raise upload_error + + # Create node in Neo4j using Neontology + with self.neontology as neo: + self.logger.info(f"Creating school node in Neo4j: {school_node.to_dict()}") + neo.create_or_merge_node(school_node, database="cc.institutes", operation="merge") + return {"status": "success", "node": school_node} + + except Exception as e: + self.logger.error(f"Error creating school node: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_private_database(self, school_data): + """Creates a private database for a specific school""" + try: + private_db_name = f"cc.institutes.{school_data['urn']}" + with self.driver.session() as session: + return self._extracted_from_create_private_database( + session, private_db_name, 'Created private database ' + ) + except Exception as e: + self.logger.error(f"Error creating private database: {str(e)}") + return {"status": "error", "message": str(e)} + + # TODO Rename this here and in `create_schools_database` and `create_private_database` + def _extracted_from_create_private_database(self, session, arg1, arg2): + session_tools.create_database(session, arg1) + self.logger.info(f"{arg2}{arg1}") + return { + "status": "success", + "message": f"Database {arg1} created successfully", + } + + def create_basic_structure(self, school_node, database_name): + """Creates basic structural nodes in the specified database""" + try: + # Create filesystem paths + fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school") + + # Create Department Structure node + department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}" + _, department_path = fs_handler.create_school_department_directory(school_node.path, "departments") + department_structure_node = entities.school_schemas.DepartmentNode( + unique_id=department_structure_node_unique_id, + path=department_path + ) + + # Create Curriculum Structure node + _, curriculum_path = fs_handler.create_school_curriculum_directory(school_node.path) + curriculum_node = school_structures.CurriculumStructureNode( + unique_id=f"CurriculumStructure_{school_node.unique_id}", + path=curriculum_path + ) + + # Create Pastoral Structure node + _, pastoral_path = fs_handler.create_school_pastoral_directory(school_node.path) + pastoral_node = school_structures.PastoralStructureNode( + unique_id=f"PastoralStructure_{school_node.unique_id}", + path=pastoral_path + ) + + with self.neontology as neo: + # Create nodes + neo.create_or_merge_node(department_structure_node, database=str(database_name), operation='merge') + fs_handler.create_default_tldraw_file(department_structure_node.path, department_structure_node.to_dict()) + + neo.create_or_merge_node(curriculum_node, database=str(database_name), operation='merge') + fs_handler.create_default_tldraw_file(curriculum_node.path, curriculum_node.to_dict()) + + neo.create_or_merge_node(pastoral_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(pastoral_node.path, pastoral_node.to_dict()) + + # Create relationships + neo.create_or_merge_relationship( + entity_relationships.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node), + database=database_name, operation='merge' + ) + + neo.create_or_merge_relationship( + entity_curriculum_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node), + database=database_name, operation='merge' + ) + + neo.create_or_merge_relationship( + entity_curriculum_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node), + database=database_name, operation='merge' + ) + + return { + "status": "success", + "message": "Basic structure created successfully", + "nodes": { + "department_structure": department_structure_node, + "curriculum_structure": curriculum_node, + "pastoral_structure": pastoral_node + } + } + + except Exception as e: + self.logger.error(f"Error creating basic structure: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_detailed_structure(self, school_node, database_name, excel_file): + """Creates detailed structural nodes from Excel file""" + try: + # First, store the Excel file in Supabase + file_path = f"{school_node.urn}/structure.xlsx" + file_options = { + "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "x-upsert": "true" + } + + # Upload Excel file to storage + self.supabase.storage.from_("cc.institutes").upload( + path=file_path, + file=excel_file, + file_options=file_options + ) + + # Process Excel file + dataframes = xl.create_dataframes(excel_file) + + # Get existing basic structure nodes + with self.neontology as neo: + result = neo.cypher_read(""" + MATCH (s:School {unique_id: $school_id}) + OPTIONAL MATCH (s)-[:HAS_DEPARTMENT_STRUCTURE]->(ds:DepartmentStructure) + OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(cs:CurriculumStructure) + OPTIONAL MATCH (s)-[:HAS_PASTORAL_STRUCTURE]->(ps:PastoralStructure) + RETURN ds, cs, ps + """, {"school_id": school_node.unique_id}, database=database_name) + + if not result: + raise Exception("Basic structure not found") + + department_structure = result['ds'] + curriculum_structure = result['cs'] + pastoral_structure = result['ps'] + + # Create departments and subjects + unique_departments = dataframes['keystagesyllabuses']['Department'].dropna().unique() + + fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school") + node_library = {} + + with self.neontology as neo: + for department_name in unique_departments: + _, department_path = fs_handler.create_school_department_directory(school_node.path, department_name) + + department_node = school_schemas.DepartmentNode( + unique_id=f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}", + department_name=department_name, + path=department_path + ) + neo.create_or_merge_node(department_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict()) + node_library[f'department_{department_name}'] = department_node + + # Link to department structure + neo.create_or_merge_relationship( + entity_relationships.DepartmentStructureHasDepartment( + source=department_structure, + target=department_node + ), + database=database_name, + operation='merge' + ) + + # Create year groups + year_groups = self.sort_year_groups(dataframes['yeargroupsyllabuses'])['YearGroup'].unique() + last_year_group_node = None + + for year_group in year_groups: + numeric_year_group = pd.to_numeric(year_group, errors='coerce') + if pd.notna(numeric_year_group): + _, year_group_path = fs_handler.create_pastoral_year_group_directory( + pastoral_structure.path, + str(int(numeric_year_group)) + ) + + year_group_node = pastoral_schemas.YearGroupNode( + unique_id=f"YearGroup_{school_node.unique_id}_YGrp{int(numeric_year_group)}", + year_group=str(int(numeric_year_group)), + year_group_name=f"Year {int(numeric_year_group)}", + path=year_group_path + ) + neo.create_or_merge_node(year_group_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(year_group_node.path, year_group_node.to_dict()) + node_library[f'year_group_{int(numeric_year_group)}'] = year_group_node + + # Create sequential relationship + if last_year_group_node: + neo.create_or_merge_relationship( + curriculum_relationships.YearGroupFollowsYearGroup( + source=last_year_group_node, + target=year_group_node + ), + database=database_name, + operation='merge' + ) + last_year_group_node = year_group_node + + # Link to pastoral structure + neo.create_or_merge_relationship( + curriculum_relationships.PastoralStructureIncludesYearGroup( + source=pastoral_structure, + target=year_group_node + ), + database=database_name, + operation='merge' + ) + + # Create key stages + key_stages = dataframes['keystagesyllabuses']['KeyStage'].unique() + last_key_stage_node = None + + for key_stage in sorted(key_stages): + _, key_stage_path = fs_handler.create_curriculum_key_stage_directory( + curriculum_structure.path, + str(key_stage) + ) + + key_stage_node = curriculum_schemas.KeyStageNode( + unique_id=f"KeyStage_{curriculum_structure.unique_id}_KStg{key_stage}", + key_stage_name=f"Key Stage {key_stage}", + key_stage=str(key_stage), + path=key_stage_path + ) + neo.create_or_merge_node(key_stage_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(key_stage_node.path, key_stage_node.to_dict()) + node_library[f'key_stage_{key_stage}'] = key_stage_node + + # Create sequential relationship + if last_key_stage_node: + neo.create_or_merge_relationship( + curriculum_relationships.KeyStageFollowsKeyStage( + source=last_key_stage_node, + target=key_stage_node + ), + database=database_name, + operation='merge' + ) + last_key_stage_node = key_stage_node + + # Link to curriculum structure + neo.create_or_merge_relationship( + curriculum_relationships.CurriculumStructureIncludesKeyStage( + source=curriculum_structure, + target=key_stage_node + ), + database=database_name, + operation='merge' + ) + + return { + "status": "success", + "message": "Detailed structure created successfully", + "node_library": node_library + } + + except Exception as e: + self.logger.error(f"Error creating detailed structure: {str(e)}") + return {"status": "error", "message": str(e)} + + def sort_year_groups(self, df): + df = df.copy() + df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce') + return df.sort_values(by='YearGroupNumeric') diff --git a/modules/database/admin/school_syllabus_provider.py b/modules/database/admin/school_syllabus_provider.py new file mode 100644 index 0000000..890a6a2 --- /dev/null +++ b/modules/database/admin/school_syllabus_provider.py @@ -0,0 +1,54 @@ +import os +from modules.logger_tool import initialise_logger +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools +import modules.database.tools.neontology_tools as neon +import modules.database.schemas.entities as neo_entity +import modules.database.schemas.nodes.schools.curriculum as curriculum_schemas +import modules.database.schemas.relationships.curriculum_relationships as curriculum_relationships +import modules.database.schemas.relationships.entity_relationships as ent_rels +import modules.database.schemas.relationships.entity_curriculum_rels as ent_cur_rels +import pandas as pd + +class SchoolSyllabusProvider: + def __init__(self): + self.driver = driver_tools.get_driver() + self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + + def process_syllabus_data(self, school_node, database_name, dataframes): + """Process syllabus data from Excel file and create nodes in the database""" + try: + # This method will contain the syllabus-specific processing code from the + # original SchoolCurriculumProvider, starting from where the comment + # "# Curriculum specific database initialisation begins here" was placed + + # We'll implement this in the next iteration after confirming the basic + # structure changes work correctly + + return { + "status": "success", + "message": "Syllabus data processed successfully" + } + + except Exception as e: + self.logger.error(f"Error processing syllabus data: {str(e)}") + return {"status": "error", "message": str(e)} + + def check_syllabus_status(self, school_node, database_name): + """Check if syllabus data exists in the database""" + try: + with self.driver.session(database=database_name) as session: + result = session.run(""" + MATCH (s:School {unique_id: $school_id}) + OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(:CurriculumStructure)-[:INCLUDES_KEY_STAGE]->(:KeyStage)-[:INCLUDES_KEY_STAGE_SYLLABUS]->(ks:KeyStageSyllabus) + OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(:CurriculumStructure)-[:INCLUDES_KEY_STAGE]->(:KeyStage)-[:INCLUDES_YEAR_GROUP_SYLLABUS]->(ys:YearGroupSyllabus) + RETURN count(ks) > 0 OR count(ys) > 0 as has_syllabus + """, school_id=school_node.unique_id) + + has_syllabus = result.single()["has_syllabus"] + + return {"has_syllabus": has_syllabus} + + except Exception as e: + self.logger.error(f"Error checking syllabus status: {str(e)}") + raise diff --git a/modules/database/admin/school_timetable_provider.py b/modules/database/admin/school_timetable_provider.py new file mode 100644 index 0000000..0fe68f0 --- /dev/null +++ b/modules/database/admin/school_timetable_provider.py @@ -0,0 +1,526 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +import modules.database.init.init_calendar as init_calendar +import modules.database.schemas.nodes.schools.timetable as timetable +import modules.database.schemas.relationships.timetables as tt_rels +import modules.database.schemas.relationships.entity_timetable_rels as entity_tt_rels +import modules.database.schemas.relationships.calendar_timetable_rels as cal_tt_rels +import modules.database.tools.neontology_tools as neon +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +from datetime import timedelta, datetime +import pandas as pd + +def create_school_timetable_from_dataframes(dataframes, db_name, school_node=None): + logger.info(f"Creating school timetable for {db_name}") + if dataframes is None: + raise ValueError("Data is required to create the calendar and timetable.") + + logger.info("Initialising neo4j connection...") + neon.init_neontology_connection() + + # Initialize the filesystem handler + fs_handler = ClassroomCopilotFilesystem(db_name, init_run_type="school") + + school_df = dataframes['school'] + if school_node is None: + logger.info("School node is None, using school data from dataframe") + school_unique_id = school_df[school_df['Identifier'] == 'SchoolID']['Data'].iloc[0] + else: + logger.info(f"School node is not None, using school data from school node: {school_node}") + school_unique_id = school_node.unique_id + + terms_df = dataframes['terms'] + weeks_df = dataframes['weeks'] + days_df = dataframes['days'] + periods_df = dataframes['periods'] + + school_df_year_start = school_df[school_df['Identifier'] == 'AcademicYearStart']['Data'].iloc[0] + school_df_year_end = school_df[school_df['Identifier'] == 'AcademicYearEnd']['Data'].iloc[0] + if isinstance(school_df_year_start, str): + school_year_start_date = datetime.strptime(school_df_year_start, '%Y-%m-%d') + else: + school_year_start_date = school_df_year_start + if isinstance(school_df_year_end, str): + school_year_end_date = datetime.strptime(school_df_year_end, '%Y-%m-%d') + else: + school_year_end_date = school_df_year_end + + # Create a dictionary to store the timetable nodes + timetable_nodes = { + 'timetable_node': None, + 'academic_year_nodes': [], + 'academic_term_nodes': [], + 'academic_week_nodes': [], + 'academic_day_nodes': [], + 'academic_period_nodes': [] + } + + if school_node: + # Create the root timetable directory + _, timetable_path = fs_handler.create_school_timetable_directory(school_node.path) + else: + # Create the root timetable directory + _, timetable_path = fs_handler.create_school_timetable_directory() + + # Create AcademicTimetable Node + school_timetable_unique_id = f"SchoolTimetable_{school_unique_id}_{school_year_start_date.year}_{school_year_end_date.year}" + school_timetable_node = timetable.SchoolTimetableNode( + unique_id=school_timetable_unique_id, + start_date=school_year_start_date, + end_date=school_year_end_date, + path=timetable_path + ) + neon.create_or_merge_neontology_node(school_timetable_node, database=db_name, operation='merge') + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(school_timetable_node.path, school_timetable_node.to_dict()) + timetable_nodes['timetable_node'] = school_timetable_node + + if school_node: + logger.info(f"Creating calendar for {school_unique_id} from Neo4j SchoolNode: {school_node.unique_id}") + calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=True, entity_node=school_node) + # Link the school node to the timetable node + neon.create_or_merge_neontology_relationship( + entity_tt_rels.SchoolHasTimetable(source=school_node, target=school_timetable_node), + database=db_name, operation='merge' + ) + timetable_nodes['calendar_nodes'] = calendar_nodes + else: + logger.info(f"Creating calendar for {school_unique_id} from dataframe SchoolID: {school_unique_id}") + calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=False, entity_node=None) + + # Create AcademicYear nodes for each year within the range + for year in range(school_year_start_date.year, school_year_end_date.year + 1): + _, timetable_year_path = fs_handler.create_school_timetable_year_directory(timetable_path, year) + year_str = str(year) + academic_year_unique_id = f"AcademicYear_{school_timetable_unique_id}_{year}" + academic_year_node = timetable.AcademicYearNode( + unique_id=academic_year_unique_id, + year=year_str, + path=timetable_year_path + ) + neon.create_or_merge_neontology_node(academic_year_node, database=db_name, operation='merge') + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(academic_year_node.path, academic_year_node.to_dict()) + timetable_nodes['academic_year_nodes'].append(academic_year_node) + logger.info(f'Created academic year node: {academic_year_node.unique_id}') + neon.create_or_merge_neontology_relationship( + tt_rels.AcademicTimetableHasAcademicYear(source=school_timetable_node, target=academic_year_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {school_timetable_node.unique_id} to {academic_year_node.unique_id}") + + # Link the academic year with the corresponding calendar year node + for year_node in calendar_nodes['calendar_year_nodes']: + if year_node.year == year: + neon.create_or_merge_neontology_relationship( + cal_tt_rels.AcademicYearIsCalendarYear(source=academic_year_node, target=year_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {year_node.unique_id}") + break + + # Create Term and TermBreak nodes linked to AcademicYear + term_number = 1 + academic_term_number = 1 + for _, term_row in terms_df.iterrows(): + term_node_class = timetable.AcademicTermNode if term_row['TermType'] == 'Term' else timetable.AcademicTermBreakNode + term_name = term_row['TermName'] + term_name_no_spaces = term_name.replace(' ', '') + term_start_date = term_row['StartDate'] + if isinstance(term_start_date, pd.Timestamp): + term_start_date = term_start_date.strftime('%Y-%m-%d') + + term_end_date = term_row['EndDate'] + if isinstance(term_end_date, pd.Timestamp): + term_end_date = term_end_date.strftime('%Y-%m-%d') + + if term_row['TermType'] == 'Term': + _, timetable_term_path = fs_handler.create_school_timetable_academic_term_directory( + timetable_path=timetable_path, + term_name=term_name, + term_number=academic_term_number + ) + term_node_unique_id = f"AcademicTerm_{school_timetable_unique_id}_{academic_term_number}_{term_name_no_spaces}" + academic_term_number_str = str(academic_term_number) + term_node = term_node_class( + unique_id=term_node_unique_id, + term_name=term_name, + term_number=academic_term_number_str, + start_date=datetime.strptime(term_start_date, '%Y-%m-%d'), + end_date=datetime.strptime(term_end_date, '%Y-%m-%d'), + path=timetable_term_path + ) + academic_term_number += 1 + else: + term_break_node_unique_id = f"AcademicTermBreak_{school_timetable_unique_id}_{term_name_no_spaces}" + term_node = term_node_class( + unique_id=term_break_node_unique_id, + term_break_name=term_name, + start_date=datetime.strptime(term_start_date, '%Y-%m-%d'), + end_date=datetime.strptime(term_end_date, '%Y-%m-%d') + ) + neon.create_or_merge_neontology_node(term_node, database=db_name, operation='merge') + if isinstance(term_node, timetable.AcademicTermNode): + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(term_node.path, term_node.to_dict()) + logger.info(f'Created academic term break node: {term_node.unique_id}') + timetable_nodes['academic_term_nodes'].append(term_node) + term_number += 1 # We don't use this but we could + + # Link term node to the correct academic year + term_years = set() + term_years.update([term_node.start_date.year, term_node.end_date.year]) + + for academic_year_node in timetable_nodes['academic_year_nodes']: + if int(academic_year_node.year) in term_years: + relationship_class = tt_rels.AcademicYearHasAcademicTerm if term_row['TermType'] == 'Term' else tt_rels.AcademicYearHasAcademicTermBreak + neon.create_or_merge_neontology_relationship( + relationship_class(source=academic_year_node, target=term_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {term_node.unique_id}") + + # Create Week nodes + academic_week_number = 1 + for _, week_row in weeks_df.iterrows(): + week_node_class = timetable.HolidayWeekNode if week_row['WeekType'] == 'Holiday' else timetable.AcademicWeekNode + week_start_date = week_row['WeekStart'] + if isinstance(week_start_date, pd.Timestamp): + week_start_date = week_start_date.strftime('%Y-%m-%d') + + if week_row['WeekType'] == 'Holiday': + week_node_unique_id = f"{week_row['WeekType']}Week_{school_timetable_unique_id}_Week_{week_row['WeekNumber']}" + week_node = week_node_class( + unique_id=week_node_unique_id, + start_date=datetime.strptime(week_start_date, '%Y-%m-%d') + ) + else: + _, timetable_week_path = fs_handler.create_school_timetable_academic_week_directory( + timetable_path=timetable_path, + week_number=academic_week_number + ) + week_node_unique_id = f"AcademicWeek_{school_timetable_unique_id}_Week_{week_row['WeekNumber']}" + academic_week_number_str = str(academic_week_number) + week_type = week_row['WeekType'] + week_node = week_node_class( + unique_id=week_node_unique_id, + academic_week_number=academic_week_number_str, + start_date=datetime.strptime(week_start_date, '%Y-%m-%d'), + week_type=week_type, + path=timetable_week_path + ) + academic_week_number += 1 + neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge') + timetable_nodes['academic_week_nodes'].append(week_node) + logger.info(f"Created week node: {week_node.unique_id}") + if isinstance(week_node, timetable.AcademicWeekNode): + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(week_node.path, week_node.to_dict()) + for calendar_node in calendar_nodes['calendar_week_nodes']: + if calendar_node.start_date == week_node.start_date: + if isinstance(week_node, timetable.AcademicWeekNode): + neon.create_or_merge_neontology_relationship( + cal_tt_rels.AcademicWeekIsCalendarWeek(source=week_node, target=calendar_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {calendar_node.unique_id} to {week_node.unique_id}") + elif isinstance(week_node, timetable.HolidayWeekNode): + neon.create_or_merge_neontology_relationship( + cal_tt_rels.HolidayWeekIsCalendarWeek(source=week_node, target=calendar_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {calendar_node.unique_id} to {week_node.unique_id}") + break + + # Link week node to the correct academic term + for term_node in timetable_nodes['academic_term_nodes']: + if term_node.start_date <= week_node.start_date <= term_node.end_date: + relationship_class = tt_rels.AcademicTermHasAcademicWeek if week_row['WeekType'] != 'Holiday' else tt_rels.AcademicTermBreakHasHolidayWeek + neon.create_or_merge_neontology_relationship( + relationship_class(source=term_node, target=week_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {term_node.unique_id} to {week_node.unique_id}") + break + + # Link week node to the correct academic year + for academic_year_node in timetable_nodes['academic_year_nodes']: + if int(academic_year_node.year) == week_node.start_date.year: + relationship_class = tt_rels.AcademicYearHasAcademicWeek if week_row['WeekType'] != 'Holiday' else tt_rels.AcademicYearHasHolidayWeek + neon.create_or_merge_neontology_relationship( + relationship_class(source=academic_year_node, target=week_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {week_node.unique_id}") + break + + # Create Day nodes + day_number = 1 + academic_day_number = 1 + for _, day_row in days_df.iterrows(): + date_str = day_row['Date'] + if isinstance(date_str, pd.Timestamp): + date_str = date_str.strftime('%Y-%m-%d') + + day_node_class = { + 'Academic': timetable.AcademicDayNode, + 'Holiday': timetable.HolidayDayNode, + 'OffTimetable': timetable.OffTimetableDayNode, + 'StaffDay': timetable.StaffDayNode + }[day_row['DayType']] + + # Format the unique ID as {day_node_class.__name__}Day + day_node_data = { + 'unique_id': f"{day_node_class.__name__}Day_{school_timetable_unique_id}_{day_number}", + 'date': datetime.strptime(date_str, '%Y-%m-%d'), + 'day_of_week': datetime.strptime(date_str, '%Y-%m-%d').strftime('%A') + } + + if day_row['DayType'] == 'Academic': + day_node_data['academic_day'] = str(academic_day_number) + day_node_data['day_type'] = day_row['WeekType'] + _, timetable_day_path = fs_handler.create_school_timetable_academic_day_directory( + timetable_path=timetable_path, + academic_day=academic_day_number + ) + day_node_data['path'] = timetable_day_path + + day_node = day_node_class(**day_node_data) + + for calendar_node in calendar_nodes['calendar_day_nodes']: + if calendar_node.date == day_node.date: + neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge') + timetable_nodes['academic_day_nodes'].append(day_node) + logger.info(f"Created day node: {day_node.unique_id}") + + if isinstance(day_node, timetable.AcademicDayNode): + fs_handler.create_default_tldraw_file(day_node.path, day_node.to_dict()) + relationship_class = cal_tt_rels.AcademicDayIsCalendarDay + elif isinstance(day_node, timetable.HolidayDayNode): + relationship_class = cal_tt_rels.HolidayDayIsCalendarDay + elif isinstance(day_node, timetable.OffTimetableDayNode): + relationship_class = cal_tt_rels.OffTimetableDayIsCalendarDay + elif isinstance(day_node, timetable.StaffDayNode): + relationship_class = cal_tt_rels.StaffDayIsCalendarDay + + neon.create_or_merge_neontology_relationship( + relationship_class(source=day_node, target=calendar_node), + database=db_name, operation='merge' + ) + logger.info(f'Created relationship from {calendar_node.unique_id} to {day_node.unique_id}') + break + + # Link day node to the correct academic week + for academic_week_node in timetable_nodes['academic_week_nodes']: + if academic_week_node.start_date <= day_node.date <= (academic_week_node.start_date + timedelta(days=6)): + if day_row['DayType'] == 'Academic': + relationship_class = tt_rels.AcademicWeekHasAcademicDay + elif day_row['DayType'] == 'Holiday': + if hasattr(academic_week_node, 'week_type') and academic_week_node.week_type in ['A', 'B']: + relationship_class = tt_rels.AcademicWeekHasHolidayDay + else: + relationship_class = tt_rels.HolidayWeekHasHolidayDay + elif day_row['DayType'] == 'OffTimetable': + relationship_class = tt_rels.AcademicWeekHasOffTimetableDay + elif day_row['DayType'] == 'Staff': + relationship_class = tt_rels.AcademicWeekHasStaffDay + else: + continue # Skip linking for other day types + neon.create_or_merge_neontology_relationship( + relationship_class(source=academic_week_node, target=day_node), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {academic_week_node.unique_id} to {day_node.unique_id}") + break + + # Link day node to the correct academic term + for term_node in timetable_nodes['academic_term_nodes']: + if term_node.start_date <= day_node.date <= term_node.end_date: + if day_row['DayType'] == 'Academic': + relationship_class = tt_rels.AcademicTermHasAcademicDay + elif day_row['DayType'] == 'Holiday': + if isinstance(term_node, timetable.AcademicTermNode): + relationship_class = tt_rels.AcademicTermHasHolidayDay + else: + relationship_class = tt_rels.AcademicTermBreakHasHolidayDay + elif day_row['DayType'] == 'OffTimetable': + relationship_class = tt_rels.AcademicTermHasOffTimetableDay + elif day_row['DayType'] == 'Staff': + relationship_class = tt_rels.AcademicTermHasStaffDay + else: + continue # Skip linking for other day types + neon.create_or_merge_neontology_relationship( + relationship_class(source=term_node, target=day_node), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {term_node.unique_id} to {day_node.unique_id}") + break + + # Create Period nodes for each academic day + if day_row['DayType'] == 'Academic': + logger.info(f"Creating periods for {day_node.unique_id}") + period_of_day = 1 + academic_or_registration_period_of_day = 1 + for _, period_row in periods_df.iterrows(): + period_node_class = { + 'Academic': timetable.AcademicPeriodNode, + 'Registration': timetable.RegistrationPeriodNode, + 'Break': timetable.BreakPeriodNode, + 'OffTimetable': timetable.OffTimetablePeriodNode + }[period_row['PeriodType']] + + logger.info(f"Creating period node for {period_node_class.__name__} Period: {period_of_day}") + period_node_unique_id = f"{period_node_class.__name__}_{school_timetable_unique_id}_Day_{academic_day_number}_Period_{period_of_day}" + logger.debug(f"Period node unique id: {period_node_unique_id}") + period_node_data = { + 'unique_id': period_node_unique_id, + 'name': period_row['PeriodName'], + 'date': day_node.date, + 'start_time': datetime.combine(day_node.date, period_row['StartTime']), + 'end_time': datetime.combine(day_node.date, period_row['EndTime']) + } + logger.debug(f"Period node data: {period_node_data}") + if period_row['PeriodType'] in ['Academic', 'Registration']: + _, timetable_period_path = fs_handler.create_school_timetable_period_directory( + timetable_path=timetable_path, + academic_day=academic_day_number, + period_dir=f"{academic_or_registration_period_of_day}_{period_row['PeriodName'].replace(' ', '_')}" + ) + week_type = day_row['WeekType'] + day_name_short = day_node.day_of_week[:3] + period_code = period_row['PeriodCode'] + period_code_formatted = f"{week_type}{day_name_short}{period_code}" + period_node_data['period_code'] = period_code_formatted + period_node_data['path'] = timetable_period_path + + academic_or_registration_period_of_day += 1 + + period_node = period_node_class(**period_node_data) + neon.create_or_merge_neontology_node(period_node, database=db_name, operation='merge') + if isinstance(period_node, timetable.AcademicPeriodNode) or isinstance(period_node, timetable.RegistrationPeriodNode): + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(period_node.path, period_node.to_dict()) + timetable_nodes['academic_period_nodes'].append(period_node) + logger.info(f'Created period node: {period_node.unique_id}') + + relationship_class = { + 'Academic': tt_rels.AcademicDayHasAcademicPeriod, + 'Registration': tt_rels.AcademicDayHasRegistrationPeriod, + 'Break': tt_rels.AcademicDayHasBreakPeriod, + 'OffTimetable': tt_rels.AcademicDayHasOffTimetablePeriod + }[period_row['PeriodType']] + + neon.create_or_merge_neontology_relationship( + relationship_class(source=day_node, target=period_node), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {day_node.unique_id} to {period_node.unique_id}") + period_of_day += 1 # We don't use this but we could + academic_day_number += 1 # This is a bit of a hack but it works to keep the directories aligned (reorganise) + day_number += 1 # We don't use this but we could + + def create_school_timetable_node_sequence_rels(timetable_nodes): + def sort_and_create_relationships(nodes, relationship_map, sort_key): + sorted_nodes = sorted(nodes, key=sort_key) + for i in range(len(sorted_nodes) - 1): + source_node = sorted_nodes[i] + target_node = sorted_nodes[i + 1] + node_type_pair = (type(source_node), type(target_node)) + relationship_class = relationship_map.get(node_type_pair) + if relationship_class: + # Avoid self-referential relationships + if source_node.unique_id != target_node.unique_id: + neon.create_or_merge_neontology_relationship( + relationship_class( + source=source_node, + target=target_node + ), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {source_node.unique_id} to {target_node.unique_id}") + else: + logger.warning(f"Skipped self-referential relationship for node {source_node.unique_id}") + + # Relationship maps for different node types + academic_year_relationship_map = { + (timetable.AcademicYearNode, timetable.AcademicYearNode): tt_rels.AcademicYearFollowsAcademicYear + } + + academic_term_relationship_map = { + (timetable.AcademicTermNode, timetable.AcademicTermBreakNode): tt_rels.AcademicTermBreakFollowsAcademicTerm, + (timetable.AcademicTermBreakNode, timetable.AcademicTermNode): tt_rels.AcademicTermFollowsAcademicTermBreak + } + + academic_week_relationship_map = { + (timetable.AcademicWeekNode, timetable.AcademicWeekNode): tt_rels.AcademicWeekFollowsAcademicWeek, + (timetable.HolidayWeekNode, timetable.HolidayWeekNode): tt_rels.HolidayWeekFollowsHolidayWeek, + (timetable.AcademicWeekNode, timetable.HolidayWeekNode): tt_rels.HolidayWeekFollowsAcademicWeek, + (timetable.HolidayWeekNode, timetable.AcademicWeekNode): tt_rels.AcademicWeekFollowsHolidayWeek + } + + academic_day_relationship_map = { + (timetable.AcademicDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsAcademicDay, + (timetable.HolidayDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsHolidayDay, + (timetable.OffTimetableDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsOffTimetableDay, + (timetable.StaffDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsStaffDay, + + (timetable.AcademicDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsAcademicDay, + (timetable.AcademicDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsAcademicDay, + (timetable.AcademicDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsAcademicDay, + + (timetable.HolidayDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsHolidayDay, + (timetable.HolidayDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsHolidayDay, + (timetable.HolidayDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsHolidayDay, + + (timetable.OffTimetableDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsOffTimetableDay, + (timetable.OffTimetableDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsOffTimetableDay, + (timetable.OffTimetableDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsOffTimetableDay, + + (timetable.StaffDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsStaffDay, + (timetable.StaffDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsStaffDay, + (timetable.StaffDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsStaffDay, + } + + academic_period_relationship_map = { + (timetable.AcademicPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsAcademicPeriod, + (timetable.AcademicPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsAcademicPeriod, + (timetable.AcademicPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsAcademicPeriod, + (timetable.AcademicPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsAcademicPeriod, + (timetable.BreakPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsBreakPeriod, + (timetable.BreakPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsBreakPeriod, + (timetable.BreakPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsBreakPeriod, + (timetable.BreakPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsBreakPeriod, + (timetable.RegistrationPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsRegistrationPeriod, + (timetable.RegistrationPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsRegistrationPeriod, + (timetable.RegistrationPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsRegistrationPeriod, + (timetable.RegistrationPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsRegistrationPeriod, + (timetable.OffTimetablePeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsOffTimetablePeriod, + (timetable.OffTimetablePeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsOffTimetablePeriod, + (timetable.OffTimetablePeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsOffTimetablePeriod, + (timetable.OffTimetablePeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsOffTimetablePeriod, + } + + + # Sort and create relationships + sort_and_create_relationships(timetable_nodes['academic_year_nodes'], academic_year_relationship_map, lambda x: int(x.year)) + sort_and_create_relationships(timetable_nodes['academic_term_nodes'], academic_term_relationship_map, lambda x: x.start_date) + sort_and_create_relationships(timetable_nodes['academic_week_nodes'], academic_week_relationship_map, lambda x: x.start_date) + sort_and_create_relationships(timetable_nodes['academic_day_nodes'], academic_day_relationship_map, lambda x: x.date) + sort_and_create_relationships(timetable_nodes['academic_period_nodes'], academic_period_relationship_map, lambda x: (x.start_time, x.end_time)) + + # Call the function with the created timetable nodes + create_school_timetable_node_sequence_rels(timetable_nodes) + + logger.info(f'Created timetable: {timetable_nodes["timetable_node"].unique_id}') + + # Log the directory structure after creation + # root_timetable_directory = fs_handler.root_path # Access the root directory of the filesystem handler + # fs_handler.log_directory_structure(root_timetable_directory) + + return { + 'school_node': school_node, + 'school_calendar_nodes': calendar_nodes, + 'school_timetable_nodes': timetable_nodes + } \ No newline at end of file diff --git a/modules/database/init/__init__.py b/modules/database/init/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/init/__pycache__/__init__.cpython-311.pyc b/modules/database/init/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..bb9941b Binary files /dev/null and b/modules/database/init/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/init_calendar.cpython-311.pyc b/modules/database/init/__pycache__/init_calendar.cpython-311.pyc new file mode 100644 index 0000000..79b71f3 Binary files /dev/null and b/modules/database/init/__pycache__/init_calendar.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/init_school.cpython-311.pyc b/modules/database/init/__pycache__/init_school.cpython-311.pyc new file mode 100644 index 0000000..0634cac Binary files /dev/null and b/modules/database/init/__pycache__/init_school.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/init_school_curriculum.cpython-311.pyc b/modules/database/init/__pycache__/init_school_curriculum.cpython-311.pyc new file mode 100644 index 0000000..8694309 Binary files /dev/null and b/modules/database/init/__pycache__/init_school_curriculum.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/init_school_timetable.cpython-311.pyc b/modules/database/init/__pycache__/init_school_timetable.cpython-311.pyc new file mode 100644 index 0000000..26e0ca0 Binary files /dev/null and b/modules/database/init/__pycache__/init_school_timetable.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/init_user.cpython-311.pyc b/modules/database/init/__pycache__/init_user.cpython-311.pyc new file mode 100644 index 0000000..6a98df0 Binary files /dev/null and b/modules/database/init/__pycache__/init_user.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/init_worker_timetable.cpython-311.pyc b/modules/database/init/__pycache__/init_worker_timetable.cpython-311.pyc new file mode 100644 index 0000000..f89e323 Binary files /dev/null and b/modules/database/init/__pycache__/init_worker_timetable.cpython-311.pyc differ diff --git a/modules/database/init/__pycache__/xl_tools.cpython-311.pyc b/modules/database/init/__pycache__/xl_tools.cpython-311.pyc new file mode 100644 index 0000000..1c8d264 Binary files /dev/null and b/modules/database/init/__pycache__/xl_tools.cpython-311.pyc differ diff --git a/modules/database/init/init_calendar.py b/modules/database/init/init_calendar.py new file mode 100644 index 0000000..9307e0e --- /dev/null +++ b/modules/database/init/init_calendar.py @@ -0,0 +1,260 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +import modules.database.schemas.nodes.calendars as calendar_schemas +import modules.database.schemas.relationships.calendars as calendar_relationships +import modules.database.schemas.relationships.calendar_sequence as calendar_sequence_relationships +import modules.database.schemas.relationships.owner_relationships as owner_relationships +import modules.database.tools.neontology_tools as neon +from datetime import timedelta, datetime + +def create_calendar(db_name, start_date, end_date, attach_to_calendar_node=False, owner_node=None, time_chunk_node_length: int = None): + logger.info(f"Creating calendar for {start_date} to {end_date}") + + logger.info(f"Initializing Neontology connection") + neon.init_neontology_connection() + + created_years = {} + created_months = {} + created_weeks = {} + created_days = {} + + last_year_node = None + last_month_node = None + last_week_node = None + last_day_node = None + + calendar_nodes = { + 'calendar_node': None, + 'calendar_year_nodes': [], + 'calendar_month_nodes': [], + 'calendar_week_nodes': [], + 'calendar_day_nodes': [] + } + + if attach_to_calendar_node and owner_node: + logger.info(f"Attaching calendar to owner's node {owner_node.unique_id} in database: {db_name}") + owner_unique_id = owner_node.unique_id + calendar_unique_id = f"{start_date.strftime('%Y-%m-%d')}_{end_date.strftime('%Y-%m-%d')}" + calendar_node = calendar_schemas.CalendarNode( + unique_id=calendar_unique_id, + name=f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}", + start_date=start_date, + end_date=end_date, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(calendar_node, database=db_name, operation='merge') + calendar_nodes['calendar_node'] = calendar_node + logger.info(f"Calendar node created: {calendar_node.unique_id}") + + import modules.database.schemas.relationships.owner_relationships as owner_relationships + + neon.create_or_merge_neontology_relationship( + owner_relationships.OwnerHasCalendar(source=owner_node, target=calendar_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {owner_node.unique_id} to {calendar_node.unique_id}") + elif attach_to_calendar_node and not owner_node: + logger.info(f"Creating calendar for {start_date} to {end_date} in database: {db_name}") + calendar_node = calendar_schemas.CalendarNode( + unique_id=f"{start_date.strftime('%Y-%m-%d')}_{end_date.strftime('%Y-%m-%d')}", + name=f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}", + start_date=start_date, + end_date=end_date, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(calendar_node, database=db_name, operation='merge') + calendar_nodes['calendar_node'] = calendar_node + logger.info(f"Calendar node created: {calendar_node.unique_id}") + else: + logger.error("Invalid combination of parameters for calendar creation.") + raise ValueError("Invalid combination of parameters for calendar creation.") + + current_date = start_date + while current_date <= end_date: + year = current_date.year + month = current_date.month + day = current_date.day + iso_year, iso_week, iso_weekday = current_date.isocalendar() + + calendar_year_unique_id = f"{year}" + + if year not in created_years: + year_node = calendar_schemas.CalendarYearNode( + unique_id=calendar_year_unique_id, + year=str(year), + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(year_node, database=db_name, operation='merge') + calendar_nodes['calendar_year_nodes'].append(year_node) + created_years[year] = year_node + logger.info(f"Year node created: {year_node.unique_id}") + + if attach_to_calendar_node: + neon.create_or_merge_neontology_relationship( + calendar_relationships.CalendarIncludesYear(source=calendar_node, target=year_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {calendar_node.unique_id} to {year_node.unique_id}") + if last_year_node: + neon.create_or_merge_neontology_relationship( + calendar_sequence_relationships.YearFollowsYear(source=last_year_node, target=year_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_year_node.unique_id} to {year_node.unique_id}") + last_year_node = year_node + + calendar_month_unique_id = f"{year}_{month}" + + month_key = f"{year}-{month}" + if month_key not in created_months: + month_node = calendar_schemas.CalendarMonthNode( + unique_id=calendar_month_unique_id, + year=str(year), + month=str(month), + month_name=datetime(year, month, 1).strftime('%B'), + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(month_node, database=db_name, operation='merge') + calendar_nodes['calendar_month_nodes'].append(month_node) + created_months[month_key] = month_node + logger.info(f"Month node created: {month_node.unique_id}") + + # Check for the end of year transition for months + if last_month_node: + if int(month) == 1 and int(last_month_node.month) == 12 and int(last_month_node.year) == year - 1: + neon.create_or_merge_neontology_relationship( + calendar_sequence_relationships.MonthFollowsMonth(source=last_month_node, target=month_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}") + elif int(month) == int(last_month_node.month) + 1: + neon.create_or_merge_neontology_relationship( + calendar_sequence_relationships.MonthFollowsMonth(source=last_month_node, target=month_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_month_node.unique_id} to {month_node.unique_id}") + last_month_node = month_node + + neon.create_or_merge_neontology_relationship( + calendar_relationships.YearIncludesMonth(source=year_node, target=month_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {year_node.unique_id} to {month_node.unique_id}") + + calendar_week_unique_id = f"{iso_year}_{iso_week}" + + week_key = f"{iso_year}-W{iso_week}" + if week_key not in created_weeks: + # Get the date of the first monday of the week + week_start_date = current_date - timedelta(days=current_date.weekday()) + week_node = calendar_schemas.CalendarWeekNode( + unique_id=calendar_week_unique_id, + start_date=week_start_date, + week_number=str(iso_week), + iso_week=f"{iso_year}-W{iso_week:02}", + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge') + calendar_nodes['calendar_week_nodes'].append(week_node) + created_weeks[week_key] = week_node + logger.info(f"Week node created: {week_node.unique_id}") + + if last_week_node and ((last_week_node.iso_week.split('-')[0] == str(iso_year) and int(last_week_node.week_number) == int(iso_week) - 1) or + (last_week_node.iso_week.split('-')[0] != str(iso_year) and int(last_week_node.week_number) == 52 and int(iso_week) == 1)): + neon.create_or_merge_neontology_relationship( + calendar_sequence_relationships.WeekFollowsWeek(source=last_week_node, target=week_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_week_node.unique_id} to {week_node.unique_id}") + last_week_node = week_node + + neon.create_or_merge_neontology_relationship( + calendar_relationships.YearIncludesWeek(source=year_node, target=week_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {year_node.unique_id} to {week_node.unique_id}") + + # Day node management + calendar_day_unique_id = f"{year}_{month}_{day}" + + day_key = f"{year}-{month}-{day}" + day_node = calendar_schemas.CalendarDayNode( + unique_id=calendar_day_unique_id, + date=current_date, + day_of_week=current_date.strftime('%A'), + iso_day=f"{year}-{month:02}-{day:02}", + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge') + calendar_nodes['calendar_day_nodes'].append(day_node) + created_days[day_key] = day_node + logger.info(f"Day node created: {day_node.unique_id}") + + if last_day_node: + neon.create_or_merge_neontology_relationship( + calendar_sequence_relationships.DayFollowsDay(source=last_day_node, target=day_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {last_day_node.unique_id} to {day_node.unique_id}") + last_day_node = day_node + + neon.create_or_merge_neontology_relationship( + calendar_relationships.MonthIncludesDay(source=month_node, target=day_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {month_node.unique_id} to {day_node.unique_id}") + neon.create_or_merge_neontology_relationship( + calendar_relationships.WeekIncludesDay(source=week_node, target=day_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {week_node.unique_id} to {day_node.unique_id}") + current_date += timedelta(days=1) + + if time_chunk_node_length: + time_chunk_interval = time_chunk_node_length + # Get every calendar day node and create time chunks of length time_chunk_node minutes for the whole day + for day_node in calendar_nodes['calendar_day_nodes']: + total_time_chunks_in_day = (24 * 60) / time_chunk_interval + for i in range(total_time_chunks_in_day): + time_chunk_unique_id = f"{day_node.unique_id}_{i}" + time_chunk_start_time = day_node.date.time() + timedelta(minutes=i * time_chunk_interval) + time_chunk_end_time = time_chunk_start_time + timedelta(minutes=time_chunk_interval) + time_chunk_node = calendar_schemas.CalendarTimeChunkNode( + unique_id=time_chunk_unique_id, + start_time=time_chunk_start_time, + end_time=time_chunk_end_time, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(time_chunk_node, database=db_name, operation='merge') + calendar_nodes['calendar_time_chunk_nodes'].append(time_chunk_node) + logger.info(f"Time chunk node created: {time_chunk_node.unique_id}") + # Create a relationship between the time chunk node and the day node + neon.create_or_merge_neontology_relationship( + calendar_relationships.DayIncludesTimeChunk(source=day_node, target=time_chunk_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {day_node.unique_id} to {time_chunk_node.unique_id}") + # Create sequential relationship between the time chunk nodes + if i > 0: + neon.create_or_merge_neontology_relationship( + calendar_sequence_relationships.TimeChunkFollowsTimeChunk(source=calendar_nodes['calendar_time_chunk_nodes'][i-1], target=time_chunk_node), + database=db_name, + operation='merge' + ) + logger.info(f"Relationship created from {calendar_nodes['calendar_time_chunk_nodes'][i-1].unique_id} to {time_chunk_node.unique_id}") + + logger.info(f'Created calendar: {calendar_nodes["calendar_node"].unique_id}') + return calendar_nodes \ No newline at end of file diff --git a/modules/database/init/init_school.py b/modules/database/init/init_school.py new file mode 100644 index 0000000..ca6b2e6 --- /dev/null +++ b/modules/database/init/init_school.py @@ -0,0 +1,158 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +from modules.database.schemas.nodes.schools.schools import SchoolNode +from modules.database.supabase.utils.client import SupabaseServiceRoleClient, CreateBucketOptions +import modules.database.init.init_school_timetable as init_school_timetable +import modules.database.tools.neontology_tools as neon + +def create_school_buckets(school_id: str, school_type: str, school_name: str, admin_access_token: str) -> dict: + """Create storage buckets for a school + Args: + school_id: The unique identifier for the school + school_type: The type of school (e.g., 'development') + school_name: The display name of the school + admin_access_token: The admin access token for Supabase operations + Returns: + Dictionary containing results of bucket creation operations + """ + logger.info(f"Creating storage buckets for school {school_name} ({school_type}/{school_id})") + + storage_client = SupabaseServiceRoleClient.for_admin(admin_access_token) + base_path = f"cc.institutes.{school_type}.{school_id}" + + buckets = [ + # Main school buckets + { + "id": f"{base_path}.public", + "options": CreateBucketOptions( + name=f"{school_type.title()} School Files - {school_name} - Public Files", + public=True, + file_size_limit=50 * 1024 * 1024, + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + }, + { + "id": f"{base_path}.private", + "options": CreateBucketOptions( + name=f"{school_type.title()} School Files - {school_name} - Private Files", + public=False, + file_size_limit=50 * 1024 * 1024, + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + }, + # Curriculum buckets + { + "id": f"{base_path}.curriculum.public", + "options": CreateBucketOptions( + name=f"{school_type.title()} School Files - {school_name} - Curriculum Public Files", + public=True, + file_size_limit=50 * 1024 * 1024, + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + }, + { + "id": f"{base_path}.curriculum.private", + "options": CreateBucketOptions( + name=f"{school_type.title()} School Files - {school_name} - Curriculum Private Files", + public=False, + file_size_limit=50 * 1024 * 1024, + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + } + ] + + results = {} + for bucket in buckets: + try: + result = storage_client.create_bucket(bucket["id"], bucket["options"]) + results[bucket["id"]] = { + "status": "success", + "result": result + } + logger.info(f"Successfully created bucket {bucket['id']}") + except Exception as e: + logger.error(f"Error creating school bucket {bucket['id']}: {str(e)}") + results[bucket["id"]] = { + "status": "error", + "error": str(e) + } + + return results + +def create_school(db_name: str, id: str, name: str, website: str, school_type: str, is_public: bool = True, school_node: SchoolNode | None = None, dataframes=None): + if not name or not id or not website or not school_type: + logger.error("School name, id, website and school_type are required to create a school.") + raise ValueError("School name, id, website and school_type are required to create a school.") + + logger.info(f"Initialising neo4j connection...") + neon.init_neontology_connection() + + # Create School Node if not provided + if not school_node: + if is_public: + school_node = SchoolNode( + unique_id=f'School_{id}', + tldraw_snapshot="", + id=id, + name=name, + website=website, + school_type=school_type + ) + else: + # Create private school node with default values + school_node = SchoolNode( + unique_id=f'School_{id}', + tldraw_snapshot="", + id=id, + name=name, + website=website, + school_type=school_type, + establishment_number="0000", + establishment_name=name, + establishment_type="Default", + establishment_status="Open", + phase_of_education="All", + statutory_low_age=11, + statutory_high_age=18, + school_capacity=1000 + ) + + # First create/merge the school node in the main cc.institutes database + logger.info(f"Creating school node in main cc.institutes database...") + neon.create_or_merge_neontology_node(school_node, database="cc.institutes", operation='merge') + + # Then create/merge the school node in the specific school database + logger.info(f"Creating school node in specific database {db_name}...") + neon.create_or_merge_neontology_node(school_node, database=db_name, operation='merge') + + school_nodes = { + 'school_node': school_node, + 'db_name': db_name + } + + if dataframes is not None: + logger.info(f"Creating school timetable for {name} with {len(dataframes)} dataframes...") + school_timetable_nodes = init_school_timetable.create_school_timetable(dataframes, db_name, school_node) + school_nodes['school_timetable_nodes'] = school_timetable_nodes + else: + logger.warning(f"No dataframes provided for {name}, skipping school timetable...") + + logger.info(f"School {name} created successfully...") + return school_nodes diff --git a/modules/database/init/init_school_curriculum.py b/modules/database/init/init_school_curriculum.py new file mode 100644 index 0000000..77ce8a3 --- /dev/null +++ b/modules/database/init/init_school_curriculum.py @@ -0,0 +1,697 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +import pandas as pd + +import modules.database.tools.neontology_tools as neon +import modules.database.schemas.nodes.schools.schools as school_nodes +import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes +import modules.database.schemas.nodes.schools.pastoral as pastoral_nodes +import modules.database.schemas.nodes.structures.schools as school_structures +import modules.database.schemas.relationships.curriculum_relationships as curriculum_relationships +import modules.database.schemas.relationships.entity_relationships as ent_rels +import modules.database.schemas.relationships.entity_curriculum_rels as ent_cur_rels + +# Default values for nodes +default_topic_values = { + 'topic_assessment_type': 'Null', + 'topic_type': 'Null', + 'total_number_of_lessons_for_topic': '1', + 'topic_title': 'Null' +} + +default_topic_lesson_values = { + 'topic_lesson_title': 'Null', + 'topic_lesson_type': 'Null', + 'topic_lesson_length': '1', + 'topic_lesson_suggested_activities': 'Null', + 'topic_lesson_skills_learned': 'Null', + 'topic_lesson_weblinks': 'Null', +} + +default_learning_statement_values = { + 'lesson_learning_statement': 'Null', + 'lesson_learning_statement_type': 'Student learning outcome' +} + +# Helper function to sort year groups numerically where possible +def sort_year_groups(df): + df = df.copy() + df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce') + return df.sort_values(by='YearGroupNumeric') + +def create_curriculum(dataframes, db_name: str, curriculum_db_name: str, school_node: school_nodes.SchoolNode): + + logger.info(f"Initialising neo4j connection...") + neon.init_neontology_connection() + + keystagesyllabus_df = dataframes['keystagesyllabuses'] + yeargroupsyllabus_df = dataframes['yeargroupsyllabuses'] + topic_df = dataframes['topics'] + lesson_df = dataframes['lessons'] + statement_df = dataframes['statements'] + # resource_df = dataframes['resources'] # TODO + + node_library = {} + node_library['key_stage_nodes'] = {} + node_library['year_group_nodes'] = {} + node_library['key_stage_syllabus_nodes'] = {} + node_library['year_group_syllabus_nodes'] = {} + node_library['topic_nodes'] = {} + node_library['topic_lesson_nodes'] = {} + node_library['statement_nodes'] = {} + node_library['department_nodes'] = {} + node_library['subject_nodes'] = {} + curriculum_node = None + pastoral_node = None + key_stage_nodes_created = {} + year_group_nodes_created = {} + last_year_group_node = None + last_key_stage_node = None + + # Create Department Structure node + department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}" + department_structure_node = school_structures.DepartmentStructureNode( + unique_id=department_structure_node_unique_id, + tldraw_snapshot="" + ) + # Create in school database only + neon.create_or_merge_neontology_node(department_structure_node, database=db_name, operation='merge') + node_library['department_structure_node'] = department_structure_node + + # Link Department Structure to School + neon.create_or_merge_neontology_relationship( + ent_rels.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node), + database=db_name, operation='merge' + ) + logger.info(f"Created department structure node and linked to school") + + curriculum_structure_node_unique_id = f"CurriculumStructure_{school_node.unique_id}" + curriculum_node = school_structures.CurriculumStructureNode( + unique_id=curriculum_structure_node_unique_id, + tldraw_snapshot="" + ) + # Create in school database only + neon.create_or_merge_neontology_node(curriculum_node, database=db_name, operation='merge') + node_library['curriculum_node'] = curriculum_node + + # Create relationship in school database only + neon.create_or_merge_neontology_relationship( + ent_cur_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node), + database=db_name, operation='merge' + ) + logger.info(f"Created curriculum node and relationship with school") + + pastoral_structure_node_unique_id = f"PastoralStructure_{school_node.unique_id}" + pastoral_node = school_structures.PastoralStructureNode( + unique_id=pastoral_structure_node_unique_id, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(pastoral_node, database=db_name, operation='merge') + node_library['pastoral_node'] = pastoral_node + neon.create_or_merge_neontology_relationship( + ent_cur_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node), + database=db_name, operation='merge' + ) + logger.info(f"Created pastoral node and relationship with school") + + # Create departments and subjects + # First get unique departments + unique_departments = keystagesyllabus_df['Department'].dropna().unique() + + for department_name in unique_departments: + department_unique_id = f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}" + department_node = school_nodes.DepartmentNode( + unique_id=department_unique_id, + name=department_name, + tldraw_snapshot="" + ) + # Create department in school database only + neon.create_or_merge_neontology_node(department_node, database=db_name, operation='merge') + node_library['department_nodes'][department_name] = department_node + + # Link department to department structure in school database + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node), + database=db_name, operation='merge' + ) + logger.info(f"Created department node for {department_name} and linked to department structure") + + # Create subjects and link to departments + # First get unique subjects from key stage syllabuses (which have department info) + unique_subjects = keystagesyllabus_df[['Subject', 'SubjectCode', 'Department']].drop_duplicates() + + # Then add any additional subjects from year group syllabuses (without department info) + additional_subjects = yeargroupsyllabus_df[['Subject', 'SubjectCode']].drop_duplicates() + additional_subjects = additional_subjects[~additional_subjects['SubjectCode'].isin(unique_subjects['SubjectCode'])] + + # Process subjects from key stage syllabuses first (these have department info) + for _, subject_row in unique_subjects.iterrows(): + subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}" + department_node = node_library['department_nodes'].get(subject_row['Department']) + if not department_node: + logger.warning(f"No department found for subject {subject_row['Subject']} with code {subject_row['SubjectCode']}") + continue + + subject_node = curriculum_nodes.SubjectNode( + unique_id=subject_unique_id, + id=subject_row['SubjectCode'], + name=subject_row['Subject'], + tldraw_snapshot="" + ) + # Create subject in both databases + neon.create_or_merge_neontology_node(subject_node, database=db_name, operation='merge') + neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge') + node_library['subject_nodes'][subject_row['Subject']] = subject_node + + # Link subject to department in school database only + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentManagesSubject(source=department_node, target=subject_node), + database=db_name, operation='merge' + ) + logger.info(f"Created subject node for {subject_row['Subject']} and linked to department {subject_row['Department']}") + + # Process any additional subjects from year group syllabuses (these won't have department info) + for _, subject_row in additional_subjects.iterrows(): + subject_unique_id = f"Subject_{school_node.unique_id}_{subject_row['SubjectCode']}" + # Create in a special "Unassigned" department + unassigned_dept_name = "Unassigned Department" + if unassigned_dept_name not in node_library['department_nodes']: + department_node = school_nodes.DepartmentNode( + unique_id=f"Department_{school_node.unique_id}_Unassigned", + name=unassigned_dept_name, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(department_node, database=db_name, operation='merge') + node_library['department_nodes'][unassigned_dept_name] = department_node + + # Link unassigned department to department structure + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentStructureHasDepartment(source=department_structure_node, target=department_node), + database=db_name, operation='merge' + ) + logger.info(f"Created unassigned department node and linked to department structure") + + subject_node = curriculum_nodes.SubjectNode( + unique_id=subject_unique_id, + id=subject_row['SubjectCode'], + name=subject_row['Subject'], + tldraw_snapshot="" + ) + # Create subject in both databases + neon.create_or_merge_neontology_node(subject_node, database=db_name, operation='merge') + neon.create_or_merge_neontology_node(subject_node, database=curriculum_db_name, operation='merge') + node_library['subject_nodes'][subject_row['Subject']] = subject_node + + # Link subject to unassigned department in school database only + neon.create_or_merge_neontology_relationship( + ent_rels.DepartmentManagesSubject( + source=node_library['department_nodes'][unassigned_dept_name], + target=subject_node + ), + database=db_name, operation='merge' + ) + logger.warning(f"Created subject node for {subject_row['Subject']} in unassigned department") + + # Process key stages and syllabuses + logger.info(f"Processing key stages") + last_key_stage_node = None + # Track last syllabus nodes per subject + last_key_stage_syllabus_nodes = {} # Dictionary to track last key stage syllabus node per subject + last_year_group_syllabus_nodes = {} # Dictionary to track last year group syllabus node per subject + topics_processed = set() # Track which topics have been processed + lessons_processed = set() # Track which lessons have been processed + statements_processed = set() # Track which statements have been processed + + # First create all key stage nodes and key stage syllabus nodes + for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows(): + key_stage = str(ks_row['KeyStage']) + logger.debug(f"Processing key stage syllabus row - Subject: {ks_row['Subject']}, Key Stage: {key_stage}") + + subject_node = node_library['subject_nodes'].get(ks_row['Subject']) + if not subject_node: + logger.warning(f"No subject node found for subject {ks_row['Subject']}") + continue + + if key_stage not in key_stage_nodes_created: + key_stage_node_unique_id = f"KeyStage_{curriculum_node.unique_id}_KStg{key_stage}" + key_stage_node = curriculum_nodes.KeyStageNode( + unique_id=key_stage_node_unique_id, + name=f"Key Stage {key_stage}", + key_stage=str(key_stage), + tldraw_snapshot="" + ) + # Create key stage node in both databases + neon.create_or_merge_neontology_node(key_stage_node, database=db_name, operation='merge') + neon.create_or_merge_neontology_node(key_stage_node, database=curriculum_db_name, operation='merge') + key_stage_nodes_created[key_stage] = key_stage_node + node_library['key_stage_nodes'][key_stage] = key_stage_node + + # Create relationship with curriculum structure in school database only + neon.create_or_merge_neontology_relationship( + curriculum_relationships.CurriculumStructureIncludesKeyStage(source=curriculum_node, target=key_stage_node), + database=db_name, operation='merge' + ) + logger.info(f"Created key stage node {key_stage_node_unique_id} and relationship with curriculum structure") + + # Create sequential relationship between key stages in both databases + if last_key_stage_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageFollowsKeyStage(source=last_key_stage_node, target=key_stage_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between key stages {last_key_stage_node.unique_id} and {key_stage_node.unique_id}") + last_key_stage_node = key_stage_node + + # Create key stage syllabus under the subject's curriculum directory + key_stage_syllabus_node_unique_id = f"KeyStageSyllabus_{curriculum_node.unique_id}_{ks_row['Title'].replace(' ', '')}" + logger.debug(f"Creating key stage syllabus node for {ks_row['Subject']} KS{key_stage} with ID {ks_row['ID']}") + + key_stage_syllabus_node_unique_id = f"KeyStageSyllabus_{curriculum_node.unique_id}_{ks_row['Title'].replace(' ', '')}" + key_stage_syllabus_node = curriculum_nodes.KeyStageSyllabusNode( + unique_id=key_stage_syllabus_node_unique_id, + id=ks_row['ID'], + name=ks_row['Title'], + key_stage=str(ks_row['KeyStage']), + subject_name=ks_row['Subject'], + tldraw_snapshot="" + ) + # Create key stage syllabus node in both databases + neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=db_name, operation='merge') + neon.create_or_merge_neontology_node(key_stage_syllabus_node, database=curriculum_db_name, operation='merge') + node_library['key_stage_syllabus_nodes'][ks_row['ID']] = key_stage_syllabus_node + logger.debug(f"Created key stage syllabus node {key_stage_syllabus_node_unique_id} for {ks_row['Subject']} KS{key_stage}") + + # Link key stage syllabus to its subject in both databases + if subject_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasKeyStageSyllabus(source=subject_node, target=key_stage_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between subject {subject_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}") + + # Link key stage syllabus to its key stage in both databases + key_stage_node = key_stage_nodes_created.get(key_stage) + if key_stage_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageIncludesKeyStageSyllabus(source=key_stage_node, target=key_stage_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between key stage {key_stage_node.unique_id} and key stage syllabus {key_stage_syllabus_node.unique_id}") + + # Create sequential relationship between key stage syllabuses in both databases + last_key_stage_syllabus_node = last_key_stage_syllabus_nodes.get(ks_row['Subject']) + if last_key_stage_syllabus_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusFollowsKeyStageSyllabus(source=last_key_stage_syllabus_node, target=key_stage_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between key stage syllabuses {last_key_stage_syllabus_node.unique_id} and {key_stage_syllabus_node.unique_id}") + last_key_stage_syllabus_nodes[ks_row['Subject']] = key_stage_syllabus_node + + # Now process year groups and their syllabuses + for index, ks_row in keystagesyllabus_df.sort_values('KeyStage').iterrows(): + key_stage = str(ks_row['KeyStage']) + related_yeargroups = sort_year_groups(yeargroupsyllabus_df[yeargroupsyllabus_df['KeyStage'] == ks_row['KeyStage']]) + + logger.info(f"Processing year groups for key stage {key_stage}") + for yg_index, yg_row in related_yeargroups.iterrows(): + year_group = yg_row['YearGroup'] + subject_code = yg_row['SubjectCode'] + numeric_year_group = pd.to_numeric(year_group, errors='coerce') + + if pd.notna(numeric_year_group): + numeric_year_group = int(numeric_year_group) + if numeric_year_group not in year_group_nodes_created: + year_group_node_unique_id = f"YearGroup_{school_node.unique_id}_YGrp{numeric_year_group}" + year_group_node = pastoral_nodes.YearGroupNode( + unique_id=year_group_node_unique_id, + year_group=str(numeric_year_group), + name=f"Year {numeric_year_group}, {year_group}", + tldraw_snapshot="" + ) + # Create year group node in both databases but use same directory + neon.create_or_merge_neontology_node(year_group_node, database=db_name, operation='merge') + neon.create_or_merge_neontology_node(year_group_node, database=curriculum_db_name, operation='merge') + + # Create sequential relationship between year groups in both databases + if last_year_group_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupFollowsYearGroup(source=last_year_group_node, target=year_group_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between year groups {last_year_group_node.unique_id} and {year_group_node.unique_id} across key stages") + last_year_group_node = year_group_node + + # Create relationship with Pastoral Structure in school database only + neon.create_or_merge_neontology_relationship( + curriculum_relationships.PastoralStructureIncludesYearGroup(source=pastoral_node, target=year_group_node), + database=db_name, operation='merge' + ) + logger.info(f"Created year group node {year_group_node_unique_id} and relationship with pastoral structure") + + year_group_nodes_created[numeric_year_group] = year_group_node + node_library['year_group_nodes'][str(numeric_year_group)] = year_group_node + + # Create year group syllabus nodes in both databases + year_group_node = year_group_nodes_created.get(numeric_year_group) + if year_group_node: + year_group_syllabus_node_unique_id = f"YearGroupSyllabus_{school_node.unique_id}_{yg_row['ID']}" + year_group_syllabus_node = pastoral_nodes.YearGroupSyllabusNode( + unique_id=year_group_syllabus_node_unique_id, + id=yg_row['ID'], + name=yg_row['Title'], + year_group=str(yg_row['YearGroup']), + subject_name=yg_row['Subject'], + tldraw_snapshot="" + ) + + # Create year group syllabus node in both databases but use same directory + neon.create_or_merge_neontology_node(year_group_syllabus_node, database=db_name, operation='merge') + neon.create_or_merge_neontology_node(year_group_syllabus_node, database=curriculum_db_name, operation='merge') + node_library['year_group_syllabus_nodes'][yg_row['ID']] = year_group_syllabus_node + + # Create sequential relationship between year group syllabuses in both databases + last_year_group_syllabus_node = last_year_group_syllabus_nodes.get(yg_row['Subject']) + # Only create sequential relationship if this year group is higher than the last one + if last_year_group_syllabus_node: + last_year = pd.to_numeric(last_year_group_syllabus_node.year_group, errors='coerce') + current_year = pd.to_numeric(year_group_syllabus_node.year_group, errors='coerce') + if pd.notna(last_year) and pd.notna(current_year) and current_year > last_year: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupSyllabusFollowsYearGroupSyllabus(source=last_year_group_syllabus_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between year group syllabuses {last_year_group_syllabus_node.unique_id} and {year_group_syllabus_node.unique_id}") + last_year_group_syllabus_nodes[yg_row['Subject']] = year_group_syllabus_node + + # Create relationships in both databases using MATCH to avoid cartesian products + subject_node = node_library['subject_nodes'].get(yg_row['Subject']) + if subject_node: + # Link to subject + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.SubjectHasYearGroupSyllabus(source=subject_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between subject {subject_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Link to year group + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupHasYearGroupSyllabus(source=year_group_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between year group {year_group_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Link to key stage syllabus if it exists for the same subject + key_stage_syllabus_node = node_library['key_stage_syllabus_nodes'].get(ks_row['ID']) + if key_stage_syllabus_node and yg_row['Subject'] == ks_row['Subject']: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node), + database=db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesYearGroupSyllabus(source=key_stage_syllabus_node, target=year_group_syllabus_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between key stage syllabus {key_stage_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Process topics for this year group syllabus only if not already processed + topics_for_syllabus = topic_df[topic_df['SyllabusYearID'] == yg_row['ID']] + for _, topic_row in topics_for_syllabus.iterrows(): + if topic_row['TopicID'] in topics_processed: + continue + topics_processed.add(topic_row['TopicID']) + + # Get the correct subject from the topic row + topic_subject = topic_row['SyllabusSubject'] + topic_key_stage = topic_row['SyllabusKeyStage'] + + logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage}") + logger.debug(f"Available key stage syllabus nodes: {[node.subject_name + '_KS' + node.key_stage for node in node_library['key_stage_syllabus_nodes'].values()]}") + + # Find the key stage syllabus node by iterating through all nodes + matching_syllabus_node = None + for syllabus_node in node_library['key_stage_syllabus_nodes'].values(): + logger.debug(f"Checking syllabus node - Subject: {syllabus_node.subject_name}, Key Stage: {syllabus_node.key_stage}") + logger.debug(f"Comparing with - Subject: {topic_subject}, Key Stage: {str(topic_key_stage)}") + logger.debug(f"Types - Node Subject: {type(syllabus_node.subject_name)}, Topic Subject: {type(topic_subject)}") + logger.debug(f"Types - Node Key Stage: {type(syllabus_node.key_stage)}, Topic Key Stage: {type(str(topic_key_stage))}") + + if (syllabus_node.subject_name == topic_subject and + syllabus_node.key_stage == str(topic_key_stage)): + matching_syllabus_node = syllabus_node + logger.debug(f"Found matching syllabus node: {syllabus_node.unique_id}") + break + + if not matching_syllabus_node: + logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation") + continue + + topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}" + topic_node = curriculum_nodes.TopicNode( + unique_id=topic_node_unique_id, + id=topic_row['TopicID'], + name=topic_row.get('TopicTitle', default_topic_values['topic_title']), + total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])), + type=topic_row.get('TopicType', default_topic_values['topic_type']), + assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']), + tldraw_snapshot="" + ) + # Create topic node in curriculum database only + neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge') + node_library['topic_nodes'][topic_row['TopicID']] = topic_node + + # Link topic to key stage syllabus as well as year group syllabus + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node), + database=curriculum_db_name, operation='merge' + ) + neon.create_or_merge_neontology_relationship( + curriculum_relationships.YearGroupSyllabusIncludesTopic(source=year_group_syllabus_node, target=topic_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationships between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id} and year group syllabus {year_group_syllabus_node_unique_id}") + + # Process lessons for this topic only if not already processed + lessons_for_topic = lesson_df[ + (lesson_df['TopicID'] == topic_row['TopicID']) & + (lesson_df['SyllabusSubject'] == topic_subject) + ].copy() + lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str) + lessons_for_topic = lessons_for_topic.sort_values('Lesson') + + previous_lesson_node = None + for _, lesson_row in lessons_for_topic.iterrows(): + if lesson_row['LessonID'] in lessons_processed: + continue + lessons_processed.add(lesson_row['LessonID']) + + lesson_node = curriculum_nodes.TopicLessonNode( + unique_id=f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}", + id=lesson_row['LessonID'], + name=lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']), + type=lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']), + length=str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])), + suggested_activities=str(lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities'])), + skills_learned=str(lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned'])), + weblinks=str(lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks'])), + tldraw_snapshot="" + ) + # Create lesson node in curriculum database only + neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge') + node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node + + # Link lesson to topic + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}") + + # Create sequential relationships between lessons + if lesson_row['Lesson'].isdigit() and previous_lesson_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}") + previous_lesson_node = lesson_node + + # Process learning statements for this lesson only if not already processed + statements_for_lesson = statement_df[ + (statement_df['LessonID'] == lesson_row['LessonID']) & + (statement_df['SyllabusSubject'] == topic_subject) + ] + for _, statement_row in statements_for_lesson.iterrows(): + if statement_row['StatementID'] in statements_processed: + continue + statements_processed.add(statement_row['StatementID']) + statement_node = curriculum_nodes.LearningStatementNode( + unique_id=f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}", + id=statement_row['StatementID'], + name=statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']), + type=statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']), + tldraw_snapshot="" + ) + # Create statement node in curriculum database only + neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge') + node_library['statement_nodes'][statement_row['StatementID']] = statement_node + + # Link learning statement to lesson + neon.create_or_merge_neontology_relationship( + curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}") + else: + logger.warning(f"No year group node found for year group {year_group}, skipping syllabus creation") + + # After processing all year groups and their syllabuses, process any remaining topics + logger.info("Processing topics without year groups") + for _, topic_row in topic_df.iterrows(): + if topic_row['TopicID'] in topics_processed: + continue + + topic_subject = topic_row['SyllabusSubject'] + topic_key_stage = topic_row['SyllabusKeyStage'] + + logger.debug(f"Processing topic {topic_row['TopicID']} for subject {topic_subject} and key stage {topic_key_stage} without year group") + + # Find the key stage syllabus node + matching_syllabus_node = None + for syllabus_node in node_library['key_stage_syllabus_nodes'].values(): + if (syllabus_node.subject_name == topic_subject and + syllabus_node.key_stage == str(topic_key_stage)): + matching_syllabus_node = syllabus_node + break + + if not matching_syllabus_node: + logger.warning(f"No key stage syllabus node found for subject {topic_subject} and key stage {topic_key_stage}, skipping topic creation") + continue + + topic_node_unique_id = f"Topic_{matching_syllabus_node.unique_id}_{topic_row['TopicID']}" + topic_node = curriculum_nodes.TopicNode( + unique_id=topic_node_unique_id, + id=topic_row['TopicID'], + name=topic_row.get('TopicTitle', default_topic_values['topic_title']), + total_number_of_lessons_for_topic=str(topic_row.get('TotalNumberOfLessonsForTopic', default_topic_values['total_number_of_lessons_for_topic'])), + type=topic_row.get('TopicType', default_topic_values['topic_type']), + assessment_type=topic_row.get('TopicAssessmentType', default_topic_values['topic_assessment_type']), + tldraw_snapshot="" + ) + # Create topic node in curriculum database only + neon.create_or_merge_neontology_node(topic_node, database=curriculum_db_name, operation='merge') + node_library['topic_nodes'][topic_row['TopicID']] = topic_node + topics_processed.add(topic_row['TopicID']) + + # Link topic to key stage syllabus + neon.create_or_merge_neontology_relationship( + curriculum_relationships.KeyStageSyllabusIncludesTopic(source=matching_syllabus_node, target=topic_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created relationship between topic {topic_node_unique_id} and key stage syllabus {matching_syllabus_node.unique_id}") + + # Process lessons for this topic + lessons_for_topic = lesson_df[ + (lesson_df['TopicID'] == topic_row['TopicID']) & + (lesson_df['SyllabusSubject'] == topic_subject) + ].copy() + lessons_for_topic.loc[:, 'Lesson'] = lessons_for_topic['Lesson'].astype(str) + lessons_for_topic = lessons_for_topic.sort_values('Lesson') + + previous_lesson_node = None + for _, lesson_row in lessons_for_topic.iterrows(): + if lesson_row['LessonID'] in lessons_processed: + continue + lessons_processed.add(lesson_row['LessonID']) + lesson_node = curriculum_nodes.TopicLessonNode( + unique_id=f"TopicLesson_{topic_node_unique_id}_{lesson_row['LessonID']}", + id=lesson_row['LessonID'], + name=lesson_row.get('LessonTitle', default_topic_lesson_values['topic_lesson_title']), + type=lesson_row.get('LessonType', default_topic_lesson_values['topic_lesson_type']), + length=str(lesson_row.get('SuggestedNumberOfPeriodsForLesson', default_topic_lesson_values['topic_lesson_length'])), + suggested_activities=str(lesson_row.get('SuggestedActivities', default_topic_lesson_values['topic_lesson_suggested_activities'])), + skills_learned=str(lesson_row.get('SkillsLearned', default_topic_lesson_values['topic_lesson_skills_learned'])), + weblinks=str(lesson_row.get('WebLinks', default_topic_lesson_values['topic_lesson_weblinks'])), + tldraw_snapshot="" + ) + # Create lesson node in curriculum database only + neon.create_or_merge_neontology_node(lesson_node, database=curriculum_db_name, operation='merge') + node_library['topic_lesson_nodes'][lesson_row['LessonID']] = lesson_node + + # Link lesson to topic + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicIncludesTopicLesson(source=topic_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created lesson node {lesson_node.unique_id} and relationship with topic {topic_node.unique_id}") + + # Create sequential relationships between lessons + if lesson_row['Lesson'].isdigit() and previous_lesson_node: + neon.create_or_merge_neontology_relationship( + curriculum_relationships.TopicLessonFollowsTopicLesson(source=previous_lesson_node, target=lesson_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created sequential relationship between lessons {previous_lesson_node.unique_id} and {lesson_node.unique_id}") + previous_lesson_node = lesson_node + + # Process learning statements for this lesson + statements_for_lesson = statement_df[ + (statement_df['LessonID'] == lesson_row['LessonID']) & + (statement_df['SyllabusSubject'] == topic_subject) + ] + for _, statement_row in statements_for_lesson.iterrows(): + if statement_row['StatementID'] in statements_processed: + continue + statements_processed.add(statement_row['StatementID']) + statement_node = curriculum_nodes.LearningStatementNode( + unique_id=f"LearningStatement_{lesson_node.unique_id}_{statement_row['StatementID']}", + id=statement_row['StatementID'], + name=statement_row.get('LearningStatement', default_learning_statement_values['lesson_learning_statement']), + type=statement_row.get('StatementType', default_learning_statement_values['lesson_learning_statement_type']), + tldraw_snapshot="" + ) + # Create statement node in curriculum database only + neon.create_or_merge_neontology_node(statement_node, database=curriculum_db_name, operation='merge') + node_library['statement_nodes'][statement_row['StatementID']] = statement_node + + # Link learning statement to lesson + neon.create_or_merge_neontology_relationship( + curriculum_relationships.LessonIncludesLearningStatement(source=lesson_node, target=statement_node), + database=curriculum_db_name, operation='merge' + ) + logger.info(f"Created learning statement node {statement_node.unique_id} and relationship with lesson {lesson_node.unique_id}") + + return node_library \ No newline at end of file diff --git a/modules/database/init/init_school_timetable.py b/modules/database/init/init_school_timetable.py new file mode 100644 index 0000000..4d91121 --- /dev/null +++ b/modules/database/init/init_school_timetable.py @@ -0,0 +1,487 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +from datetime import timedelta, datetime +import pandas as pd +from modules.database.schemas.structures import structures +import modules.database.schemas.nodes.schools.timetable as timetable +import modules.database.schemas.relationships.timetables as tt_rels +import modules.database.schemas.relationships.entity_timetable_rels as entity_tt_rels +import modules.database.schemas.relationships.calendar_timetable_rels as cal_tt_rels +import modules.database.init.init_calendar as init_calendar +import modules.database.tools.neontology_tools as neon + +def create_school_timetable(dataframes, db_name, school_node=None): + logger.info(f"Creating school timetable for {db_name}") + if dataframes is None: + raise ValueError("Data is required to create the calendar and timetable.") + + logger.info(f"Initialising neo4j connection...") + neon.init_neontology_connection() + + school_df = dataframes['school'] + if school_node is None: + logger.info(f"School node is None, using school data from dataframe") + school_unique_id = school_df[school_df['Identifier'] == 'SchoolID']['Data'].iloc[0] + else: + logger.info(f"School node is not None, using school data from school node: {school_node}") + school_unique_id = school_node.unique_id + + terms_df = dataframes['terms'] + weeks_df = dataframes['weeks'] + days_df = dataframes['days'] + periods_df = dataframes['periods'] + + school_df_year_start = school_df[school_df['Identifier'] == 'AcademicYearStart']['Data'].iloc[0] + school_df_year_end = school_df[school_df['Identifier'] == 'AcademicYearEnd']['Data'].iloc[0] + if isinstance(school_df_year_start, str): + school_year_start_date = datetime.strptime(school_df_year_start, '%Y-%m-%d') + else: + school_year_start_date = school_df_year_start + if isinstance(school_df_year_end, str): + school_year_end_date = datetime.strptime(school_df_year_end, '%Y-%m-%d') + else: + school_year_end_date = school_df_year_end + + # Create a dictionary to store the timetable nodes + timetable_nodes = { + 'timetable_node': None, + 'academic_year_nodes': [], + 'academic_term_nodes': [], + 'academic_week_nodes': [], + 'academic_day_nodes': [], + 'academic_period_nodes': [] + } + + # Create AcademicTimetable Node + school_timetable_unique_id = f"{school_unique_id}_{school_year_start_date.year}_{school_year_end_date.year}" + school_timetable_node = timetable.SchoolTimetableNode( + school_timetable_id=school_timetable_unique_id, + unique_id=school_timetable_unique_id, + start_date=school_year_start_date, + end_date=school_year_end_date, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(school_timetable_node, database=db_name, operation='merge') + timetable_nodes['timetable_node'] = school_timetable_node + + if school_node: + logger.info(f"Creating calendar for {school_unique_id} from Neo4j SchoolNode: {school_node.unique_id}") + calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=True, owner_node=school_node) + # Link the school node to the timetable node + neon.create_or_merge_neontology_relationship( + entity_tt_rels.SchoolHasTimetable(source=school_node, target=school_timetable_node), + database=db_name, operation='merge' + ) + timetable_nodes['calendar_nodes'] = calendar_nodes + else: + logger.info(f"Creating calendar for {school_unique_id} from dataframe SchoolID: {school_unique_id}") + calendar_nodes = init_calendar.create_calendar(db_name, school_year_start_date, school_year_end_date, attach_to_calendar_node=False, owner_node=None) + + # Create AcademicYear nodes for each year within the range + for year in range(school_year_start_date.year, school_year_end_date.year + 1): + year_str = str(year) + academic_year_unique_id = f"{school_timetable_unique_id}_{year}" + academic_year_node = timetable.AcademicYearNode( + unique_id=academic_year_unique_id, + year=year_str, + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(academic_year_node, database=db_name, operation='merge') + timetable_nodes['academic_year_nodes'].append(academic_year_node) + logger.info(f'Created academic year node: {academic_year_node.unique_id}') + neon.create_or_merge_neontology_relationship( + tt_rels.AcademicTimetableHasAcademicYear(source=school_timetable_node, target=academic_year_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {school_timetable_node.unique_id} to {academic_year_node.unique_id}") + + # Link the academic year with the corresponding calendar year node + for year_node in calendar_nodes['calendar_year_nodes']: + if year_node.year == year: + neon.create_or_merge_neontology_relationship( + cal_tt_rels.AcademicYearIsCalendarYear(source=academic_year_node, target=year_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {year_node.unique_id}") + break + + # Create Term and TermBreak nodes linked to AcademicYear + term_number = 1 + academic_term_number = 1 + for _, term_row in terms_df.iterrows(): + term_node_class = timetable.AcademicTermNode if term_row['TermType'] == 'Term' else timetable.AcademicTermBreakNode + term_name = term_row['TermName'] + term_name_no_spaces = term_name.replace(' ', '') + term_start_date = term_row['StartDate'] + if isinstance(term_start_date, pd.Timestamp): + term_start_date = term_start_date.strftime('%Y-%m-%d') + + term_end_date = term_row['EndDate'] + if isinstance(term_end_date, pd.Timestamp): + term_end_date = term_end_date.strftime('%Y-%m-%d') + + if term_row['TermType'] == 'Term': + term_node_unique_id = f"{school_timetable_unique_id}_{academic_term_number}_{term_name_no_spaces}" + academic_term_number_str = str(academic_term_number) + term_node = term_node_class( + unique_id=term_node_unique_id, + term_name=term_name, + term_number=academic_term_number_str, + start_date=datetime.strptime(term_start_date, '%Y-%m-%d'), + end_date=datetime.strptime(term_end_date, '%Y-%m-%d'), + tldraw_snapshot="" + ) + academic_term_number += 1 + else: + term_break_node_unique_id = f"{school_timetable_unique_id}_{term_name_no_spaces}" + term_node = term_node_class( + unique_id=term_break_node_unique_id, + term_break_name=term_name, + start_date=datetime.strptime(term_start_date, '%Y-%m-%d'), + end_date=datetime.strptime(term_end_date, '%Y-%m-%d'), + tldraw_snapshot="" + ) + neon.create_or_merge_neontology_node(term_node, database=db_name, operation='merge') + logger.info(f'Created academic term break node: {term_node.unique_id}') + timetable_nodes['academic_term_nodes'].append(term_node) + term_number += 1 # We don't use this but we could + + # Link term node to the correct academic year + term_years = set() + term_years.update([term_node.start_date.year, term_node.end_date.year]) + + for academic_year_node in timetable_nodes['academic_year_nodes']: + if int(academic_year_node.year) in term_years: + relationship_class = tt_rels.AcademicYearHasAcademicTerm if term_row['TermType'] == 'Term' else tt_rels.AcademicYearHasAcademicTermBreak + neon.create_or_merge_neontology_relationship( + relationship_class(source=academic_year_node, target=term_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {term_node.unique_id}") + + # Create Week nodes + academic_week_number = 1 + for _, week_row in weeks_df.iterrows(): + week_node_class = timetable.HolidayWeekNode if week_row['WeekType'] == 'Holiday' else timetable.AcademicWeekNode + week_start_date = week_row['WeekStart'] + if isinstance(week_start_date, pd.Timestamp): + week_start_date = week_start_date.strftime('%Y-%m-%d') + + week_node_unique_id = f"{school_timetable_unique_id}_{week_row['WeekNumber']}_{week_row['WeekType']}Week" + + if week_row['WeekType'] == 'Holiday': + week_node = week_node_class( + unique_id=week_node_unique_id, + start_date=datetime.strptime(week_start_date, '%Y-%m-%d'), + tldraw_snapshot="" + ) + else: + academic_week_number_str = str(academic_week_number) + week_type = week_row['WeekType'] + week_node = week_node_class( + unique_id=week_node_unique_id, + academic_week_number=academic_week_number_str, + start_date=datetime.strptime(week_start_date, '%Y-%m-%d'), + week_type=week_type, + tldraw_snapshot="" + ) + academic_week_number += 1 + neon.create_or_merge_neontology_node(week_node, database=db_name, operation='merge') + timetable_nodes['academic_week_nodes'].append(week_node) + logger.info(f"Created week node: {week_node.unique_id}") + for calendar_node in calendar_nodes['calendar_week_nodes']: + if calendar_node.start_date == week_node.start_date: + if isinstance(week_node, timetable.AcademicWeekNode): + neon.create_or_merge_neontology_relationship( + cal_tt_rels.AcademicWeekIsCalendarWeek(source=week_node, target=calendar_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {calendar_node.unique_id} to {week_node.unique_id}") + elif isinstance(week_node, timetable.HolidayWeekNode): + neon.create_or_merge_neontology_relationship( + cal_tt_rels.HolidayWeekIsCalendarWeek(source=week_node, target=calendar_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {calendar_node.unique_id} to {week_node.unique_id}") + break + + # Link week node to the correct academic term + for term_node in timetable_nodes['academic_term_nodes']: + if term_node.start_date <= week_node.start_date <= term_node.end_date: + relationship_class = tt_rels.AcademicTermHasAcademicWeek if week_row['WeekType'] != 'Holiday' else tt_rels.AcademicTermBreakHasHolidayWeek + neon.create_or_merge_neontology_relationship( + relationship_class(source=term_node, target=week_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {term_node.unique_id} to {week_node.unique_id}") + break + + # Link week node to the correct academic year + for academic_year_node in timetable_nodes['academic_year_nodes']: + if int(academic_year_node.year) == week_node.start_date.year: + relationship_class = tt_rels.AcademicYearHasAcademicWeek if week_row['WeekType'] != 'Holiday' else tt_rels.AcademicYearHasHolidayWeek + neon.create_or_merge_neontology_relationship( + relationship_class(source=academic_year_node, target=week_node), + database=db_name, operation='merge' + ) + logger.info(f"Created school timetable relationship from {academic_year_node.unique_id} to {week_node.unique_id}") + break + + # Create Day nodes + day_number = 1 + academic_day_number = 1 + for _, day_row in days_df.iterrows(): + date_str = day_row['Date'] + if isinstance(date_str, pd.Timestamp): + date_str = date_str.strftime('%Y-%m-%d') + + day_node_class = { + 'Academic': timetable.AcademicDayNode, + 'Holiday': timetable.HolidayDayNode, + 'OffTimetable': timetable.OffTimetableDayNode, + 'StaffDay': timetable.StaffDayNode + }[day_row['DayType']] + + # Format the unique ID as {day_node_class.__name__}Day + day_node_data = { + 'unique_id': f"{school_timetable_unique_id}_{day_number}_{day_node_class.__name__}Day", + 'date': datetime.strptime(date_str, '%Y-%m-%d'), + 'day_of_week': datetime.strptime(date_str, '%Y-%m-%d').strftime('%A'), + 'tldraw_snapshot': "" + } + + if day_row['DayType'] == 'Academic': + day_node_data['academic_day'] = str(academic_day_number) + day_node_data['day_type'] = day_row['WeekType'] + day_node_data['tldraw_snapshot'] = "" + + day_node = day_node_class(**day_node_data) + + for calendar_node in calendar_nodes['calendar_day_nodes']: + if calendar_node.date == day_node.date: + neon.create_or_merge_neontology_node(day_node, database=db_name, operation='merge') + timetable_nodes['academic_day_nodes'].append(day_node) + logger.info(f"Created day node: {day_node.unique_id}") + + if isinstance(day_node, timetable.AcademicDayNode): + relationship_class = cal_tt_rels.AcademicDayIsCalendarDay + elif isinstance(day_node, timetable.HolidayDayNode): + relationship_class = cal_tt_rels.HolidayDayIsCalendarDay + elif isinstance(day_node, timetable.OffTimetableDayNode): + relationship_class = cal_tt_rels.OffTimetableDayIsCalendarDay + elif isinstance(day_node, timetable.StaffDayNode): + relationship_class = cal_tt_rels.StaffDayIsCalendarDay + + neon.create_or_merge_neontology_relationship( + relationship_class(source=day_node, target=calendar_node), + database=db_name, operation='merge' + ) + logger.info(f'Created relationship from {calendar_node.unique_id} to {day_node.unique_id}') + break + + # Link day node to the correct academic week + for academic_week_node in timetable_nodes['academic_week_nodes']: + if academic_week_node.start_date <= day_node.date <= (academic_week_node.start_date + timedelta(days=6)): + if day_row['DayType'] == 'Academic': + relationship_class = tt_rels.AcademicWeekHasAcademicDay + elif day_row['DayType'] == 'Holiday': + if hasattr(academic_week_node, 'week_type') and academic_week_node.week_type in ['A', 'B']: + relationship_class = tt_rels.AcademicWeekHasHolidayDay + else: + relationship_class = tt_rels.HolidayWeekHasHolidayDay + elif day_row['DayType'] == 'OffTimetable': + relationship_class = tt_rels.AcademicWeekHasOffTimetableDay + elif day_row['DayType'] == 'Staff': + relationship_class = tt_rels.AcademicWeekHasStaffDay + else: + continue # Skip linking for other day types + neon.create_or_merge_neontology_relationship( + relationship_class(source=academic_week_node, target=day_node), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {academic_week_node.unique_id} to {day_node.unique_id}") + break + + # Link day node to the correct academic term + for term_node in timetable_nodes['academic_term_nodes']: + if term_node.start_date <= day_node.date <= term_node.end_date: + if day_row['DayType'] == 'Academic': + relationship_class = tt_rels.AcademicTermHasAcademicDay + elif day_row['DayType'] == 'Holiday': + if isinstance(term_node, timetable.AcademicTermNode): + relationship_class = tt_rels.AcademicTermHasHolidayDay + else: + relationship_class = tt_rels.AcademicTermBreakHasHolidayDay + elif day_row['DayType'] == 'OffTimetable': + relationship_class = tt_rels.AcademicTermHasOffTimetableDay + elif day_row['DayType'] == 'Staff': + relationship_class = tt_rels.AcademicTermHasStaffDay + else: + continue # Skip linking for other day types + neon.create_or_merge_neontology_relationship( + relationship_class(source=term_node, target=day_node), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {term_node.unique_id} to {day_node.unique_id}") + break + + # Create Period nodes for each academic day + if day_row['DayType'] == 'Academic': + logger.info(f"Creating periods for {day_node.unique_id}") + period_of_day = 1 + academic_or_registration_period_of_day = 1 + for _, period_row in periods_df.iterrows(): + period_node_class = { + 'Academic': timetable.AcademicPeriodNode, + 'Registration': timetable.RegistrationPeriodNode, + 'Break': timetable.BreakPeriodNode, + 'OffTimetable': timetable.OffTimetablePeriodNode + }[period_row['PeriodType']] + + logger.info(f"Creating period node for {period_node_class.__name__} Period: {period_of_day}") + period_node_unique_id = f"{school_timetable_unique_id}_{academic_day_number}_{period_of_day}_{period_node_class.__name__}Period" + logger.debug(f"Period node unique id: {period_node_unique_id}") + period_node_data = { + 'unique_id': period_node_unique_id, + 'name': period_row['PeriodName'], + 'date': day_node.date, + 'start_time': datetime.combine(day_node.date, period_row['StartTime']), + 'end_time': datetime.combine(day_node.date, period_row['EndTime']), + 'tldraw_snapshot': "" + } + logger.debug(f"Period node data: {period_node_data}") + if period_row['PeriodType'] in ['Academic', 'Registration']: + week_type = day_row['WeekType'] + day_name_short = day_node.day_of_week[:3] + period_code = period_row['PeriodCode'] + period_code_formatted = f"{week_type}{day_name_short}{period_code}" + period_node_data['period_code'] = period_code_formatted + period_node_data['tldraw_snapshot'] = "" + + academic_or_registration_period_of_day += 1 + + period_node = period_node_class(**period_node_data) + neon.create_or_merge_neontology_node(period_node, database=db_name, operation='merge') + timetable_nodes['academic_period_nodes'].append(period_node) + logger.info(f'Created period node: {period_node.unique_id}') + + relationship_class = { + 'Academic': tt_rels.AcademicDayHasAcademicPeriod, + 'Registration': tt_rels.AcademicDayHasRegistrationPeriod, + 'Break': tt_rels.AcademicDayHasBreakPeriod, + 'OffTimetable': tt_rels.AcademicDayHasOffTimetablePeriod + }[period_row['PeriodType']] + + neon.create_or_merge_neontology_relationship( + relationship_class(source=day_node, target=period_node), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {day_node.unique_id} to {period_node.unique_id}") + period_of_day += 1 # We don't use this but we could + academic_day_number += 1 # This is a bit of a hack but it works to keep the directories aligned (reorganise) + day_number += 1 # We don't use this but we could + + def create_school_timetable_node_sequence_rels(timetable_nodes): + def sort_and_create_relationships(nodes, relationship_map, sort_key): + sorted_nodes = sorted(nodes, key=sort_key) + for i in range(len(sorted_nodes) - 1): + source_node = sorted_nodes[i] + target_node = sorted_nodes[i + 1] + node_type_pair = (type(source_node), type(target_node)) + relationship_class = relationship_map.get(node_type_pair) + if relationship_class: + # Avoid self-referential relationships + if source_node.unique_id != target_node.unique_id: + neon.create_or_merge_neontology_relationship( + relationship_class( + source=source_node, + target=target_node + ), + database=db_name, operation='merge' + ) + logger.info(f"Created relationship from {source_node.unique_id} to {target_node.unique_id}") + else: + logger.warning(f"Skipped self-referential relationship for node {source_node.unique_id}") + + # Relationship maps for different node types + academic_year_relationship_map = { + (timetable.AcademicYearNode, timetable.AcademicYearNode): tt_rels.AcademicYearFollowsAcademicYear + } + + academic_term_relationship_map = { + (timetable.AcademicTermNode, timetable.AcademicTermBreakNode): tt_rels.AcademicTermBreakFollowsAcademicTerm, + (timetable.AcademicTermBreakNode, timetable.AcademicTermNode): tt_rels.AcademicTermFollowsAcademicTermBreak + } + + academic_week_relationship_map = { + (timetable.AcademicWeekNode, timetable.AcademicWeekNode): tt_rels.AcademicWeekFollowsAcademicWeek, + (timetable.HolidayWeekNode, timetable.HolidayWeekNode): tt_rels.HolidayWeekFollowsHolidayWeek, + (timetable.AcademicWeekNode, timetable.HolidayWeekNode): tt_rels.HolidayWeekFollowsAcademicWeek, + (timetable.HolidayWeekNode, timetable.AcademicWeekNode): tt_rels.AcademicWeekFollowsHolidayWeek + } + + academic_day_relationship_map = { + (timetable.AcademicDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsAcademicDay, + (timetable.HolidayDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsHolidayDay, + (timetable.OffTimetableDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsOffTimetableDay, + (timetable.StaffDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsStaffDay, + + (timetable.AcademicDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsAcademicDay, + (timetable.AcademicDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsAcademicDay, + (timetable.AcademicDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsAcademicDay, + + (timetable.HolidayDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsHolidayDay, + (timetable.HolidayDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsHolidayDay, + (timetable.HolidayDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsHolidayDay, + + (timetable.OffTimetableDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsOffTimetableDay, + (timetable.OffTimetableDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsOffTimetableDay, + (timetable.OffTimetableDayNode, timetable.StaffDayNode): tt_rels.StaffDayFollowsOffTimetableDay, + + (timetable.StaffDayNode, timetable.AcademicDayNode): tt_rels.AcademicDayFollowsStaffDay, + (timetable.StaffDayNode, timetable.HolidayDayNode): tt_rels.HolidayDayFollowsStaffDay, + (timetable.StaffDayNode, timetable.OffTimetableDayNode): tt_rels.OffTimetableDayFollowsStaffDay, + } + + academic_period_relationship_map = { + (timetable.AcademicPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsAcademicPeriod, + (timetable.AcademicPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsAcademicPeriod, + (timetable.AcademicPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsAcademicPeriod, + (timetable.AcademicPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsAcademicPeriod, + (timetable.BreakPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsBreakPeriod, + (timetable.BreakPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsBreakPeriod, + (timetable.BreakPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsBreakPeriod, + (timetable.BreakPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsBreakPeriod, + (timetable.RegistrationPeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsRegistrationPeriod, + (timetable.RegistrationPeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsRegistrationPeriod, + (timetable.RegistrationPeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsRegistrationPeriod, + (timetable.RegistrationPeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsRegistrationPeriod, + (timetable.OffTimetablePeriodNode, timetable.OffTimetablePeriodNode): tt_rels.OffTimetablePeriodFollowsOffTimetablePeriod, + (timetable.OffTimetablePeriodNode, timetable.AcademicPeriodNode): tt_rels.AcademicPeriodFollowsOffTimetablePeriod, + (timetable.OffTimetablePeriodNode, timetable.BreakPeriodNode): tt_rels.BreakPeriodFollowsOffTimetablePeriod, + (timetable.OffTimetablePeriodNode, timetable.RegistrationPeriodNode): tt_rels.RegistrationPeriodFollowsOffTimetablePeriod, + } + + + # Sort and create relationships + sort_and_create_relationships(timetable_nodes['academic_year_nodes'], academic_year_relationship_map, lambda x: int(x.year)) + sort_and_create_relationships(timetable_nodes['academic_term_nodes'], academic_term_relationship_map, lambda x: x.start_date) + sort_and_create_relationships(timetable_nodes['academic_week_nodes'], academic_week_relationship_map, lambda x: x.start_date) + sort_and_create_relationships(timetable_nodes['academic_day_nodes'], academic_day_relationship_map, lambda x: x.date) + sort_and_create_relationships(timetable_nodes['academic_period_nodes'], academic_period_relationship_map, lambda x: (x.start_time, x.end_time)) + + # Call the function with the created timetable nodes + create_school_timetable_node_sequence_rels(timetable_nodes) + + logger.info(f'Created timetable: {timetable_nodes["timetable_node"].unique_id}') + + # Log the directory structure after creation + # root_timetable_directory = fs_handler.root_path # Access the root directory of the filesystem handler + # fs_handler.log_directory_structure(root_timetable_directory) + + return { + 'school_node': school_node, + 'school_calendar_nodes': calendar_nodes, + 'school_timetable_nodes': timetable_nodes + } \ No newline at end of file diff --git a/modules/database/init/init_user.py b/modules/database/init/init_user.py new file mode 100644 index 0000000..c541010 --- /dev/null +++ b/modules/database/init/init_user.py @@ -0,0 +1,379 @@ +import os +from datetime import timedelta, datetime +from abc import ABC, abstractmethod +from typing import Dict, Optional, Any, Union + +from modules.database.services.neo4j_service import Neo4jService +import modules.database.schemas.nodes.users as user_nodes +import modules.database.schemas.nodes.workers.workers as worker_nodes +import modules.database.init.init_calendar as init_calendar +import modules.database.schemas.relationships.entity_relationships as entity_relationships +import modules.database.tools.neontology_tools as neon +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +def create_and_check_db(db_name): + neo4j_service = Neo4jService() + neo4j_service.create_database(db_name) + database_status = neo4j_service.check_database_exists(db_name) + if not database_status['exists']: + raise ValueError(f"Database {db_name} not found") + return database_status + +class UserCreator(ABC): + def __init__(self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date): + cc_schools_db_name = "cc.institutes" # Fix the TODO + self.cc_users_db_name = cc_users_db_name + self.user_db_name = f"{cc_users_db_name}.{user_type}.{cc_username}" + self.worker_db_name = f"{cc_schools_db_name}.{user_type}.{cc_username}" + self.user_type = user_type + self.worker_type = worker_type + self.cc_username = cc_username + self.user_email = user_email + self.worker_email = worker_email + self.user_name = user_name + self.worker_name = worker_name + self.user_id = user_id + self.user_nodes: Dict[str, Optional[Any]] = { + 'default_user_node': None, + 'private_user_node': None, + 'worker_node': None, + 'calendar_node': None + } + if calendar_start_date and calendar_end_date: + self.calendar_start_date = calendar_start_date + self.calendar_end_date = calendar_end_date + else: + logger.warning("No calendar start and end date provided, using default values") + self.calendar_start_date = datetime.now().date() + self.calendar_end_date = (datetime.now() + timedelta(days=5)).date() + + @abstractmethod + def create_user(self): + pass + + def create_user_node(self, db_name: str): + logger.info(f"Module is creating {self.cc_users_db_name} user node for {self.user_type} user {self.cc_username}") + try: + user_node = self._create_user_node(db_name) + logger.debug(f"User node creation completed for {self.cc_users_db_name} user node for {self.user_type} user {self.cc_username}: {user_node.to_dict()}") + return user_node + except Exception as e: + logger.error(f"Error creating user node: {e}") + raise + + def _create_user_node(self, db_name: str): + # Ensure Neontology is initialized + neon.init_neontology_connection() + + user_node = user_nodes.UserNode( + unique_id=f"{self.user_id}", + tldraw_snapshot="", + cc_username=f"{self.cc_username}", + user_email=f"{self.user_email}", + user_name=f"{self.user_name}", + user_db_name=f"{self.user_db_name}", + user_type=f"{self.user_type}", + ) + logger.debug(f"User node template created: {user_node.to_dict()}. Writing to database {db_name}") + neon.create_or_merge_neontology_node(node=user_node, database=db_name, operation='merge') + logger.info(f"User node created: {user_node.to_dict()}") + return user_node + + def create_storage_bucket(self, bucket_id: str, bucket_name: str, access_token: Optional[str] = None) -> bool: + """Create public and private storage buckets for the user using their access token or service role during initialization""" + logger.info(f"Creating storage buckets for user {self.cc_username}") + + try: + from modules.database.supabase.utils.client import SupabaseServiceRoleClient, SupabaseAnonClient, CreateBucketOptions + + # During initialization (no access token provided), use service role + if not access_token: + logger.info("Using service role client for bucket creation during initialization") + supabase = SupabaseServiceRoleClient() + else: + # For regular operations, use the user's access token + logger.info("Using user token for bucket creation") + supabase = SupabaseAnonClient.for_user(access_token) + + # Create both public and private buckets + buckets = [ + { + "id": f"{bucket_id}.public", + "options": CreateBucketOptions( + name=f"{bucket_name} - Public Files", + public=True, + file_size_limit=50 * 1024 * 1024, # 50MB + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + }, + { + "id": f"{bucket_id}.private", + "options": CreateBucketOptions( + name=f"{bucket_name} - Private Files", + public=False, + file_size_limit=50 * 1024 * 1024, # 50MB + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + } + ] + + success = True + for bucket in buckets: + try: + result = supabase.create_bucket(bucket["id"], bucket["options"]) + if not result: + logger.error(f"Failed to create bucket {bucket['id']}") + success = False + else: + logger.info(f"Successfully created bucket {bucket['id']}") + except Exception as e: + logger.error(f"Error creating bucket {bucket['id']}: {str(e)}") + success = False + + return success + + except Exception as e: + logger.error(f"Error creating storage buckets: {str(e)}") + return False + +class SchoolUserCreator(UserCreator): + def __init__(self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, school_node, worker_node=None): + super().__init__(user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date) + self.school_node = school_node + self.worker_node = worker_node + + def create_user(self): + # Ensure Neontology is initialized + logger.debug(f"Initializing Neontology connection. Closing any existing connection") + neon.close_neontology_connection() + logger.debug(f"Neontology connection closed. Initializing new connection") + neon.init_neontology_connection() + + if self.user_type in ['email_teacher', 'ms_teacher']: + worker_node = self.create_teacher_node() + elif self.user_type in ['email_student', 'ms_student']: + worker_node = self.create_student_node() + else: + raise ValueError(f"User type {self.user_type} not supported") + + self.user_nodes[f'worker_node'] = worker_node + + user_node = self.create_user_node(self.cc_users_db_name) + + logger.info(f"User node created: {user_node}") + + self.user_nodes['default_user_node'] = user_node + + self.create_user_worker_relationship(user_node, worker_node) + + self.create_worker_school_relationship(worker_node, self.school_node) + + logger.info(f"Worker school relationship created between {worker_node} and {self.school_node}") + return self.user_nodes + + def create_teacher_node(self): + logger.debug(f"Teacher node will be created for school: {self.school_node}") + try: + return self._create_teacher_node() + except KeyError as ke: + raise ValueError(f"Missing required key in worker_data: {ke}") from ke + except Exception as e: + raise ValueError(f"Error creating teacher node: {e}") from e + + def _create_teacher_node(self): + teacher_node = worker_nodes.TeacherNode( + unique_id=f"{self.user_id}", + tldraw_snapshot="", + worker_name=self.worker_name, + worker_email=self.worker_email, + worker_db_name=self.worker_db_name, + worker_type=self.worker_type + ) + # Use the school's private database name if available + school_db = self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{self.school_node.id}" + logger.info(f"Teacher node template created: {teacher_node}... setting school db to {school_db}") + + neon.create_or_merge_neontology_node(node=teacher_node, database=school_db, operation='merge') + + logger.info(f"Teacher node merged into database {school_db}: {teacher_node}") + return teacher_node + + def create_student_node(self): + student_node = worker_nodes.StudentNode( + unique_id=f"Student_{self.user_id}", + worker_name=self.worker_name, + worker_email=self.worker_email, + worker_db_name=self.worker_db_name, + worker_type=self.worker_type, + tldraw_snapshot="" + ) + # Use the school's private database name if available + school_db = self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{self.school_node.id}" + logger.info(f"Student node template created: {student_node}... setting school db to {school_db}") + + neon.create_or_merge_neontology_node(node=student_node, database=school_db, operation='merge') + + logger.info(f"Student node merged into database {school_db}: {student_node}") + return student_node + + def create_user_worker_relationship(self, user_node, worker_node): + user_role_rel = entity_relationships.UserIsSchoolWorker(source=user_node, target=worker_node) + # Use the school's private database name if available + school_db = self.school_node.private_database_name if hasattr(self.school_node, 'private_database_name') else f"cc.institutes.{self.school_node.school_type}.{self.school_node.id}" + neon.create_or_merge_neontology_relationship(user_role_rel, database=school_db, operation='merge') + logger.info(f"Relationship created between user and worker in database {school_db}") + + def create_worker_school_relationship(self, worker_node, school_node): + worker_school_rel = entity_relationships.EntityBelongsToSchool(source=worker_node, target=school_node) + # Use the school's private database name if available + school_db = school_node.private_database_name if hasattr(school_node, 'private_database_name') else f"cc.institutes.{school_node.school_type}.{school_node.id}" + neon.create_or_merge_neontology_relationship(worker_school_rel, database=school_db, operation='merge') + logger.info(f"Relationship created between worker and school in database {school_db}") + +class NonSchoolUserCreator(UserCreator): + def __init__(self, user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date, developer_role: str = "developer"): + super().__init__(user_id, cc_users_db_name, user_type, worker_type, user_email, worker_email, cc_username, user_name, worker_name, calendar_start_date, calendar_end_date) + self.developer_role = developer_role + + def create_user(self, access_token: Optional[str] = None): + logger.debug(f"Creating user node for {self.user_type} user {self.cc_username} in database {self.cc_users_db_name}") + + # Create storage buckets for the user + user_bucket_id = self.user_db_name + user_bucket_name = f"{self.user_type.title()} User Files - {self.user_name}" + if not self.create_storage_bucket(user_bucket_id, user_bucket_name, access_token=access_token): + logger.error(f"Failed to create storage bucket for user {self.cc_username}") + raise ValueError(f"Failed to create storage bucket for user {self.cc_username}") + + # Create default user node first + default_user_node = self.create_user_node(self.cc_users_db_name) + logger.debug(f"Default user node created: {default_user_node}") + + # Verify the return value of create_user_node + if default_user_node is None: + logger.error("Failed to create default user node. It is None.") + raise ValueError("Failed to create default user node. It is None.") + + self.user_nodes[f'default_user_node'] = default_user_node + + # Create the appropriate user db based on user_type + if self.user_type == 'admin': + logger.debug(f"Creating super admin db for {self.user_type} user {self.cc_username} in database {self.user_db_name}") + self.create_super_admin_db() + elif self.user_type == 'developer': + logger.debug(f"Creating developer db for {self.user_type} user {self.cc_username} in database {self.user_db_name}") + self.create_developer_db() + else: + raise ValueError(f"User type {self.user_type} not supported") + + logger.debug(f"User nodes after creation: {self.user_nodes}") + return self.user_nodes + + def create_super_admin_db(self): + logger.debug(f"Creating super admin db for {self.user_type} user {self.cc_username} in database {self.user_db_name}") + neon.init_neontology_connection() + + # Create the user db self.user_db_name + create_and_check_db(self.user_db_name) + + try: + # Create the user node again for the user db + logger.debug(f"Creating super admin user node for {self.user_type} user {self.cc_username} in database {self.user_db_name}") + private_user_node = self.create_user_node(self.user_db_name) + + super_admin_node = worker_nodes.SuperAdminNode( + unique_id=f"SuperAdmin_{self.user_id}", + worker_email=self.worker_email, + tldraw_snapshot="", + worker_name=self.worker_name, + worker_db_name=self.worker_db_name, + worker_type=self.worker_type + ) + logger.debug(f"Super admin node template created: {super_admin_node}. Writing to database {self.user_db_name}") + neon.create_or_merge_neontology_node(node=super_admin_node, database=self.user_db_name, operation='merge') + logger.info(f"Super admin node created: {super_admin_node}") + + logger.debug(f"Creating relationship between user node: {private_user_node} and worker node: {super_admin_node}") + self.create_user_specific_relationship(user_node=private_user_node, worker_node=super_admin_node) + + logger.debug(f"Creating calendar for {self.user_type} user {self.cc_username} in database {self.user_db_name}") + calendar_nodes = self.create_calendar(user_node=private_user_node) + logger.info(f"Super admin calendar created.") + + self.user_nodes['private_user_node'] = private_user_node + self.user_nodes['worker_node'] = super_admin_node + + logger.info(f"Returning user nodes: {self.user_nodes}") + + return self.user_nodes + except Exception as e: + logger.error(f"Error creating super admin node: {e}") + raise ValueError(f"Error creating super admin node: {e}") from e + + def create_developer_db(self): + neon.init_neontology_connection() + + # Create the user db self.user_db_name + create_and_check_db(self.user_db_name) + + try: + # Create the user node again for the user db + private_user_node = self.create_user_node(self.user_db_name) + + developer_node = worker_nodes.DeveloperNode( + unique_id=f"Developer_{self.user_id}", + worker_name=self.worker_name, + worker_email=self.worker_email, + tldraw_snapshot="", + worker_db_name=self.worker_db_name, + worker_type=self.worker_type, + developer_role=self.developer_role + ) + + neon.create_or_merge_neontology_node(developer_node, database=self.user_db_name, operation='merge') + logger.info(f"Developer node created: {developer_node}") + + self.user_nodes['private_user_node'] = private_user_node + self.user_nodes['worker_node'] = developer_node + + self.create_user_specific_relationship(user_node=private_user_node, worker_node=developer_node) + + return self.user_nodes + except Exception as e: + raise ValueError(f"Error creating developer node: {e}") from e + + def create_user_specific_relationship(self, user_node: user_nodes.UserNode, worker_node: Union[worker_nodes.SuperAdminNode, worker_nodes.DeveloperNode]): + if user_node is None or worker_node is None: + logger.error("User node or worker node is None. Cannot create relationship.") + raise ValueError("User node or worker node is None. Cannot create relationship.") + + logger.info(f"Creating relationship between user node: {user_node} and worker node: {worker_node}") + + # Log the state of user_node and worker_node + logger.debug(f"user_node: {user_node}") + logger.debug(f"worker_node: {worker_node}") + + if worker_node.worker_type == 'developer': + specific_user_rel = entity_relationships.UserIsSystemWorker(source=user_node, target=worker_node) + elif worker_node.worker_type == 'superadmin': + specific_user_rel = entity_relationships.UserIsSystemWorker(source=user_node, target=worker_node) + else: + raise ValueError(f"User type {worker_node.worker_type} not supported") + + neon.create_or_merge_neontology_relationship(specific_user_rel, database=self.user_db_name, operation='merge') + logger.info("Relationship created between user and specific node") + + def create_calendar(self, user_node: user_nodes.UserNode): + calendar_nodes = init_calendar.create_calendar(self.user_db_name, self.calendar_start_date, self.calendar_end_date, attach_to_calendar_node=True, owner_node=user_node) + + logger.info(f"Calendar nodes created.") + return calendar_nodes diff --git a/modules/database/init/init_user_timetable.py b/modules/database/init/init_user_timetable.py new file mode 100644 index 0000000..8fdbb08 --- /dev/null +++ b/modules/database/init/init_user_timetable.py @@ -0,0 +1,326 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +import modules.database.tools.neo4j_driver_tools as driver +import modules.database.tools.neontology_tools as neon +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +from modules.database.schemas.nodes.users import UserNode +from modules.database.schemas.nodes.schools.schools import SubjectClassNode +from modules.database.schemas.nodes.workers.workers import TeacherNode +from modules.database.schemas.nodes.calendars import CalendarDayNode +from modules.database.schemas.nodes.workers.timetable import ( + UserTeacherTimetableNode +) +from modules.database.schemas.relationships.entity_timetable_rels import ( + EntityHasTimetable +) +from modules.database.schemas.relationships.planning_relationships import ( + TeacherHasTimetable, TimetableHasClass, ClassHasLesson,TimetableLessonFollowsTimetableLesson +) +from modules.database.schemas.relationships.calendar_timetable_rels import ( + CalendarDayHasTimetableLesson, TimetableLessonBelongsToCalendarDay, + CalendarDayHasPlannedLesson, PlannedLessonBelongsToCalendarDay +) + +def get_school_worker_classes(school_db_name: str, user_unique_id: str, worker_unique_id: str) -> list: + """ + Retrieve all classes for a worker from the school database. + """ + query = """ + MATCH (w:Teacher {unique_id: $worker_id})-[:TEACHER_HAS_TIMETABLE]->(tt:TeacherTimetable) + -[:TIMETABLE_HAS_CLASS]->(c:SubjectClass) + RETURN c + """ + with driver.get_driver(db_name=school_db_name).session(database=school_db_name) as session: + result = session.run(query, worker_id=worker_unique_id) + classes = [record['c'] for record in result] + if not classes: + logger.warning(f"No classes found for teacher {worker_unique_id} in school database") + return classes + +def get_school_class_periods(school_db_name: str, class_unique_id: str) -> list: + """ + Retrieve all periods for a class from the school database. + """ + query = """ + MATCH (c:SubjectClass {unique_id: $class_id})-[:CLASS_HAS_LESSON]->(l:TimetableLesson) + RETURN l + """ + with driver.get_driver(db_name=school_db_name).session(database=school_db_name) as session: + result = session.run(query, class_id=class_unique_id) + periods = [record['l'] for record in result] + if not periods: + logger.warning(f"No periods found for class {class_unique_id} in school database") + return periods + +def get_user_calendar_nodes(user_db_name: str, user_node: UserNode) -> list: + """ + Retrieve all calendar day nodes for a user. + """ + # First try to find any calendar days to verify the structure + verify_query = """ + MATCH (w:User {unique_id: $user_id}) + OPTIONAL MATCH (w)-[:HAS_CALENDAR]->(c:Calendar) + OPTIONAL MATCH (c)-[:CALENDAR_INCLUDES_YEAR]->(y:CalendarYear) + OPTIONAL MATCH (y)-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth) + OPTIONAL MATCH (m)-[:MONTH_INCLUDES_DAY]->(d:CalendarDay) + RETURN w.unique_id as user_id, + count(c) as calendar_count, + count(y) as year_count, + count(m) as month_count, + count(d) as day_count, + collect(DISTINCT y.year) as years + LIMIT 1 + """ + + with driver.get_driver(db_name=user_db_name).session(database=user_db_name) as session: + # First check the calendar structure + result = session.run(verify_query, user_id=user_node.unique_id) + if stats := result.single(): + logger.info(f"Calendar structure for user {stats['user_id']}: " + f"calendars={stats['calendar_count']}, " + f"years={stats['year_count']}, " + f"months={stats['month_count']}, " + f"days={stats['day_count']}, " + f"available years={stats['years']}") + + if stats['calendar_count'] == 0: + logger.error(f"No calendar found for user {user_node.unique_id}") + return [] + if stats['year_count'] == 0: + logger.error(f"No calendar years found for user {user_node.unique_id}") + return [] + if stats['month_count'] == 0: + logger.error(f"No calendar months found for user {user_node.unique_id}") + return [] + if stats['day_count'] == 0: + logger.error(f"No calendar days found for user {user_node.unique_id}") + return [] + + # Get all calendar days without year filter + query = """ + MATCH (w:User {unique_id: $user_id})-[:HAS_CALENDAR]->(c:Calendar) + -[:CALENDAR_INCLUDES_YEAR]->(y:CalendarYear) + -[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth) + -[:MONTH_INCLUDES_DAY]->(d:CalendarDay) + RETURN d.unique_id as unique_id, + d.date as date, + d.day_of_week as day_of_week, + d.iso_day as iso_day, + d.path as path + ORDER BY d.date + """ + + result = session.run(query, user_id=user_node.unique_id) + calendar_days = [] + for record in result: + calendar_day = CalendarDayNode( + unique_id=record['unique_id'], + date=record['date'], + day_of_week=record['day_of_week'], + iso_day=record['iso_day'], + path=record['path'] + ) + calendar_days.append(calendar_day) + + if not calendar_days: + logger.error(f"No calendar days found for user {user_node.unique_id}") + else: + # Log the date range we have + dates = sorted([day.date for day in calendar_days]) + logger.info(f"Found {len(calendar_days)} calendar days for user {user_node.unique_id}") + logger.info(f"Calendar days range from {dates[0]} to {dates[-1]}") + + return calendar_days + +def create_user_worker_timetable( + user_node: UserNode, + user_worker_node: TeacherNode, + school_db_name: str +): + """ + Create a worker timetable structure in the user's database that mirrors + the school timetable, with lessons linked to the user's calendar structure. + """ + user_db_name = user_worker_node.user_db_name + + # Initialize filesystem and Neo4j + fs_handler = ClassroomCopilotFilesystem(db_name=user_db_name, init_run_type="user") + + # Create teacher timetable directory under the worker's directory + _, worker_timetable_path = fs_handler.create_teacher_timetable_directory(user_worker_node.path) + + # Initialize neontology connection + neon.init_neontology_connection() + + # Get user's calendar nodes + calendar_nodes = get_user_calendar_nodes(user_db_name, user_node) + if not calendar_nodes: + logger.warning(f"No calendar nodes found for user {user_node.unique_id}") + return { + "status": "error", + "message": "No calendar nodes found for user" + } + + try: + # Create UserTeacherTimetableNode + timetable_unique_id = f"UserTeacherTimetable_{user_worker_node.teacher_code}" + worker_timetable = UserTeacherTimetableNode( + unique_id=timetable_unique_id, + school_db_name=school_db_name, + school_timetable_id=f"TeacherTimetable_{user_worker_node.teacher_code}", + path=worker_timetable_path + ) + + # Create the timetable node and its tldraw file + neon.create_or_merge_neontology_node(worker_timetable, database=user_db_name, operation='merge') + fs_handler.create_default_tldraw_file(worker_timetable.path, worker_timetable.to_dict()) + + # Link timetable to teacher using the correct relationship structure + neon.create_or_merge_neontology_relationship( + TeacherHasTimetable(source=user_worker_node, target=worker_timetable), + database=user_db_name, + operation='merge' + ) + + # Get classes from school database + school_classes = get_school_worker_classes(school_db_name, user_node.unique_id, user_worker_node.unique_id) + if not school_classes: + logger.warning(f"No classes found for teacher {user_worker_node.unique_id} in school database") + return { + "status": "warning", + "message": "No classes found in school database" + } + + # Dictionary to store lessons by class + class_lessons = {} + + for class_data in school_classes: + class_name_safe = class_data['subject_class_code'].replace(' ', '_') + _, class_path = fs_handler.create_teacher_class_directory(worker_timetable_path, class_name_safe) + + # Create SubjectClassNode + subject_class_node = SubjectClassNode( + unique_id=class_data['unique_id'], + subject_class_code=class_data['subject_class_code'], + year_group=class_data['year_group'], + subject=class_data['subject'], + subject_code=class_data['subject_code'], + path=class_path + ) + neon.create_or_merge_neontology_node(subject_class_node, database=user_db_name, operation='merge') + fs_handler.create_default_tldraw_file(subject_class_node.path, subject_class_node.to_dict()) + + # Link class to timetable + neon.create_or_merge_neontology_relationship( + TimetableHasClass(source=worker_timetable, target=subject_class_node), + database=user_db_name, + operation='merge' + ) + + # Initialize empty list for this class's lessons + class_lessons[class_data['unique_id']] = [] + + # Get periods from school database + periods = get_school_class_periods(school_db_name, class_data['unique_id']) + if not periods: + logger.warning(f"No periods found for class {class_data['unique_id']} in school database") + continue + + for period_data in periods: + # Create UserTimetableLessonNode + lesson_unique_id = f"UserTimetableLesson_{timetable_unique_id}_{class_name_safe}_{period_data['date']}_{period_data['period_code']}" + timetable_lesson_node = UserTimetableLessonNode( + unique_id=lesson_unique_id, + subject_class=class_data['subject_class_code'], + date=period_data['date'], + start_time=period_data['start_time'], + end_time=period_data['end_time'], + period_code=period_data['period_code'], + school_db_name=school_db_name, + school_period_id=period_data['unique_id'], + path="Not set" # Will be set after creating directories + ) + + if calendar_day := next( + ( + day + for day in calendar_nodes + if day.date == period_data['date'] + ), + None, + ): + # Create lesson directory using calendar info + _, lesson_path = fs_handler.create_teacher_timetable_lesson_directory( + class_path, + f"{calendar_day.date}_{period_data['period_code']}" + ) + timetable_lesson_node.path = lesson_path + + # Create and link nodes + neon.create_or_merge_neontology_node(timetable_lesson_node, database=user_db_name, operation='merge') + fs_handler.create_default_tldraw_file(timetable_lesson_node.path, timetable_lesson_node.to_dict()) + + # Link lesson to class + neon.create_or_merge_neontology_relationship( + ClassHasLesson(source=subject_class_node, target=timetable_lesson_node), + database=user_db_name, + operation='merge' + ) + + # Link lesson to calendar day (keeping only one direction) + neon.create_or_merge_neontology_relationship( + CalendarDayHasTimetableLesson( + source=calendar_day, + target=timetable_lesson_node + ), + database=user_db_name, + operation='merge' + ) + + # Store the lesson node + class_lessons[class_data['unique_id']].append({ + 'node': timetable_lesson_node, + 'date': period_data['date'], + 'start_time': period_data['start_time'] + }) + else: + logger.warning(f"No calendar day found for date {period_data['date']} - this is expected if the date is not in the current calendar year") + + # Create sequential relationships for each class + for class_id, lessons in class_lessons.items(): + # Sort lessons by date and start time + sorted_lessons = sorted(lessons, key=lambda x: (x['date'], x['start_time'])) + + # Create relationships between consecutive lessons + for i in range(len(sorted_lessons) - 1): + current_lesson = sorted_lessons[i]['node'] + next_lesson = sorted_lessons[i + 1]['node'] + + # Skip if current and next lesson are the same node + if current_lesson.unique_id != next_lesson.unique_id: + neon.create_or_merge_neontology_relationship( + TimetableLessonFollowsTimetableLesson( + source=current_lesson, + target=next_lesson + ), + database=user_db_name, + operation='merge' + ) + + logger.info(f"Created sequential relationships for class {class_id}") + + logger.info(f"Successfully created user timetable structure for {user_worker_node.teacher_code}") + return { + "status": "success", + "message": "User timetable structure created successfully", + "timetable_node": worker_timetable.to_dict() + } + + except Exception as e: + logger.error(f"Error creating user timetable structure: {str(e)}") + return { + "status": "error", + "message": f"Error creating user timetable structure: {str(e)}" + } \ No newline at end of file diff --git a/modules/database/init/init_worker_timetable.py b/modules/database/init/init_worker_timetable.py new file mode 100644 index 0000000..37facfa --- /dev/null +++ b/modules/database/init/init_worker_timetable.py @@ -0,0 +1,241 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_init_init_worker_timetable' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import pandas as pd +import re +import modules.database.tools.neo4j_driver_tools as driver +import modules.database.tools.neontology_tools as neon +import modules.database.tools.neo4j_session_tools as session +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +from modules.database.schemas.nodes.schools.schools import SubjectClassNode +from modules.database.schemas.nodes.workers.workers import TeacherNode +from modules.database.schemas.nodes.schools.timetable import AcademicPeriodNode, RegistrationPeriodNode +from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode +from modules.database.schemas.nodes.schools.pastoral import YearGroupSyllabusNode +from modules.database.schemas.relationships.planning_relationships import TimetableLessonBelongsToPeriod, TimetableLessonHasPlannedLesson, TeacherHasTimetable, TimetableHasClass, ClassHasLesson, TimetableLessonFollowsTimetableLesson, PlannedLessonFollowsPlannedLesson, SubjectClassBelongsToYearGroupSyllabus + +def init_worker_timetable(timetable_df: pd.DataFrame, school_worker_node: TeacherNode): + logging.info(f"School worker node: {school_worker_node}") + worker_node = TeacherNode(**school_worker_node) + logging.info(f"Worker node: {worker_node}") + worker_db_name = worker_node.worker_db_name + + logging.info(f"Initialising filesystem handler...") + fs_handler = ClassroomCopilotFilesystem(db_name=worker_db_name, init_run_type="user") + _, worker_timetable_path = fs_handler.create_teacher_timetable_directory(worker_node.path) + + logging.info(f"Initialising neo4j connection...") + neon.init_neontology_connection() + + try: + timetable_unique_id = f"TeacherTimetable_{worker_node.teacher_code}" + worker_timetable = TeacherTimetableNode( + unique_id=timetable_unique_id, + path=worker_timetable_path + ) + neon.create_or_merge_neontology_node(worker_timetable, database=worker_db_name, operation='merge') + fs_handler.create_default_tldraw_file(worker_timetable.path, worker_timetable.to_dict()) + neon.create_or_merge_neontology_relationship( + TeacherHasTimetable(source=worker_node, target=worker_timetable), + database=worker_db_name, operation='merge' + ) + logging.info(f"Teacher timetable node created: {worker_timetable}") + + # Group the timetable by class + class_groups = timetable_df.groupby('Class') + for class_name, class_df in class_groups: + if pd.notna(class_name): + class_name_safe = re.sub(r'[^A-Za-z0-9_ ]+', '', class_name) + _, class_path = fs_handler.create_teacher_class_directory(worker_timetable.path, class_name_safe) + + subject_class_node_unique_id = f"SubjectClass_{class_name}" + subject_class_node = SubjectClassNode( + unique_id=subject_class_node_unique_id, + subject_class_code=class_name, + year_group=str(int(class_df['YearGroup'].iloc[0])), # TODO: Hacky fix for the year group being a float + subject=str(class_df['Subject'].iloc[0]), + subject_code=str(class_df['SubjectCode'].iloc[0]), + path=class_path + ) + neon.create_or_merge_neontology_node(subject_class_node, database=worker_db_name, operation='merge') + logging.info(f"Class node created: {subject_class_node}") + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(subject_class_node.path, subject_class_node.to_dict()) + + # Link ClassNode to TeacherTimetableNode + neon.create_or_merge_neontology_relationship( + TimetableHasClass(source=worker_timetable, target=subject_class_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Relationship created from {worker_timetable.unique_id} to {subject_class_node.unique_id}") + + # Link class to corresponding YearGoupSyllabus + + year_group_syllabus_search_driver = driver.get_driver(worker_db_name) + year_group_syllabus_search_session = year_group_syllabus_search_driver.session(database=worker_db_name) + year_group_syllabus = session.find_nodes_by_label_and_properties(year_group_syllabus_search_session, "YearGroupSyllabus", {"yr_syllabus_year_group": subject_class_node.year_group, "yr_syllabus_subject_code": subject_class_node.subject_code}) + if year_group_syllabus: + year_group_syllabus_node_data = year_group_syllabus[0] + year_group_syllabus_node = YearGroupSyllabusNode(**year_group_syllabus_node_data) + neon.create_or_merge_neontology_relationship( + SubjectClassBelongsToYearGroupSyllabus(source=subject_class_node, target=year_group_syllabus_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Relationship created from {subject_class_node.unique_id} to {year_group_syllabus_node.unique_id}") + else: + logging.warning(f"No YearGroupSyllabus found for class {class_name} with year group {subject_class_node.year_group} and subject code {subject_class_node.subject_code}") + + class_lesson_nodes = [] + planned_lesson_nodes = [] + lesson_number = 0 + for _, row in class_df.iterrows(): + properties = { + "period_code": row['PeriodCode'] + } + class_lessons_search_driver = driver.get_driver(worker_db_name) + class_lessons_search_session = class_lessons_search_driver.session(database=worker_db_name) + # If the period code contains "Rg" then we want to find the corresponding registration period and use its unique id + if "Rg" in row['PeriodCode']: # TODO: This is hacky and not very flexible. We are assuming that any period code containing "Rg" is a registration period. We should probably find a more robust way to identify registration periods + logging.info(f"Registration period found for class {class_name} with period code {row['PeriodCode']}") + class_lessons = session.find_nodes_by_label_and_properties(class_lessons_search_session, "RegistrationPeriod", properties) + else: + logging.info(f"Academic period found for class {class_name} with period code {row['PeriodCode']}") + class_lessons = session.find_nodes_by_label_and_properties(class_lessons_search_session, "AcademicPeriod", properties) + if class_lessons: + lesson_of_same_period = 0 + number_of_lessons = len(class_lessons) + while lesson_of_same_period < number_of_lessons: + class_lesson = class_lessons[lesson_of_same_period] + if "Rg" in row['PeriodCode']: + period_node = RegistrationPeriodNode(**class_lesson) + else: + period_node = AcademicPeriodNode(**class_lesson) + lesson_period_code = row['PeriodCode'] + date = class_lesson['date'] + date_safe = date.strftime("%Y-%m-%d") + # Clean the class_name to make it directory-safe (catch all for invalid characters) + timetable_lesson_unique_id = f"TimetableLesson_{timetable_unique_id}_Class_{class_name}_Lesson_{lesson_number}_{date_safe}_{lesson_period_code}" + + timetable_lesson_node = TimetableLessonNode( + unique_id=timetable_lesson_unique_id, + subject_class=class_name, + date=date, + start_time=class_lesson['start_time'].time(), # TODO: This is probably how we should format the start and end time properties for all such nodes + end_time=class_lesson['end_time'].time(), + period_code=lesson_period_code, + path="Not set" + ) + neon.create_or_merge_neontology_node(timetable_lesson_node, database=worker_db_name, operation='merge') + logging.info(f"TimetableLessonNode created: {timetable_lesson_node}") + class_lesson_nodes.append(timetable_lesson_node) + + neon.create_or_merge_neontology_relationship( + TimetableLessonBelongsToPeriod(source=timetable_lesson_node, target=period_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Relationship created from {timetable_lesson_node.unique_id} to {period_node.unique_id}") + + # Link TimetableLessonNode to ClassNode + neon.create_or_merge_neontology_relationship( + ClassHasLesson(source=subject_class_node, target=timetable_lesson_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Relationship created from {subject_class_node.unique_id} to {timetable_lesson_node.unique_id}") + + # Create PlannedLessonNode + planned_lesson_unique_id = f"PlannedLesson_{timetable_unique_id}_Class_{class_name}_Lesson_{lesson_number}_{date_safe}_{lesson_period_code}" + planned_lesson_node = PlannedLessonNode( + unique_id=planned_lesson_unique_id, + date=date, + start_time=class_lesson['start_time'].time(), + end_time=class_lesson['end_time'].time(), + period_code=lesson_period_code, + subject_class=class_name, + year_group=subject_class_node.year_group, + subject=subject_class_node.subject, + teacher_code=worker_node.teacher_code, + planning_status="Unplanned", + topic_code=None, + topic_name=None, + lesson_code=None, + lesson_name=None, + learning_statement_codes=None, + learning_statements=None, + learning_resource_codes=None, + learning_resources=None, + path="Not set" + ) + # Create the PlannedLessonNode + neon.create_or_merge_neontology_node(planned_lesson_node, database=worker_db_name, operation='merge') + logging.info(f"PlannedLessonNode created: {planned_lesson_node}") + planned_lesson_nodes.append(planned_lesson_node) + + # Link PlannedLessonNode to TimetableLessonNode + neon.create_or_merge_neontology_relationship( + TimetableLessonHasPlannedLesson(source=timetable_lesson_node, target=planned_lesson_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Relationship created from {timetable_lesson_node.unique_id} to {planned_lesson_node.unique_id}") + lesson_of_same_period += 1 + lesson_number += 1 + else: + logging.warning(f"No class periods found for class {class_name} on day {row['DayOfWeek']}") + # Sort the nodes by date and start time + class_lesson_nodes.sort(key=lambda x: (x.date, x.start_time)) + planned_lesson_nodes.sort(key=lambda x: (x.date, x.start_time)) + + # Create sequential relationships and directories for TimetableLessonNodes + for i in range(1, len(class_lesson_nodes)): + previous_node = class_lesson_nodes[i - 1] + current_node = class_lesson_nodes[i] + i_safe = f"{i:02d}" + _, class_lesson_path = fs_handler.create_teacher_timetable_lesson_directory(class_path, f"{i_safe}_{current_node.date}_{current_node.period_code}") + current_node.path = class_lesson_path + neon.create_or_merge_neontology_node(current_node, database=worker_db_name, operation='merge') + logging.info(f"TimetableLessonNode directory created and node merged into database: {current_node}") + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(current_node.path, current_node.to_dict()) + if previous_node: + neon.create_or_merge_neontology_relationship( + TimetableLessonFollowsTimetableLesson(source=previous_node, target=current_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Sequential relationship created between {previous_node.unique_id} and {current_node.unique_id}") + + # Create sequential relationships for PlannedLessonNodes + for i in range(1, len(planned_lesson_nodes)): + previous_node = planned_lesson_nodes[i - 1] + current_node = planned_lesson_nodes[i] + i_safe = f"{i:02d}" + _, planned_lesson_path = fs_handler.create_teacher_planned_lesson_directory(class_path, f"{i_safe}_{current_node.date}_{current_node.period_code}") + current_node.path = planned_lesson_path + neon.create_or_merge_neontology_node(current_node, database=worker_db_name, operation='merge') + logging.info(f"PlannedLessonNode directory created and node merged into database: {current_node}") + # Create the tldraw file for the node + fs_handler.create_default_tldraw_file(current_node.path, current_node.to_dict()) + if previous_node: + neon.create_or_merge_neontology_relationship( + PlannedLessonFollowsPlannedLesson(source=previous_node, target=current_node), + database=worker_db_name, operation='merge' + ) + logging.info(f"Sequential relationship created between {previous_node.unique_id} and {current_node.unique_id}") + logging.info(f"Successfully initialized worker timetable for worker {worker_node.teacher_code}") + return {"status": "success", "message": "Worker timetable initialized successfully"} + + except Exception as e: + logging.error(f"Error initializing worker timetable: {str(e)}") + return {"status": "error", "message": f"Error initializing worker timetable: {str(e)}"} + + + diff --git a/modules/database/init/xl_tools.py b/modules/database/init/xl_tools.py new file mode 100644 index 0000000..53d1087 --- /dev/null +++ b/modules/database/init/xl_tools.py @@ -0,0 +1,35 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_xl_tools' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import pandas as pd +from fastapi import UploadFile + +def create_dataframes(excel_file, return_clean=False): + excel_sheets = pd.read_excel(excel_file, sheet_name=None) + # Log the sheet names + logging.info(f"Sheet names: {excel_sheets.keys()}") + return {sheet.lower(): data for sheet, data in excel_sheets.items()} + +def create_dataframes_from_fastapiuploadfile(upload_file: UploadFile): + from io import BytesIO + file_content = upload_file.file.read() + file_content_io = BytesIO(file_content) + return pd.read_excel(file_content_io, sheet_name=None, engine='openpyxl') + +def replace_nan_with_default(data, default_values): + for key in default_values: + if pd.isna(data.get(key, None)): + # logging.debug(f"Replacing NaN in {key} with default value '{default_values[key]}'") + data[key] = default_values[key] + return data diff --git a/modules/database/neo4j/__init__.py b/modules/database/neo4j/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/neo4j/utils/__init__.py b/modules/database/neo4j/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/neo4j/utils/neontology.py b/modules/database/neo4j/utils/neontology.py new file mode 100644 index 0000000..f79f3ae --- /dev/null +++ b/modules/database/neo4j/utils/neontology.py @@ -0,0 +1,221 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +from modules.database.tools.neontology.graphconnection import GraphConnection, init_neontology +from modules.database.tools.neontology.basenode import BaseNode +from modules.database.tools.neontology.baserelationship import BaseRelationship +from typing import Optional, Dict, Any, List +from neo4j import Record as Neo4jRecord +import re + +log_name = 'api_modules_database_neo4j_utils_neontology' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +class NeontologyProvider: + """Provider class for managing Neontology connections and operations.""" + + def __init__(self): + """Initialize the provider with Neo4j connection details from environment.""" + self.bolt_url = os.getenv("APP_BOLT_URL") + self.user = os.getenv("USER_NEO4J") + self.password = os.getenv("PASSWORD_NEO4J") + self.connection = None + self.current_database = None + + def _validate_database_name(self, database: str) -> str: + """Validate and format database name to handle special characters.""" + if not database: + raise ValueError("Database name cannot be empty") + + if not re.match(r'^[a-zA-Z0-9_\.]+$', database): + raise ValueError("Database name contains invalid characters") + + if database.count('.') > 1: + parts = database.split('.') + if len(parts) > 2: + formatted_name = f"{parts[0]}.{'.'.join(parts[1:])}" + logging.info(f"Reformatted database name from {database} to {formatted_name}") + return formatted_name + + return database + + def connect(self, database: str = 'neo4j') -> None: + """Establish connection to Neo4j using Neontology.""" + try: + formatted_database = self._validate_database_name(database) + + if self.current_database != formatted_database and self.connection is not None: + self.close() + + if self.connection is None: + init_neontology( + neo4j_uri=self.bolt_url, + neo4j_username=self.user, + neo4j_password=self.password + ) + self.connection = GraphConnection() + self.current_database = formatted_database + logging.info(f"Neontology connection initialized with host: {self.host}, port: {self.port}, database: {formatted_database}") + + except Exception as e: + logging.error(f"Failed to initialize Neontology connection: {str(e)}") + raise + + def list_databases(self) -> List[str]: + """List all available Neo4j databases.""" + try: + with self.connection.driver.session() as session: + result = session.run("SHOW DATABASES") + return [record["name"] for record in result] + except Exception as e: + logging.error(f"Error listing databases: {str(e)}") + raise + + def check_database_exists(self, database_name: str) -> bool: + """Check if a specific database exists.""" + try: + databases = self.list_databases() + return database_name in databases + except Exception as e: + logging.error(f"Error checking database existence: {str(e)}") + return False + + def create_database(self, database_name: str) -> None: + """Create a new Neo4j database.""" + try: + formatted_name = self._validate_database_name(database_name) + with self.connection.driver.session() as session: + session.run(f"CREATE DATABASE {formatted_name} IF NOT EXISTS") + logging.info(f"Created database: {formatted_name}") + except Exception as e: + logging.error(f"Error creating database: {str(e)}") + raise + + def reset_connection(self) -> None: + """Reset the connection, forcing a new one to be created on next use.""" + if self.connection: + self.close() + + def create_or_merge_node(self, node: BaseNode, database: str = 'neo4j', operation: str = "merge") -> None: + """Create or merge a node in the Neo4j database.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + + if operation == "create": + node.create(database=database) + elif operation == "merge": + node.merge(database=database) + else: + logging.error(f"Invalid operation: {operation}") + raise ValueError(f"Invalid operation: {operation}") + except Exception as e: + logging.error(f"Error in processing node: {e}") + raise + + def create_or_merge_relationship(self, relationship: BaseRelationship, database: str = 'neo4j', operation: str = "merge") -> None: + """Create or merge a relationship in the Neo4j database.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + + if operation == "create": + relationship.create(database=database) + elif operation == "merge": + relationship.merge(database=database) + else: + logging.error(f"Invalid operation: {operation}") + raise ValueError(f"Invalid operation: {operation}") + except Exception as e: + logging.error(f"Error in processing relationship: {e}") + raise + + def cypher_write(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> None: + """Execute a write transaction.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + self.connection.cypher_write(cypher, params) + except Exception as e: + logging.error(f"Error in cypher write: {e}") + raise + + def cypher_read(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> Optional[Neo4jRecord]: + """Execute a read transaction returning a single record.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + return self.connection.cypher_read(cypher, params) + except Exception as e: + logging.error(f"Error in cypher read: {e}") + raise + + def cypher_read_many(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Neo4jRecord]: + """Execute a read transaction returning multiple records.""" + try: + if not self.connection or self.current_database != database: + self.connect(database) + return self.connection.cypher_read_many(cypher, params) + except Exception as e: + logging.error(f"Error in cypher read many: {e}") + raise + + def run_query(self, cypher: str, params: Dict[str, Any] = {}, database: str = 'neo4j') -> List[Dict[str, Any]]: + """ + Execute a Cypher query and return results as a list of dictionaries. + This is a convenience method that handles both single and multiple record results. + + Args: + cypher: The Cypher query to execute + params: Query parameters + database: Target database name + + Returns: + List[Dict[str, Any]]: Query results as a list of dictionaries + """ + try: + if not self.connection or self.current_database != database: + self.connect(database) + + # Use cypher_read_many for consistent return type + records = self.connection.cypher_read_many(cypher, params) + + # Convert Neo4j records to dictionaries + results = [] + for record in records: + # Handle both Record and dict types + if isinstance(record, Neo4jRecord): + results.append(dict(record)) + else: + results.append(record) + + return results + + except Exception as e: + logging.error(f"Error in run_query: {e}") + raise + + def close(self) -> None: + """Close the Neontology connection.""" + if self.connection: + # The connection will be closed when the GraphConnection instance is deleted + self.connection = None + self.current_database = None + logging.info("Neontology connection closed") + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.close() diff --git a/modules/database/schemas/__init__.py b/modules/database/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a254fd4 Binary files /dev/null and b/modules/database/schemas/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/__pycache__/entities.cpython-311.pyc b/modules/database/schemas/__pycache__/entities.cpython-311.pyc new file mode 100644 index 0000000..aa3a537 Binary files /dev/null and b/modules/database/schemas/__pycache__/entities.cpython-311.pyc differ diff --git a/modules/database/schemas/__pycache__/owners.cpython-311.pyc b/modules/database/schemas/__pycache__/owners.cpython-311.pyc new file mode 100644 index 0000000..bc0d0c5 Binary files /dev/null and b/modules/database/schemas/__pycache__/owners.cpython-311.pyc differ diff --git a/modules/database/schemas/__pycache__/structures.cpython-311.pyc b/modules/database/schemas/__pycache__/structures.cpython-311.pyc new file mode 100644 index 0000000..8b97148 Binary files /dev/null and b/modules/database/schemas/__pycache__/structures.cpython-311.pyc differ diff --git a/modules/database/schemas/entities.py b/modules/database/schemas/entities.py new file mode 100644 index 0000000..11ae864 --- /dev/null +++ b/modules/database/schemas/entities.py @@ -0,0 +1,29 @@ +from typing import Union +import modules.database.schemas.nodes.users as user_schemas +import modules.database.schemas.nodes.workers.workers as worker_schemas +import modules.database.schemas.nodes.schools.schools as school_schemas + +user_entities = [ + user_schemas.UserNode +] + +worker_entities = Union[ + worker_schemas.TeacherNode, + worker_schemas.StudentNode, + worker_schemas.SchoolAdminNode, + worker_schemas.DeveloperNode, + worker_schemas.SuperAdminNode, +] + +school_entities = Union[ + school_schemas.SchoolNode, + school_schemas.DepartmentNode, + school_schemas.SubjectClassNode, + school_schemas.RoomNode, +] + +entities = Union[ + user_entities, + worker_entities, + school_entities, +] \ No newline at end of file diff --git a/modules/database/schemas/graph/__init__.py b/modules/database/schemas/graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/graph/nodes/__init__.py b/modules/database/schemas/graph/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/graph/nodes/schools/__init__.py b/modules/database/schemas/graph/nodes/schools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/graph/nodes/structures/__init__.py b/modules/database/schemas/graph/nodes/structures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/graph/nodes/workers/__init__.py b/modules/database/schemas/graph/nodes/workers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/graph/relationships/__init__.py b/modules/database/schemas/graph/relationships/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/nodes/__init__.py b/modules/database/schemas/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/nodes/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/nodes/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..cf7098b Binary files /dev/null and b/modules/database/schemas/nodes/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/__pycache__/base_nodes.cpython-311.pyc b/modules/database/schemas/nodes/__pycache__/base_nodes.cpython-311.pyc new file mode 100644 index 0000000..2a31523 Binary files /dev/null and b/modules/database/schemas/nodes/__pycache__/base_nodes.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/__pycache__/calendars.cpython-311.pyc b/modules/database/schemas/nodes/__pycache__/calendars.cpython-311.pyc new file mode 100644 index 0000000..a319395 Binary files /dev/null and b/modules/database/schemas/nodes/__pycache__/calendars.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/__pycache__/users.cpython-311.pyc b/modules/database/schemas/nodes/__pycache__/users.cpython-311.pyc new file mode 100644 index 0000000..893d1cb Binary files /dev/null and b/modules/database/schemas/nodes/__pycache__/users.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/base_nodes.py b/modules/database/schemas/nodes/base_nodes.py new file mode 100644 index 0000000..fe2242a --- /dev/null +++ b/modules/database/schemas/nodes/base_nodes.py @@ -0,0 +1,52 @@ +from typing import ClassVar +from modules.database.tools.neontology.basenode import BaseNode + +class CCBaseNode(BaseNode): + __primarylabel__: ClassVar[str] = '' + __primaryproperty__: ClassVar[str] = 'unique_id' + unique_id: str + tldraw_snapshot: str + + def to_dict(self): + return { + "__primarylabel__": self.__primarylabel__, + "unique_id": self.unique_id, + "tldraw_snapshot": self.tldraw_snapshot, + } + +class UserBaseNode(CCBaseNode): + __primarylabel__: ClassVar[str] = '' + cc_username: str + user_email: str + user_db_name: str + user_name: str + user_type: str + + def to_dict(self): + return { + "__primarylabel__": self.__primarylabel__, + "unique_id": self.unique_id, + "tldraw_snapshot": self.tldraw_snapshot, + "cc_username": self.cc_username, + "user_db_name": self.user_db_name, + "user_email": self.user_email, + "user_name": self.user_name, + "user_type": self.user_type, + } + +class WorkerBaseNode(CCBaseNode): + __primarylabel__: ClassVar[str] = '' + worker_name: str + worker_email: str + worker_db_name: str + worker_type: str + def to_dict(self): + return { + "__primarylabel__": self.__primarylabel__, + "unique_id": self.unique_id, + "tldraw_snapshot": self.tldraw_snapshot, + "worker_name": self.worker_name, + "worker_email": self.worker_email, + "worker_db_name": self.worker_db_name, + "worker_type": self.worker_type, + } diff --git a/modules/database/schemas/nodes/calendars.py b/modules/database/schemas/nodes/calendars.py new file mode 100644 index 0000000..3744c9d --- /dev/null +++ b/modules/database/schemas/nodes/calendars.py @@ -0,0 +1,36 @@ +import datetime +from typing import ClassVar +from .base_nodes import CCBaseNode + +class CalendarNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'Calendar' + name: str + start_date: datetime.date + end_date: datetime.date + +class CalendarYearNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'CalendarYear' + year: str + +class CalendarMonthNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'CalendarMonth' + year: str + month: str + month_name: str + +class CalendarWeekNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'CalendarWeek' + start_date: datetime.date + week_number: str + iso_week: str # ISO 8601 week + +class CalendarDayNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'CalendarDay' + date: datetime.date + day_of_week: str + iso_day: str # ISO 8601 day + +class CalendarTimeChunkNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'CalendarTimeChunk' + start_time: datetime.time + end_time: datetime.time diff --git a/modules/database/schemas/nodes/schools/__init__.py b/modules/database/schemas/nodes/schools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/nodes/schools/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/nodes/schools/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7e76dcb Binary files /dev/null and b/modules/database/schemas/nodes/schools/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/schools/__pycache__/curriculum.cpython-311.pyc b/modules/database/schemas/nodes/schools/__pycache__/curriculum.cpython-311.pyc new file mode 100644 index 0000000..ce8c0b6 Binary files /dev/null and b/modules/database/schemas/nodes/schools/__pycache__/curriculum.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/schools/__pycache__/pastoral.cpython-311.pyc b/modules/database/schemas/nodes/schools/__pycache__/pastoral.cpython-311.pyc new file mode 100644 index 0000000..77955ff Binary files /dev/null and b/modules/database/schemas/nodes/schools/__pycache__/pastoral.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/schools/__pycache__/schools.cpython-311.pyc b/modules/database/schemas/nodes/schools/__pycache__/schools.cpython-311.pyc new file mode 100644 index 0000000..70a5019 Binary files /dev/null and b/modules/database/schemas/nodes/schools/__pycache__/schools.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/schools/__pycache__/timetable.cpython-311.pyc b/modules/database/schemas/nodes/schools/__pycache__/timetable.cpython-311.pyc new file mode 100644 index 0000000..2e879e3 Binary files /dev/null and b/modules/database/schemas/nodes/schools/__pycache__/timetable.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/schools/curriculum.py b/modules/database/schemas/nodes/schools/curriculum.py new file mode 100644 index 0000000..06b92e5 --- /dev/null +++ b/modules/database/schemas/nodes/schools/curriculum.py @@ -0,0 +1,56 @@ +from typing import ClassVar, Optional +from ..base_nodes import CCBaseNode + +class CurriculumBaseNode(CCBaseNode): + __primarylabel__: ClassVar[str] = '' + +class KeyStageNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'KeyStage' + key_stage: str + name: str + +class KeyStageSyllabusNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'KeyStageSyllabus' + id: str + name: str + key_stage: str + subject_name: str + +class SubjectNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'Subject' + id: str + name: str + +class TopicNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'Topic' + id: str + name: str + type: str + assessment_type: str + total_number_of_lessons_for_topic: str + +class TopicLessonNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'TopicLesson' + id: str + name: str + type: str + length: Optional[str] = None + suggested_activities: Optional[str] = None + skills_learned: Optional[str] = None + weblinks: Optional[str] = None + +class LearningStatementNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'LearningStatement' + id: str + name: str + type: str + +class ScienceLabNode(CurriculumBaseNode): + __primarylabel__: ClassVar[str] = 'ScienceLab' + id: str + name: str + summary: Optional[str] = None + requirements: Optional[str] = None + procedure: Optional[str] = None + safety: Optional[str] = None + weblinks: Optional[str] = None diff --git a/modules/database/schemas/nodes/schools/pastoral.py b/modules/database/schemas/nodes/schools/pastoral.py new file mode 100644 index 0000000..73eba63 --- /dev/null +++ b/modules/database/schemas/nodes/schools/pastoral.py @@ -0,0 +1,14 @@ +from typing import ClassVar, Optional +from ..base_nodes import CCBaseNode + +class YearGroupNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'YearGroup' + year_group: str + name: str + +class YearGroupSyllabusNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'YearGroupSyllabus' + id: str + name: str + year_group: str + subject_name: str diff --git a/modules/database/schemas/nodes/schools/schools.py b/modules/database/schemas/nodes/schools/schools.py new file mode 100644 index 0000000..0b94721 --- /dev/null +++ b/modules/database/schemas/nodes/schools/schools.py @@ -0,0 +1,60 @@ +from typing import ClassVar, Optional +from ..base_nodes import CCBaseNode + +class SchoolNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'School' + + # Core identification fields (required for all databases) + id: str # School's unique identifier within its type + school_type: str # e.g., 'development', 'state', 'private', etc. + name: str + website: str = 'unknown' + + # Public school fields (required for public database) + statutory_low_age: Optional[int] = None + statutory_high_age: Optional[int] = None + phase_of_education: Optional[str] = None + school_capacity: Optional[int] = None + religious_character: Optional[str] = None + ofsted_rating: Optional[str] = None + + # Private school fields (required for private database) + establishment_number: Optional[str] = None + establishment_name: Optional[str] = None + establishment_type: Optional[str] = None + establishment_status: Optional[str] = None + + @property + def database_id(self) -> str: + """Get the full database identifier for this school""" + return f"{self.school_type}.{self.id}" + + @property + def public_database_name(self) -> str: + """Get the public database name for this school""" + return "cc.institutes" + + @property + def private_database_name(self) -> str: + """Get the private database name for this school""" + return f"cc.institutes.{self.database_id}" + + @property + def curriculum_database_name(self) -> str: + """Get the curriculum database name for this school""" + return f"{self.private_database_name}.curriculum" + +class DepartmentNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'Department' + name: str + +class SubjectClassNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'SubjectClass' + name: Optional[str] = 'unknown' + year_group_id: str + subject_id: str + +class RoomNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'Room' + name: Optional[str] = 'unknown' + building_id: Optional[str] = 'unknown' diff --git a/modules/database/schemas/nodes/schools/timetable.py b/modules/database/schemas/nodes/schools/timetable.py new file mode 100644 index 0000000..165551f --- /dev/null +++ b/modules/database/schemas/nodes/schools/timetable.py @@ -0,0 +1,88 @@ +import datetime +from typing import ClassVar +from ..base_nodes import CCBaseNode + +class SchoolTimetableNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'SchoolTimetable' + school_timetable_id: str + start_date: datetime.date + end_date: datetime.date + +class AcademicYearNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'AcademicYear' + year: str + +class AcademicTermNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'AcademicTerm' + term_name: str + term_number: str + start_date: datetime.date + end_date: datetime.date + +class AcademicTermBreakNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'AcademicTermBreak' + term_break_name: str + start_date: datetime.date + end_date: datetime.date + +class AcademicWeekNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'AcademicWeek' + academic_week_number: str + start_date: datetime.date + week_type: str + +class HolidayWeekNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'HolidayWeek' + start_date: datetime.date + +class AcademicDayNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'AcademicDay' + academic_day: str + date: datetime.date + day_of_week: str + day_type: str + +class OffTimetableDayNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'OffTimetableDay' + date: datetime.date + day_of_week: str + +class StaffDayNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'StaffDay' + date: datetime.date + day_of_week: str + +class HolidayDayNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'HolidayDay' + date: datetime.date + day_of_week: str + +class AcademicPeriodNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'AcademicPeriod' + name: str + date: datetime.date + start_time: datetime.datetime + end_time: datetime.datetime + period_code: str + +class RegistrationPeriodNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'RegistrationPeriod' + name: str + date: datetime.date + start_time: datetime.datetime + end_time: datetime.datetime + period_code: str + +class BreakPeriodNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'BreakPeriod' + name: str + date: datetime.date + start_time: datetime.datetime + end_time: datetime.datetime + +class OffTimetablePeriodNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'OffTimetablePeriod' + name: str + date: datetime.date + start_time: datetime.datetime + end_time: datetime.datetime diff --git a/modules/database/schemas/nodes/structures/__init__.py b/modules/database/schemas/nodes/structures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/nodes/structures/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/nodes/structures/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..2548b29 Binary files /dev/null and b/modules/database/schemas/nodes/structures/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/structures/__pycache__/schools.cpython-311.pyc b/modules/database/schemas/nodes/structures/__pycache__/schools.cpython-311.pyc new file mode 100644 index 0000000..7f7152a Binary files /dev/null and b/modules/database/schemas/nodes/structures/__pycache__/schools.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/structures/__pycache__/users.cpython-311.pyc b/modules/database/schemas/nodes/structures/__pycache__/users.cpython-311.pyc new file mode 100644 index 0000000..c58ac17 Binary files /dev/null and b/modules/database/schemas/nodes/structures/__pycache__/users.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/structures/__pycache__/workers.cpython-311.pyc b/modules/database/schemas/nodes/structures/__pycache__/workers.cpython-311.pyc new file mode 100644 index 0000000..584d030 Binary files /dev/null and b/modules/database/schemas/nodes/structures/__pycache__/workers.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/structures/schools.py b/modules/database/schemas/nodes/structures/schools.py new file mode 100644 index 0000000..1efd22e --- /dev/null +++ b/modules/database/schemas/nodes/structures/schools.py @@ -0,0 +1,23 @@ +from typing import ClassVar +from ..base_nodes import CCBaseNode + +class DepartmentStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'DepartmentStructure' + +class PastoralStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'PastoralStructure' + +class CurriculumStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'CurriculumStructure' + +class SiteStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'SiteStructure' + +class StaffStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'StaffStructure' + +class StudentStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'StudentStructure' + +class ITAdminStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'ITAdminStructure' \ No newline at end of file diff --git a/modules/database/schemas/nodes/structures/users.py b/modules/database/schemas/nodes/structures/users.py new file mode 100644 index 0000000..10409ed --- /dev/null +++ b/modules/database/schemas/nodes/structures/users.py @@ -0,0 +1,8 @@ +from typing import ClassVar +from ..base_nodes import CCBaseNode + +class WorkerStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'WorkerStructure' + +class UserCalendarStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'UserCalendarStructure' \ No newline at end of file diff --git a/modules/database/schemas/nodes/structures/workers.py b/modules/database/schemas/nodes/structures/workers.py new file mode 100644 index 0000000..2aded87 --- /dev/null +++ b/modules/database/schemas/nodes/structures/workers.py @@ -0,0 +1,5 @@ +from typing import ClassVar, Optional +from ..base_nodes import CCBaseNode + +class WorkerTimetableStructureNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'WorkerTimetableStructure' diff --git a/modules/database/schemas/nodes/users.py b/modules/database/schemas/nodes/users.py new file mode 100644 index 0000000..f58b5f0 --- /dev/null +++ b/modules/database/schemas/nodes/users.py @@ -0,0 +1,5 @@ +from typing import ClassVar +from .base_nodes import UserBaseNode + +class UserNode(UserBaseNode): + __primarylabel__: ClassVar[str] = 'User' diff --git a/modules/database/schemas/nodes/workers/__init__.py b/modules/database/schemas/nodes/workers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/nodes/workers/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/nodes/workers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c77f83a Binary files /dev/null and b/modules/database/schemas/nodes/workers/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/workers/__pycache__/timetable.cpython-311.pyc b/modules/database/schemas/nodes/workers/__pycache__/timetable.cpython-311.pyc new file mode 100644 index 0000000..ca4a8ef Binary files /dev/null and b/modules/database/schemas/nodes/workers/__pycache__/timetable.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/workers/__pycache__/workers.cpython-311.pyc b/modules/database/schemas/nodes/workers/__pycache__/workers.cpython-311.pyc new file mode 100644 index 0000000..a04be64 Binary files /dev/null and b/modules/database/schemas/nodes/workers/__pycache__/workers.cpython-311.pyc differ diff --git a/modules/database/schemas/nodes/workers/timetable.py b/modules/database/schemas/nodes/workers/timetable.py new file mode 100644 index 0000000..819d66a --- /dev/null +++ b/modules/database/schemas/nodes/workers/timetable.py @@ -0,0 +1,63 @@ +import datetime +from typing import ClassVar, Optional, List, Union +from ..base_nodes import CCBaseNode + +class EntityTimetableBaseNode(CCBaseNode): + start_date: datetime.date + end_date: datetime.date + +class WorkerTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'WorkerTimetable' + worker_timetable_id: str + +class TeacherTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'TeacherTimetable' + teacher_timetable_id: str + +class UserTeacherTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'UserTeacherTimetable' + user_teacher_timetable_id: str + +class StudentTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'StudentTimetable' + student_timetable_id: str + +class SchoolAdminTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'SchoolAdminTimetable' + school_admin_timetable_id: str + +class DeveloperTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'DeveloperTimetable' + developer_timetable_id: str + +class SuperAdminTimetableNode(EntityTimetableBaseNode): + __primarylabel__: ClassVar[str] = 'SuperAdminTimetable' + super_admin_timetable_id: str + +class TimetableLessonNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'TimetableLesson' + subject_class: str + date: datetime.date + start_time: datetime.time + end_time: datetime.time + period_code: str + +class PlannedLessonNode(CCBaseNode): + __primarylabel__: ClassVar[str] = 'PlannedLesson' + date: datetime.date + start_time: datetime.time + end_time: datetime.time + period_code: str + subject_class: str + year_group: str + subject: str + teacher_code: str + planning_status: str + topic_code: Optional[str] = None + topic_name: Optional[str] = None + lesson_code: Optional[str] = None + lesson_name: Optional[str] = None + learning_statement_codes: Optional[List[str]] = None + learning_statements: Optional[List[str]] = None + learning_resource_codes: Optional[List[str]] = None + learning_resources: Optional[List[str]] = None \ No newline at end of file diff --git a/modules/database/schemas/nodes/workers/workers.py b/modules/database/schemas/nodes/workers/workers.py new file mode 100644 index 0000000..9b87d5e --- /dev/null +++ b/modules/database/schemas/nodes/workers/workers.py @@ -0,0 +1,19 @@ +import datetime +from typing import ClassVar +from ..base_nodes import WorkerBaseNode + +class SchoolAdminNode(WorkerBaseNode): + __primarylabel__: ClassVar[str] = 'SchoolAdmin' + +class TeacherNode(WorkerBaseNode): + __primarylabel__: ClassVar[str] = 'Teacher' + +class StudentNode(WorkerBaseNode): + __primarylabel__: ClassVar[str] = 'Student' + +class DeveloperNode(WorkerBaseNode): + __primarylabel__: ClassVar[str] = 'Developer' + developer_role: str # To distinguish between admin/developer roles + +class SuperAdminNode(WorkerBaseNode): + __primarylabel__: ClassVar[str] = 'SuperAdmin' diff --git a/modules/database/schemas/owners.py b/modules/database/schemas/owners.py new file mode 100644 index 0000000..48b4ab6 --- /dev/null +++ b/modules/database/schemas/owners.py @@ -0,0 +1,29 @@ +from typing import Union +import modules.database.schemas.nodes.users as user_schemas +import modules.database.schemas.nodes.workers.workers as worker_schemas +import modules.database.schemas.nodes.schools.schools as school_schemas + +user_owners = Union[ + user_schemas.UserNode +] + +worker_owners = Union[ + worker_schemas.TeacherNode, + worker_schemas.StudentNode, + worker_schemas.SchoolAdminNode, + worker_schemas.DeveloperNode, + worker_schemas.SuperAdminNode, +] + +school_owners = Union[ + school_schemas.SchoolNode, + school_schemas.DepartmentNode, + school_schemas.SubjectClassNode, + school_schemas.RoomNode, +] + +owners = Union[ + user_owners, + worker_owners, + school_owners, +] \ No newline at end of file diff --git a/modules/database/schemas/relationships/__init__.py b/modules/database/schemas/relationships/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/relationships/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9e6c53f Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/calendar_sequence.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/calendar_sequence.cpython-311.pyc new file mode 100644 index 0000000..11f7e2a Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/calendar_sequence.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/calendar_timetable_rels.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/calendar_timetable_rels.cpython-311.pyc new file mode 100644 index 0000000..6b157b6 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/calendar_timetable_rels.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/calendars.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/calendars.cpython-311.pyc new file mode 100644 index 0000000..a4c6fe1 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/calendars.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/curriculum_relationships.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/curriculum_relationships.cpython-311.pyc new file mode 100644 index 0000000..ef1b58d Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/curriculum_relationships.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/entity_curriculum_rels.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/entity_curriculum_rels.cpython-311.pyc new file mode 100644 index 0000000..5d9b188 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/entity_curriculum_rels.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/entity_relationships.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/entity_relationships.cpython-311.pyc new file mode 100644 index 0000000..78a6c6e Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/entity_relationships.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/entity_timetable_rels.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/entity_timetable_rels.cpython-311.pyc new file mode 100644 index 0000000..b6a5f35 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/entity_timetable_rels.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/owner_relationships.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/owner_relationships.cpython-311.pyc new file mode 100644 index 0000000..3139b32 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/owner_relationships.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/planning_relationships.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/planning_relationships.cpython-311.pyc new file mode 100644 index 0000000..e241462 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/planning_relationships.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/__pycache__/timetables.cpython-311.pyc b/modules/database/schemas/relationships/__pycache__/timetables.cpython-311.pyc new file mode 100644 index 0000000..28b54d1 Binary files /dev/null and b/modules/database/schemas/relationships/__pycache__/timetables.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/calendar_sequence.py b/modules/database/schemas/relationships/calendar_sequence.py new file mode 100644 index 0000000..2186742 --- /dev/null +++ b/modules/database/schemas/relationships/calendar_sequence.py @@ -0,0 +1,29 @@ +import modules.database.schemas.nodes.calendars as calendar_schemas +from modules.database.tools.neontology.baserelationship import BaseRelationship +from typing import ClassVar + +# Sequenced Relationships for Calendar +class YearFollowsYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_FOLLOWS_YEAR' + source: calendar_schemas.CalendarYearNode + target: calendar_schemas.CalendarYearNode + +class MonthFollowsMonth(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'MONTH_FOLLOWS_MONTH' + source: calendar_schemas.CalendarMonthNode + target: calendar_schemas.CalendarMonthNode + +class WeekFollowsWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'WEEK_FOLLOWS_WEEK' + source: calendar_schemas.CalendarWeekNode + target: calendar_schemas.CalendarWeekNode + +class DayFollowsDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'DAY_FOLLOWS_DAY' + source: calendar_schemas.CalendarDayNode + target: calendar_schemas.CalendarDayNode + +class TimeChunkFollowsTimeChunk(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TIME_CHUNK_FOLLOWS_TIME_CHUNK' + source: calendar_schemas.CalendarTimeChunkNode + target: calendar_schemas.CalendarTimeChunkNode diff --git a/modules/database/schemas/relationships/calendar_timetable_rels.py b/modules/database/schemas/relationships/calendar_timetable_rels.py new file mode 100644 index 0000000..1c5d2cd --- /dev/null +++ b/modules/database/schemas/relationships/calendar_timetable_rels.py @@ -0,0 +1,96 @@ +import modules.database.schemas.nodes.schools.timetable as neo_timetable +import modules.database.schemas.nodes.calendars as neo_calendar +import modules.database.schemas.nodes.workers.timetable as neo_teacher_timetable +from modules.database.tools.neontology.baserelationship import BaseRelationship +from typing import ClassVar + +class CalendarYearIsAcademicYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_ACADEMIC_YEAR' + source: neo_calendar.CalendarYearNode + target: neo_timetable.AcademicYearNode + +class AcademicYearIsCalendarYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_YEAR' + source: neo_timetable.AcademicYearNode + target: neo_calendar.CalendarYearNode + +class CalendarWeekIsAcademicWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_ACADEMIC_WEEK' + source: neo_calendar.CalendarWeekNode + target: neo_timetable.AcademicWeekNode + +class AcademicWeekIsCalendarWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_WEEK' + source: neo_timetable.AcademicWeekNode + target: neo_calendar.CalendarWeekNode + +class CalendarWeekIsHolidayWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_HOLIDAY_WEEK' + source: neo_calendar.CalendarWeekNode + target: neo_timetable.HolidayWeekNode + +class HolidayWeekIsCalendarWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_WEEK' + source: neo_timetable.HolidayWeekNode + target: neo_calendar.CalendarWeekNode + +class CalendarDayIsAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_ACADEMIC_DAY' + source: neo_calendar.CalendarDayNode + target: neo_timetable.AcademicDayNode + +class AcademicDayIsCalendarDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY' + source: neo_timetable.AcademicDayNode + target: neo_calendar.CalendarDayNode + +class CalendarDayIsHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_HOLIDAY_DAY' + source: neo_calendar.CalendarDayNode + target: neo_timetable.HolidayDayNode + +class HolidayDayIsCalendarDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY' + source: neo_timetable.HolidayDayNode + target: neo_calendar.CalendarDayNode + +class CalendarDayIsOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_OFF_TIMETABLE_DAY' + source: neo_calendar.CalendarDayNode + target: neo_timetable.OffTimetableDayNode + +class OffTimetableDayIsCalendarDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY' + source: neo_timetable.OffTimetableDayNode + target: neo_calendar.CalendarDayNode + +class CalendarDayIsStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_STAFF_DAY' + source: neo_calendar.CalendarDayNode + target: neo_timetable.StaffDayNode + +class StaffDayIsCalendarDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_CALENDAR_DAY' + source: neo_timetable.StaffDayNode + target: neo_calendar.CalendarDayNode + +# New relationships for user timetable +class CalendarDayHasTimetableLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_TIMETABLE_LESSON' + source: neo_calendar.CalendarDayNode + target: neo_teacher_timetable.TimetableLessonNode + +class TimetableLessonBelongsToCalendarDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BELONGS_TO_CALENDAR_DAY' + source: neo_teacher_timetable.TimetableLessonNode + target: neo_calendar.CalendarDayNode + +class CalendarDayHasPlannedLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_PLANNED_LESSON' + source: neo_calendar.CalendarDayNode + target: neo_teacher_timetable.PlannedLessonNode + +class PlannedLessonBelongsToCalendarDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BELONGS_TO_CALENDAR_DAY' + source: neo_teacher_timetable.PlannedLessonNode + target: neo_calendar.CalendarDayNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/calendars.py b/modules/database/schemas/relationships/calendars.py new file mode 100644 index 0000000..bb822fa --- /dev/null +++ b/modules/database/schemas/relationships/calendars.py @@ -0,0 +1,34 @@ +import modules.database.schemas.nodes.calendars as calendar_schemas +from modules.database.tools.neontology.baserelationship import BaseRelationship +from typing import ClassVar + +## Calendar layer relationships +class CalendarIncludesYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'CALENDAR_INCLUDES_YEAR' + source: calendar_schemas.CalendarNode + target: calendar_schemas.CalendarYearNode + +class YearIncludesMonth(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_INCLUDES_MONTH' + source: calendar_schemas.CalendarYearNode + target: calendar_schemas.CalendarMonthNode + +class YearIncludesWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_INCLUDES_WEEK' + source: calendar_schemas.CalendarYearNode + target: calendar_schemas.CalendarWeekNode + +class MonthIncludesDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'MONTH_INCLUDES_DAY' + source: calendar_schemas.CalendarMonthNode + target: calendar_schemas.CalendarDayNode + +class WeekIncludesDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'WEEK_INCLUDES_DAY' + source: calendar_schemas.CalendarWeekNode + target: calendar_schemas.CalendarDayNode + +class DayIncludesTimeChunk(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'DAY_INCLUDES_TIME_CHUNK' + source: calendar_schemas.CalendarDayNode + target: calendar_schemas.CalendarTimeChunkNode diff --git a/modules/database/schemas/relationships/curriculum_relationships.py b/modules/database/schemas/relationships/curriculum_relationships.py new file mode 100644 index 0000000..9a8e980 --- /dev/null +++ b/modules/database/schemas/relationships/curriculum_relationships.py @@ -0,0 +1,119 @@ +from typing import ClassVar +from modules.database.tools.neontology.baserelationship import BaseRelationship + +import modules.database.schemas.nodes.schools.pastoral as pastoral_nodes +import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes +import modules.database.schemas.nodes.structures.schools as school_structures + +# Structure layer relationships +class CurriculumStructureIncludesKeyStage(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'CURRICULUM_STRUCTURE_INCLUDES_KEY_STAGE' + source: school_structures.CurriculumStructureNode + target: curriculum_nodes.KeyStageNode + +class PastoralStructureIncludesYearGroup(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'PASTORAL_STRUCTURE_INCLUDES_YEAR_GROUP' + source: school_structures.PastoralStructureNode + target: pastoral_nodes.YearGroupNode + +## Curriculum layer relationships +class SubjectForKeyStage(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'SUBJECT_FOR_KEY_STAGE' + source: curriculum_nodes.SubjectNode + target: curriculum_nodes.KeyStageNode + +class SubjectHasYearGroupSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'SUBJECT_HAS_YEAR_GROUP_SYLLABUS' + source: curriculum_nodes.SubjectNode + target: pastoral_nodes.YearGroupSyllabusNode + +class SubjectHasKeyStageSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'SUBJECT_HAS_KEY_STAGE_SYLLABUS' + source: curriculum_nodes.SubjectNode + target: curriculum_nodes.KeyStageSyllabusNode + +class TopicPartOfYearGroupSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_SYLLABUS_INCLUDES_TOPIC' + source: pastoral_nodes.YearGroupSyllabusNode + target: curriculum_nodes.TopicNode + +class KeyStageSyllabusIncludesTopic(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'KEY_STAGE_SYLLABUS_INCLUDES_TOPIC' + source: curriculum_nodes.KeyStageSyllabusNode + target: curriculum_nodes.TopicNode + +class YearGroupHasYearGroupSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_GROUP_HAS_YEAR_GROUP_SYLLABUS' + source: pastoral_nodes.YearGroupNode + target: pastoral_nodes.YearGroupSyllabusNode + +class KeyStageSyllabusIncludesYearGroupSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'KEY_STAGE_SYLLABUS_INCLUDES_YEAR_GROUP_SYLLABUS' + source: curriculum_nodes.KeyStageSyllabusNode + target: pastoral_nodes.YearGroupSyllabusNode + +class KeyStageIncludesKeyStageSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'KEY_STAGE_INCLUDES_KEY_STAGE_SYLLABUS' + source: curriculum_nodes.KeyStageNode + target: curriculum_nodes.KeyStageSyllabusNode + +class KeyStageIncludesTopic(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'KEY_STAGE_INCLUDES_TOPIC' + source: curriculum_nodes.KeyStageNode + target: curriculum_nodes.TopicNode + +class SubjectIncludesTopic(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'SUBJECT_INCLUDES_TOPIC' + source: curriculum_nodes.SubjectNode + target: curriculum_nodes.TopicNode + +class TopicIncludesTopicLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TOPIC_INCLUDES_LESSON' + source: curriculum_nodes.TopicNode + target: curriculum_nodes.TopicLessonNode + +class TopicIncludesLearningStatement(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TOPIC_INCLUDES_LEARNING_STATEMENT' + source: curriculum_nodes.TopicNode + target: curriculum_nodes.LearningStatementNode + +class LessonIncludesLearningStatement(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'LESSON_INCLUDES_LEARNING_STATEMENT' + source: curriculum_nodes.TopicLessonNode + target: curriculum_nodes.LearningStatementNode + +# Science-specific curriculum layer relationships +class TopicLessonIncludesScienceLab(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'LESSON_INCLUDES_SCIENCE_LAB' + source: curriculum_nodes.TopicLessonNode + target: curriculum_nodes.ScienceLabNode + +class KeyStageFollowsKeyStage(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'KEY_STAGE_FOLLOWS_KEY_STAGE' + source: curriculum_nodes.KeyStageNode + target: curriculum_nodes.KeyStageNode + +class YearGroupFollowsYearGroup(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_GROUP_FOLLOWS_YEAR_GROUP' + source: pastoral_nodes.YearGroupNode + target: pastoral_nodes.YearGroupNode + +class KeyStageSyllabusFollowsKeyStageSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'KEY_STAGE_SYLLABUS_FOLLOWS_KEY_STAGE_SYLLABUS' + source: curriculum_nodes.KeyStageSyllabusNode + target: curriculum_nodes.KeyStageSyllabusNode + +class YearGroupSyllabusFollowsYearGroupSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_GROUP_SYLLABUS_FOLLOWS_YEAR_GROUP_SYLLABUS' + source: pastoral_nodes.YearGroupSyllabusNode + target: pastoral_nodes.YearGroupSyllabusNode + +class TopicLessonFollowsTopicLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'LESSON_FOLLOWS_LESSON' + source: curriculum_nodes.TopicLessonNode + target: curriculum_nodes.TopicLessonNode + +class YearGroupSyllabusIncludesTopic(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'YEAR_GROUP_SYLLABUS_INCLUDES_TOPIC' + source: pastoral_nodes.YearGroupSyllabusNode + target: curriculum_nodes.TopicNode diff --git a/modules/database/schemas/relationships/entity_curriculum_rels.py b/modules/database/schemas/relationships/entity_curriculum_rels.py new file mode 100644 index 0000000..cc2d335 --- /dev/null +++ b/modules/database/schemas/relationships/entity_curriculum_rels.py @@ -0,0 +1,15 @@ +from typing import ClassVar +from modules.database.tools.neontology.baserelationship import BaseRelationship + +import modules.database.schemas.nodes.schools.schools as school_nodes +import modules.database.schemas.nodes.structures.schools as school_structures + +class SchoolHasCurriculumStructure(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_CURRICULUM_STRUCTURE' + source: school_nodes.SchoolNode + target: school_structures.CurriculumStructureNode + +class SchoolHasPastoralStructure(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_PASTORAL_STRUCTURE' + source: school_nodes.SchoolNode + target: school_structures.PastoralStructureNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/entity_relationships.py b/modules/database/schemas/relationships/entity_relationships.py new file mode 100644 index 0000000..50615c9 --- /dev/null +++ b/modules/database/schemas/relationships/entity_relationships.py @@ -0,0 +1,49 @@ +from typing import ClassVar, Union +import modules.database.schemas.entities as entities +import modules.database.schemas.nodes.users as user_nodes +import modules.database.schemas.nodes.workers.workers as worker_nodes +import modules.database.schemas.nodes.schools.schools as school_nodes +import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes +import modules.database.schemas.nodes.structures.schools as school_structures +from modules.database.tools.neontology.baserelationship import BaseRelationship + +class UserIsSchoolWorker(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_SCHOOL_WORKER' + source: user_nodes.UserNode + target: Union[worker_nodes.SchoolAdminNode, worker_nodes.TeacherNode, worker_nodes.StudentNode] + +class UserIsSystemWorker(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'IS_SYSTEM_WORKER' + source: user_nodes.UserNode + target: Union[worker_nodes.DeveloperNode, worker_nodes.SuperAdminNode] + +class EntityBelongsToSchool(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BELONGS_TO' + source: Union[entities.worker_entities, entities.school_entities] + target: entities.school_entities + +class EntityBelongsToDepartment(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BELONGS_TO' + source: entities.worker_entities + target: school_nodes.DepartmentNode + +class SchoolHasDepartmentStructure(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_DEPARTMENT_STRUCTURE' + source: school_nodes.SchoolNode + target: school_structures.DepartmentStructureNode + +class DepartmentStructureHasDepartment(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_DEPARTMENT' + source: school_structures.DepartmentStructureNode + target: school_nodes.DepartmentNode + +class DepartmentManagesKeyStageSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'MANAGES_KEY_STAGE_SYLLABUS' + source: school_nodes.DepartmentNode + target: curriculum_nodes.KeyStageSyllabusNode + +class DepartmentManagesSubject(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'MANAGES_SUBJECT' + source: school_nodes.DepartmentNode + target: curriculum_nodes.SubjectNode + diff --git a/modules/database/schemas/relationships/entity_timetable_rels.py b/modules/database/schemas/relationships/entity_timetable_rels.py new file mode 100644 index 0000000..27cc047 --- /dev/null +++ b/modules/database/schemas/relationships/entity_timetable_rels.py @@ -0,0 +1,18 @@ +from typing import ClassVar, Union +from modules.database.tools.neontology.baserelationship import BaseRelationship +import modules.database.schemas.nodes.users as user_nodes +import modules.database.schemas.nodes.workers.workers as worker_nodes +import modules.database.schemas.nodes.schools.schools as school_nodes +import modules.database.schemas.nodes.workers.timetable as worker_timetable +import modules.database.schemas.nodes.schools.timetable as school_timetable + +class EntityHasTimetable(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_TIMETABLE' + source: Union[user_nodes.UserNode, worker_nodes.TeacherNode, worker_nodes.StudentNode, school_nodes.SubjectClassNode] + target: worker_timetable.WorkerTimetableNode + + +class SchoolHasTimetable(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_TIMETABLE' + source: school_nodes.SchoolNode + target: school_timetable.SchoolTimetableNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/owner_relationships.py b/modules/database/schemas/relationships/owner_relationships.py new file mode 100644 index 0000000..159e4a0 --- /dev/null +++ b/modules/database/schemas/relationships/owner_relationships.py @@ -0,0 +1,9 @@ +from typing import ClassVar, Union +import modules.database.schemas.nodes.calendars as calendar_schemas +import modules.database.schemas.owners as owner_schemas +from modules.database.tools.neontology.baserelationship import BaseRelationship + +class OwnerHasCalendar(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HAS_CALENDAR' + source: owner_schemas.owners + target: calendar_schemas.CalendarNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/owner_structures.py b/modules/database/schemas/relationships/owner_structures.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/relationships/planning_relationships.py b/modules/database/schemas/relationships/planning_relationships.py new file mode 100644 index 0000000..70ba592 --- /dev/null +++ b/modules/database/schemas/relationships/planning_relationships.py @@ -0,0 +1,48 @@ +from typing import ClassVar, Union +from modules.database.tools.neontology.baserelationship import BaseRelationship +from modules.database.schemas.nodes.schools.timetable import AcademicPeriodNode, RegistrationPeriodNode +from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode +from modules.database.schemas.nodes.workers.workers import TeacherNode +from modules.database.schemas.nodes.schools.schools import SubjectClassNode +from modules.database.schemas.nodes.schools.pastoral import YearGroupSyllabusNode + +class TimetableLessonBelongsToPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'LESSON_BELONGS_TO_PERIOD' + source: TimetableLessonNode + target: Union[AcademicPeriodNode, RegistrationPeriodNode] + +class TimetableLessonHasPlannedLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TIMETABLE_LESSON_HAS_PLANNED_LESSON' + source: TimetableLessonNode + target: PlannedLessonNode + +class TeacherHasTimetable(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TEACHER_HAS_TIMETABLE' + source: TeacherNode + target: TeacherTimetableNode + +class TimetableHasClass(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TIMETABLE_HAS_CLASS' + source: TeacherTimetableNode + target: SubjectClassNode + +class ClassHasLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'CLASS_HAS_LESSON' + source: SubjectClassNode + target: TimetableLessonNode + +class TimetableLessonFollowsTimetableLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TIMETABLE_LESSON_FOLLOWS_TIMETABLE_LESSON' + source: Union[TimetableLessonNode, PlannedLessonNode] + target: Union[TimetableLessonNode, PlannedLessonNode] + + +class PlannedLessonFollowsPlannedLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'PLANNED_LESSON_FOLLOWS_PLANNED_LESSON' + source: PlannedLessonNode + target: PlannedLessonNode + +class SubjectClassBelongsToYearGroupSyllabus(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'SUBJECT_CLASS_BELONGS_TO_YEAR_GROUP_SYLLABUS' + source: SubjectClassNode + target: YearGroupSyllabusNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/school_structures.py b/modules/database/schemas/relationships/school_structures.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/relationships/sequencing/calendar_sequence_relationships.py b/modules/database/schemas/relationships/sequencing/calendar_sequence_relationships.py new file mode 100644 index 0000000..beb06c4 --- /dev/null +++ b/modules/database/schemas/relationships/sequencing/calendar_sequence_relationships.py @@ -0,0 +1,11 @@ +from neontology import BaseRelationship + +class CalendarYearHasNextCalendarYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'CALENDAR_YEAR_HAS_NEXT_CALENDAR_YEAR' + source: CalendarYearNode + target: CalendarYearNode + +class CalendarYearHasPreviousCalendarYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'CALENDAR_YEAR_HAS_PREVIOUS_CALENDAR_YEAR' + source: CalendarYearNode + target: CalendarYearNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/sequencing/curricular_sequencing_relationships.py b/modules/database/schemas/relationships/sequencing/curricular_sequencing_relationships.py new file mode 100644 index 0000000..2d93a3b --- /dev/null +++ b/modules/database/schemas/relationships/sequencing/curricular_sequencing_relationships.py @@ -0,0 +1,2 @@ +from neontology import BaseRelationship + diff --git a/modules/database/schemas/relationships/sequencing/planning_sequencing_relationships.py b/modules/database/schemas/relationships/sequencing/planning_sequencing_relationships.py new file mode 100644 index 0000000..491bd4f --- /dev/null +++ b/modules/database/schemas/relationships/sequencing/planning_sequencing_relationships.py @@ -0,0 +1,11 @@ +from neontology import BaseRelationship + +class PlannedLessonHasNextPlannedLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'PLANNED_LESSON_HAS_NEXT_PLANNED_LESSON' + source: PlannedLessonNode + target: PlannedLessonNode + +class PlannedLessonHasPreviousPlannedLesson(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'PLANNED_LESSON_HAS_PREVIOUS_PLANNED_LESSON' + source: PlannedLessonNode + target: PlannedLessonNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/sequencing/timetabling_sequencing_relationships.py b/modules/database/schemas/relationships/sequencing/timetabling_sequencing_relationships.py new file mode 100644 index 0000000..d50150b --- /dev/null +++ b/modules/database/schemas/relationships/sequencing/timetabling_sequencing_relationships.py @@ -0,0 +1,41 @@ +from neontology import BaseRelationship + +class AcademicTermHasNextTerm(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TERM_HAS_NEXT_TERM' + source: TermNode + target: TermNode + +class AcademicTermHasPreviousTerm(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'TERM_HAS_PREVIOUS_TERM' + source: TermNode + target: TermNode + +class AcademicWeekHasNextWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'WEEK_HAS_NEXT_WEEK' + source: WeekNode + target: WeekNode + +class AcademicWeekHasPreviousWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'WEEK_HAS_PREVIOUS_WEEK' + source: WeekNode + target: WeekNode + +class AcademicDayHasNextDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'DAY_HAS_NEXT_DAY' + source: DayNode + target: DayNode + +class AcademicDayHasPreviousDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'DAY_HAS_PREVIOUS_DAY' + source: DayNode + target: DayNode + +class AcademicPeriodHasNextPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'PERIOD_HAS_NEXT_PERIOD' + source: PeriodNode + target: PeriodNode + +class AcademicPeriodHasPreviousPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'PERIOD_HAS_PREVIOUS_PERIOD' + source: PeriodNode + target: PeriodNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/structures/__init__.py b/modules/database/schemas/relationships/structures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/relationships/structures/__pycache__/__init__.cpython-311.pyc b/modules/database/schemas/relationships/structures/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f49ea22 Binary files /dev/null and b/modules/database/schemas/relationships/structures/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/structures/__pycache__/schools.cpython-311.pyc b/modules/database/schemas/relationships/structures/__pycache__/schools.cpython-311.pyc new file mode 100644 index 0000000..ebe388c Binary files /dev/null and b/modules/database/schemas/relationships/structures/__pycache__/schools.cpython-311.pyc differ diff --git a/modules/database/schemas/relationships/structures/schools.py b/modules/database/schemas/relationships/structures/schools.py new file mode 100644 index 0000000..2e90f15 --- /dev/null +++ b/modules/database/schemas/relationships/structures/schools.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +ClassroomCopilot Structure Relationships +This module defines the relationships between schools and structure nodes, +as well as between entities and structure nodes. +""" + +from typing import ClassVar +from modules.database.tools.neontology.baserelationship import BaseRelationship +import modules.database.schemas.nodes.schools.schools as school_nodes +import modules.database.schemas.nodes.structures.schools as school_structures +import modules.database.schemas.nodes.workers.workers as worker_nodes + + +class SchoolHasStaffStructure(BaseRelationship): + """Relationship between a school and its staff structure node.""" + __relationshiptype__: ClassVar[str] = 'HAS_STAFF_STRUCTURE' + source: school_nodes.SchoolNode + target: school_structures.StaffStructureNode + + +class SchoolHasStudentStructure(BaseRelationship): + """Relationship between a school and its student structure node.""" + __relationshiptype__: ClassVar[str] = 'HAS_STUDENT_STRUCTURE' + source: school_nodes.SchoolNode + target: school_structures.StudentStructureNode + + +class SchoolHasITAdminStructure(BaseRelationship): + """Relationship between a school and its IT admin structure node.""" + __relationshiptype__: ClassVar[str] = 'HAS_IT_ADMIN_STRUCTURE' + source: school_nodes.SchoolNode + target: school_structures.ITAdminStructureNode + + +class EntityBelongsToStructure(BaseRelationship): + """ + Relationship between an entity (worker, student, etc.) and a structure node. + This is a generic relationship that can be used for any entity type. + """ + __relationshiptype__: ClassVar[str] = 'BELONGS_TO_STRUCTURE' + source: object # Generic source type to allow any entity + target: object # Generic target type to allow any structure + + +# Specific entity-to-structure relationships +class SuperAdminBelongsToITAdminStructure(BaseRelationship): + """Relationship between a super admin and the IT admin structure.""" + __relationshiptype__: ClassVar[str] = 'BELONGS_TO_STRUCTURE' + source: worker_nodes.SuperAdminNode + target: school_structures.ITAdminStructureNode + + +class TeacherBelongsToStaffStructure(BaseRelationship): + """Relationship between a teacher and the staff structure.""" + __relationshiptype__: ClassVar[str] = 'BELONGS_TO_STRUCTURE' + source: worker_nodes.TeacherNode + target: school_structures.StaffStructureNode + + +class StudentBelongsToStudentStructure(BaseRelationship): + """Relationship between a student and the student structure.""" + __relationshiptype__: ClassVar[str] = 'BELONGS_TO_STRUCTURE' + source: worker_nodes.StudentNode + target: school_structures.StudentStructureNode diff --git a/modules/database/schemas/relationships/timetables.py b/modules/database/schemas/relationships/timetables.py new file mode 100644 index 0000000..d6b5208 --- /dev/null +++ b/modules/database/schemas/relationships/timetables.py @@ -0,0 +1,305 @@ +import modules.database.schemas.nodes.schools.timetable as neo_timetable +from modules.database.tools.neontology.baserelationship import BaseRelationship +from typing import ClassVar + +# Timetable hierarchy structure relationships +class AcademicTimetableHasAcademicYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TIMETABLE_HAS_ACADEMIC_YEAR' + source: neo_timetable.SchoolTimetableNode + target: neo_timetable.AcademicYearNode + +class AcademicYearHasAcademicTerm(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_YEAR_HAS_ACADEMIC_TERM' + source: neo_timetable.AcademicYearNode + target: neo_timetable.AcademicTermNode + +class AcademicYearHasAcademicTermBreak(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_YEAR_HAS_ACADEMIC_TERM_BREAK' + source: neo_timetable.AcademicYearNode + target: neo_timetable.AcademicTermBreakNode + +class AcademicYearHasAcademicWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_YEAR_HAS_ACADEMIC_WEEK' + source: neo_timetable.AcademicYearNode + target: neo_timetable.AcademicWeekNode + +class AcademicYearHasHolidayWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_YEAR_HAS_HOLIDAY_WEEK' + source: neo_timetable.AcademicYearNode + target: neo_timetable.HolidayWeekNode + +class AcademicTermHasAcademicWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_HAS_ACADEMIC_WEEK' + source: neo_timetable.AcademicTermNode + target: neo_timetable.AcademicWeekNode + +class AcademicTermBreakHasHolidayWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_BREAK_HAS_HOLIDAY_WEEK' + source: neo_timetable.AcademicTermBreakNode + target: neo_timetable.HolidayWeekNode + +class AcademicTermBreakHasHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_BREAK_HAS_HOLIDAY_DAY' + source: neo_timetable.AcademicTermBreakNode + target: neo_timetable.HolidayDayNode + +class AcademicTermHasAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_HAS_ACADEMIC_DAY' + source: neo_timetable.AcademicTermNode + target: neo_timetable.AcademicDayNode + +class AcademicTermHasHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_HAS_HOLIDAY_DAY' + source: neo_timetable.AcademicTermNode + target: neo_timetable.HolidayDayNode + +class AcademicTermHasOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_HAS_OFF_TIMETABLE_DAY' + source: neo_timetable.AcademicTermNode + target: neo_timetable.OffTimetableDayNode + +class AcademicTermHasStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_HAS_STAFF_DAY' + source: neo_timetable.AcademicTermNode + target: neo_timetable.StaffDayNode + +class AcademicWeekHasAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_WEEK_HAS_ACADEMIC_DAY' + source: neo_timetable.AcademicWeekNode + target: neo_timetable.AcademicDayNode + +class AcademicWeekHasHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_WEEK_HAS_HOLIDAY_DAY' + source: neo_timetable.AcademicWeekNode + target: neo_timetable.HolidayDayNode + +class AcademicWeekHasOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_WEEK_HAS_OFF_TIMETABLE_DAY' + source: neo_timetable.AcademicWeekNode + target: neo_timetable.OffTimetableDayNode + +class AcademicWeekHasStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_WEEK_HAS_STAFF_DAY' + source: neo_timetable.AcademicWeekNode + target: neo_timetable.StaffDayNode + +class HolidayWeekHasHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_WEEK_HAS_HOLIDAY_DAY' + source: neo_timetable.HolidayWeekNode + target: neo_timetable.HolidayDayNode + +class AcademicDayHasAcademicPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_HAS_ACADEMIC_PERIOD' + source: neo_timetable.AcademicDayNode + target: neo_timetable.AcademicPeriodNode + +class AcademicDayHasRegistrationPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_HAS_REGISTRATION_PERIOD' + source: neo_timetable.AcademicDayNode + target: neo_timetable.RegistrationPeriodNode + +class AcademicDayHasBreakPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_HAS_BREAK_PERIOD' + source: neo_timetable.AcademicDayNode + target: neo_timetable.BreakPeriodNode + +class AcademicDayHasOffTimetablePeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_HAS_OFF_TIMETABLE_PERIOD' + source: neo_timetable.AcademicDayNode + target: neo_timetable.OffTimetablePeriodNode + +# Timetable sequence relationships +class AcademicYearFollowsAcademicYear(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_YEAR_FOLLOWS_ACADEMIC_YEAR' + source: neo_timetable.AcademicYearNode + target: neo_timetable.AcademicYearNode + +class AcademicTermFollowsAcademicTermBreak(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_FOLLOWS_ACADEMIC_TERM_BREAK' + source: neo_timetable.AcademicTermBreakNode # Term break ends + target: neo_timetable.AcademicTermNode # New term starts + +class AcademicTermBreakFollowsAcademicTerm(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_TERM_BREAK_FOLLOWS_ACADEMIC_TERM' + source: neo_timetable.AcademicTermNode # Term ends + target: neo_timetable.AcademicTermBreakNode # Term break starts + +class AcademicWeekFollowsAcademicWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_WEEK_FOLLOWS_ACADEMIC_WEEK' + source: neo_timetable.AcademicWeekNode + target: neo_timetable.AcademicWeekNode + +class HolidayWeekFollowsHolidayWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_WEEK_FOLLOWS_HOLIDAY_WEEK' + source: neo_timetable.HolidayWeekNode + target: neo_timetable.HolidayWeekNode + +class AcademicWeekFollowsHolidayWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_WEEK_FOLLOWS_HOLIDAY_WEEK' + source: neo_timetable.HolidayWeekNode + target: neo_timetable.AcademicWeekNode + +class HolidayWeekFollowsAcademicWeek(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_WEEK_FOLLOWS_ACADEMIC_WEEK' + source: neo_timetable.AcademicWeekNode + target: neo_timetable.HolidayWeekNode + +class AcademicDayFollowsAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_FOLLOWS_ACADEMIC_DAY' + source: neo_timetable.AcademicDayNode + target: neo_timetable.AcademicDayNode + +class AcademicDayFollowsHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_FOLLOWS_HOLIDAY_DAY' + source: neo_timetable.HolidayDayNode + target: neo_timetable.AcademicDayNode + +class AcademicDayFollowsOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_FOLLOWS_OFF_TIMETABLE_DAY' + source: neo_timetable.OffTimetableDayNode + target: neo_timetable.AcademicDayNode + +class AcademicDayFollowsStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_DAY_FOLLOWS_STAFF_DAY' + source: neo_timetable.StaffDayNode + target: neo_timetable.AcademicDayNode + +class HolidayDayFollowsHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_DAY_FOLLOWS_HOLIDAY_DAY' + source: neo_timetable.HolidayDayNode + target: neo_timetable.HolidayDayNode + +class HolidayDayFollowsAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_DAY_FOLLOWS_ACADEMIC_DAY' + source: neo_timetable.AcademicDayNode + target: neo_timetable.HolidayDayNode + +class HolidayDayFollowsOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_DAY_FOLLOWS_OFF_TIMETABLE_DAY' + source: neo_timetable.OffTimetableDayNode + target: neo_timetable.HolidayDayNode + +class HolidayDayFollowsStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'HOLIDAY_DAY_FOLLOWS_STAFF_DAY' + source: neo_timetable.StaffDayNode + target: neo_timetable.HolidayDayNode + +class OffTimetableDayFollowsOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_DAY_FOLLOWS_OFF_TIMETABLE_DAY' + source: neo_timetable.OffTimetableDayNode + target: neo_timetable.OffTimetableDayNode + +class OffTimetableDayFollowsAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_DAY_FOLLOWS_ACADEMIC_DAY' + source: neo_timetable.AcademicDayNode + target: neo_timetable.OffTimetableDayNode + +class OffTimetableDayFollowsHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_DAY_FOLLOWS_HOLIDAY_DAY' + source: neo_timetable.HolidayDayNode + target: neo_timetable.OffTimetableDayNode + +class OffTimetableDayFollowsStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_DAY_FOLLOWS_STAFF_DAY' + source: neo_timetable.StaffDayNode + target: neo_timetable.OffTimetableDayNode + +class StaffDayFollowsStaffDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'STAFF_DAY_FOLLOWS_STAFF_DAY' + source: neo_timetable.StaffDayNode + target: neo_timetable.StaffDayNode + +class StaffDayFollowsAcademicDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'STAFF_DAY_FOLLOWS_ACADEMIC_DAY' + source: neo_timetable.AcademicDayNode + target: neo_timetable.StaffDayNode + +class StaffDayFollowsHolidayDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'STAFF_DAY_FOLLOWS_HOLIDAY_DAY' + source: neo_timetable.HolidayDayNode + target: neo_timetable.StaffDayNode + +class StaffDayFollowsOffTimetableDay(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'STAFF_DAY_FOLLOWS_OFF_TIMETABLE_DAY' + source: neo_timetable.OffTimetableDayNode + target: neo_timetable.StaffDayNode + +class AcademicPeriodFollowsAcademicPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_PERIOD_FOLLOWS_ACADEMIC_PERIOD' + source: neo_timetable.AcademicPeriodNode + target: neo_timetable.AcademicPeriodNode + +class AcademicPeriodFollowsBreakPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_PERIOD_FOLLOWS_BREAK_PERIOD' + source: neo_timetable.BreakPeriodNode + target: neo_timetable.AcademicPeriodNode + +class AcademicPeriodFollowsRegistrationPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_PERIOD_FOLLOWS_REGISTRATION_PERIOD' + source: neo_timetable.RegistrationPeriodNode + target: neo_timetable.AcademicPeriodNode + +class AcademicPeriodFollowsOffTimetablePeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'ACADEMIC_PERIOD_FOLLOWS_OFF_TIMETABLE_PERIOD' + source: neo_timetable.OffTimetablePeriodNode + target: neo_timetable.AcademicPeriodNode + +class BreakPeriodFollowsAcademicPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BREAK_PERIOD_FOLLOWS_ACADEMIC_PERIOD' + source: neo_timetable.AcademicPeriodNode + target: neo_timetable.BreakPeriodNode + +class RegistrationPeriodFollowsAcademicPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'REGISTRATION_PERIOD_FOLLOWS_ACADEMIC_PERIOD' + source: neo_timetable.AcademicPeriodNode + target: neo_timetable.RegistrationPeriodNode + +class RegistrationPeriodFollowsOffTimetablePeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'REGISTRATION_PERIOD_FOLLOWS_OFF_TIMETABLE_PERIOD' + source: neo_timetable.OffTimetablePeriodNode + target: neo_timetable.RegistrationPeriodNode + +class OffTimetablePeriodFollowsAcademicPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_PERIOD_FOLLOWS_ACADEMIC_PERIOD' + source: neo_timetable.AcademicPeriodNode + target: neo_timetable.OffTimetablePeriodNode + +class OffTimetablePeriodFollowsRegistrationPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_PERIOD_FOLLOWS_REGISTRATION_PERIOD' + source: neo_timetable.RegistrationPeriodNode + target: neo_timetable.OffTimetablePeriodNode + +class OffTimetablePeriodFollowsOffTimetablePeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_PERIOD_FOLLOWS_OFF_TIMETABLE_PERIOD' + source: neo_timetable.OffTimetablePeriodNode + target: neo_timetable.OffTimetablePeriodNode + +class BreakPeriodFollowsBreakPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BREAK_PERIOD_FOLLOWS_BREAK_PERIOD' + source: neo_timetable.BreakPeriodNode + target: neo_timetable.BreakPeriodNode + +class BreakPeriodFollowsRegistrationPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BREAK_PERIOD_FOLLOWS_REGISTRATION_PERIOD' + source: neo_timetable.RegistrationPeriodNode + target: neo_timetable.BreakPeriodNode + +class BreakPeriodFollowsOffTimetablePeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'BREAK_PERIOD_FOLLOWS_OFF_TIMETABLE_PERIOD' + source: neo_timetable.OffTimetablePeriodNode + target: neo_timetable.BreakPeriodNode + +class RegistrationPeriodFollowsBreakPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'REGISTRATION_PERIOD_FOLLOWS_BREAK_PERIOD' + source: neo_timetable.BreakPeriodNode + target: neo_timetable.RegistrationPeriodNode + +class OffTimetablePeriodFollowsBreakPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'OFF_TIMETABLE_PERIOD_FOLLOWS_BREAK_PERIOD' + source: neo_timetable.BreakPeriodNode + target: neo_timetable.OffTimetablePeriodNode + +class RegistrationPeriodFollowsRegistrationPeriod(BaseRelationship): + __relationshiptype__: ClassVar[str] = 'REGISTRATION_PERIOD_FOLLOWS_REGISTRATION_PERIOD' + source: neo_timetable.RegistrationPeriodNode + target: neo_timetable.RegistrationPeriodNode \ No newline at end of file diff --git a/modules/database/schemas/relationships/worker_structures.py b/modules/database/schemas/relationships/worker_structures.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/schemas/structures.py b/modules/database/schemas/structures.py new file mode 100644 index 0000000..f9250dd --- /dev/null +++ b/modules/database/schemas/structures.py @@ -0,0 +1,27 @@ +from typing import Union +import modules.database.schemas.nodes.structures.users as user_structures +import modules.database.schemas.nodes.structures.workers as worker_structures +import modules.database.schemas.nodes.structures.schools as school_structures + +user_structures = Union[ + user_structures.UserCalendarStructureNode, +] + +worker_structures = Union[ + worker_structures.WorkerTimetableStructureNode, +] + +school_structures = Union[ + school_structures.PastoralStructureNode, + school_structures.CurriculumStructureNode, + school_structures.DepartmentStructureNode, + school_structures.SiteStructureNode, + school_structures.StaffStructureNode, + school_structures.StudentStructureNode, +] + +structures = Union[ + user_structures, + worker_structures, + school_structures, +] \ No newline at end of file diff --git a/modules/database/services/__init__.py b/modules/database/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/services/__pycache__/__init__.cpython-311.pyc b/modules/database/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..84b2c41 Binary files /dev/null and b/modules/database/services/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/services/__pycache__/admin_service.cpython-311.pyc b/modules/database/services/__pycache__/admin_service.cpython-311.pyc new file mode 100644 index 0000000..ae8c207 Binary files /dev/null and b/modules/database/services/__pycache__/admin_service.cpython-311.pyc differ diff --git a/modules/database/services/__pycache__/auth_service.cpython-311.pyc b/modules/database/services/__pycache__/auth_service.cpython-311.pyc new file mode 100644 index 0000000..cf96009 Binary files /dev/null and b/modules/database/services/__pycache__/auth_service.cpython-311.pyc differ diff --git a/modules/database/services/__pycache__/neo4j_service.cpython-311.pyc b/modules/database/services/__pycache__/neo4j_service.cpython-311.pyc new file mode 100644 index 0000000..2845710 Binary files /dev/null and b/modules/database/services/__pycache__/neo4j_service.cpython-311.pyc differ diff --git a/modules/database/services/__pycache__/school_admin_service.cpython-311.pyc b/modules/database/services/__pycache__/school_admin_service.cpython-311.pyc new file mode 100644 index 0000000..b9684bb Binary files /dev/null and b/modules/database/services/__pycache__/school_admin_service.cpython-311.pyc differ diff --git a/modules/database/services/admin_service.py b/modules/database/services/admin_service.py new file mode 100644 index 0000000..1d08d2b --- /dev/null +++ b/modules/database/services/admin_service.py @@ -0,0 +1,193 @@ +import os +from typing import Dict, List, Optional +from supabase import create_client +from modules.logger_tool import initialise_logger +from pydantic import BaseModel + + +class AdminProfileBase(BaseModel): + email: str + display_name: Optional[str] = None + admin_role: Optional[str] = "admin" + is_super_admin: Optional[bool] = False + metadata: Optional[dict] = {} + + +class AdminService: + def __init__(self): + self.logger = initialise_logger( + __name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True + ) + + # Initialize Supabase client with service role key + supabase_url = os.getenv("SUPABASE_URL") + service_role_key = os.getenv("SERVICE_ROLE_KEY") + + self.supabase = create_client(supabase_url, service_role_key) + + # Set headers for admin operations + self.supabase.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + + def get_admin_profile(self, admin_id: str) -> Optional[Dict]: + """Get admin profile by ID""" + try: + self.logger.info(f"Getting admin profile for ID: {admin_id}") + result = ( + self.supabase.table("admin_profiles") + .select("*") + .eq("id", admin_id) + .single() + .execute() + ) + return result.data if result else None + except Exception as e: + self.logger.error(f"Error getting admin profile: {str(e)}") + raise + + def list_admins(self) -> List[Dict]: + """List all admin profiles""" + try: + self.logger.info("Listing all admin profiles") + result = self.supabase.table("admin_profiles").select("*").execute() + return result.data if result else [] + except Exception as e: + self.logger.error(f"Error listing admins: {str(e)}") + raise + + def create_admin(self, admin_data: AdminProfileBase, current_admin: Dict) -> Dict: + """Create a new admin profile""" + try: + # Verify super admin status + if not current_admin.get("is_super_admin"): + raise Exception("Only super admins can create new admins") + + self.logger.info( + f"Creating new admin profile for email: {admin_data.email}" + ) + + # Create auth user first + auth_user = self.supabase.auth.admin.create_user( + { + "email": admin_data.email, + "email_confirm": True, + "user_metadata": {"is_admin": True}, + } + ) + + if not auth_user: + raise Exception("Failed to create auth user") + + # Create admin profile + profile_data = admin_data.dict() + profile_data["id"] = auth_user.id + + result = ( + self.supabase.table("admin_profiles").insert(profile_data).execute() + ) + return result.data[0] if result else None + + except Exception as e: + self.logger.error(f"Error creating admin: {str(e)}") + raise + + def update_admin( + self, admin_id: str, admin_data: AdminProfileBase, current_admin: Dict + ) -> Dict: + """Update an admin profile""" + try: + # Verify super admin status for certain operations + if admin_data.is_super_admin and not current_admin.get("is_super_admin"): + raise Exception("Only super admins can modify super admin status") + + self.logger.info(f"Updating admin profile for ID: {admin_id}") + result = ( + self.supabase.table("admin_profiles") + .update(admin_data.dict()) + .eq("id", admin_id) + .execute() + ) + return result.data[0] if result else None + + except Exception as e: + self.logger.error(f"Error updating admin: {str(e)}") + raise + + def delete_admin(self, admin_id: str, current_admin: Dict) -> None: + """Delete an admin profile""" + try: + # Verify super admin status + if not current_admin.get("is_super_admin"): + raise Exception("Only super admins can delete admins") + + # Get admin profile to check if it's a super admin + admin_profile = self.get_admin_profile(admin_id) + if admin_profile and admin_profile.get("is_super_admin"): + raise Exception("Cannot delete super admin accounts") + + self.logger.info(f"Deleting admin profile for ID: {admin_id}") + + # Delete auth user + self.supabase.auth.admin.delete_user(admin_id) + + # Delete admin profile + self.supabase.table("admin_profiles").delete().eq("id", admin_id).execute() + + except Exception as e: + self.logger.error(f"Error deleting admin: {str(e)}") + raise + + def setup_super_admin(self, admin_data: dict) -> Dict: + """Set up the initial super admin account""" + try: + self.logger.info(f"Setting up super admin for email: {admin_data['email']}") + + # Check if any super admin exists + existing_super_admin = ( + self.supabase.table("admin_profiles") + .select("*") + .eq("is_super_admin", True) + .execute() + ) + if existing_super_admin.data: + raise Exception("Super admin already exists") + + # Create the auth user first + auth_user = self.supabase.auth.admin.create_user( + { + "email": admin_data["email"], + "password": admin_data["password"], + "email_confirm": True, + "user_metadata": {"is_admin": True, "is_super_admin": True}, + } + ) + + if not auth_user: + raise Exception("Failed to create auth user") + + # Update user metadata + self.supabase.auth.admin.update_user_by_id( + auth_user.user.id, + {"user_metadata": {"is_admin": True, "is_super_admin": True}}, + ) + + # Create super admin profile + profile_data = { + "id": auth_user.user.id, + "email": admin_data["email"], + "display_name": admin_data.get("display_name", "Super Admin"), + "admin_role": "super_admin", + "is_super_admin": True, + } + + result = ( + self.supabase.table("admin_profiles").insert(profile_data).execute() + ) + return result.data[0] if result else None + + except Exception as e: + self.logger.error(f"Error setting up super admin: {str(e)}") + raise diff --git a/modules/database/services/auth_service.py b/modules/database/services/auth_service.py new file mode 100644 index 0000000..1c91284 --- /dev/null +++ b/modules/database/services/auth_service.py @@ -0,0 +1,100 @@ +import os +from typing import Dict, Optional +from fastapi import HTTPException +from supabase import create_client, Client +from modules.logger_tool import initialise_logger + +logger = initialise_logger( + __name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True +) + + +class AuthService: + def __init__(self): + """Initialize the AuthService with Supabase clients""" + self.supabase_url = os.getenv("SUPABASE_URL") + self.anon_key = os.getenv("ANON_KEY") + self.service_role_key = os.getenv("SERVICE_ROLE_KEY") + + # Create clients with different access levels + self.supabase: Client = create_client(self.supabase_url, self.anon_key) + self.admin_supabase: Client = create_client( + self.supabase_url, self.service_role_key + ) + + async def verify_admin(self, session_token: str) -> Dict: + """Verify that the user is an admin and has necessary permissions""" + try: + if not session_token: + raise HTTPException(status_code=401, detail="Not authenticated") + + # Verify session with Supabase + user = self.admin_supabase.auth.get_user(session_token) + if not user: + raise HTTPException(status_code=401, detail="Invalid session") + + # Get admin profile + admin = ( + self.admin_supabase.table("admin_profiles") + .select("*") + .eq("id", user.user.id) + .single() + .execute() + ) + if not admin.data: + raise HTTPException(status_code=403, detail="Not an admin") + + return admin.data + + except Exception as e: + logger.error(f"Error verifying admin: {str(e)}") + raise HTTPException(status_code=401, detail="Authentication failed") + + async def check_super_admin_exists(self) -> bool: + """Check if any super admin exists in the system""" + try: + result = ( + self.admin_supabase.table("admin_profiles") + .select("*") + .eq("is_super_admin", True) + .execute() + ) + return bool(result.data) + except Exception as e: + logger.error(f"Error checking super admin: {str(e)}") + return False + + async def login_admin(self, email: str, password: str) -> Dict: + """Handle admin login and return session data""" + try: + # Attempt login with Supabase + auth_response = self.supabase.auth.sign_in_with_password( + {"email": email, "password": password} + ) + + if not auth_response.user: + raise HTTPException(status_code=401, detail="Invalid credentials") + + # Verify admin status + admin = ( + self.admin_supabase.table("admin_profiles") + .select("*") + .eq("id", auth_response.user.id) + .single() + .execute() + ) + if not admin.data: + raise HTTPException(status_code=403, detail="Not authorized as admin") + + return { + "access_token": auth_response.session.access_token, + "admin": admin.data, + } + + except Exception as e: + logger.error(f"Login error: {str(e)}") + raise HTTPException(status_code=401, detail=str(e)) + + +# Create a singleton instance +auth_service = AuthService() diff --git a/modules/database/services/graph_service.py b/modules/database/services/graph_service.py new file mode 100644 index 0000000..d201e84 --- /dev/null +++ b/modules/database/services/graph_service.py @@ -0,0 +1,67 @@ +import os +from typing import Dict, Any +from modules.logger_tool import initialise_logger +import modules.database.tools.neo4j_driver_tools as driver_tools +from modules.database.admin.neontology_provider import NeontologyProvider +from modules.database.admin.graph_provider import GraphNamingProvider + +class GraphService: + def __init__(self): + self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + self.driver = driver_tools.get_driver() + self.neontology = NeontologyProvider() + self.graph_naming = GraphNamingProvider() + + def check_schema_status(self, database_name: str = "neo4j") -> Dict[str, Any]: + """Check the status of Neo4j schema including constraints, indexes, and labels""" + try: + with self.driver.session(database=database_name) as session: + # Check constraints + constraints_result = session.run("SHOW CONSTRAINTS") + constraints = list(constraints_result) + + # Check indexes + indexes_result = session.run("SHOW INDEXES") + indexes = list(indexes_result) + + # Check labels + labels_result = session.run("CALL db.labels()") + labels = list(labels_result) + + return { + "constraints_count": len(constraints), + "indexes_count": len(indexes), + "labels_count": len(labels), + "constraints": [dict(record) for record in constraints], + "indexes": [dict(record) for record in indexes], + "labels": [dict(record) for record in labels] + } + except Exception as e: + self.logger.error(f"Error checking schema status: {str(e)}") + return { + "constraints_count": 0, + "indexes_count": 0, + "labels_count": 0, + "error": str(e) + } + + def initialize_schema(self, database_name: str = "neo4j") -> Dict[str, Any]: + """Initialize Neo4j schema with required constraints and indexes""" + try: + schema_queries = self.graph_naming.get_schema_creation_queries() + + with self.driver.session(database=database_name) as session: + for query in schema_queries: + session.run(query) + + return { + "status": "success", + "message": "Schema initialized successfully", + "details": self.check_schema_status(database_name) + } + except Exception as e: + self.logger.error(f"Error initializing schema: {str(e)}") + return { + "status": "error", + "message": str(e) + } diff --git a/modules/database/services/graph_storage_service.py b/modules/database/services/graph_storage_service.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/services/jwt_service.py b/modules/database/services/jwt_service.py new file mode 100644 index 0000000..c2f8475 --- /dev/null +++ b/modules/database/services/jwt_service.py @@ -0,0 +1,45 @@ +from datetime import datetime, timedelta +import jwt +from typing import Dict, List + +class JWTService: + """JWT Service for Neo4j authentication + + TODO: Security Enhancements Needed + - Implement token refresh mechanism + - Add token revocation capability + - Add token validation checks + - Implement rate limiting + - Add audit logging for token generation/usage + - Consider reducing token expiry time and implementing refresh tokens + """ + + def __init__(self, secret_key: str, algorithm: str = "HS256"): + self.secret_key = secret_key + self.algorithm = algorithm + + def generate_neo4j_token(self, user_data: Dict) -> str: + """Generate JWT token for Neo4j database access""" + payload = { + "sub": user_data["email"], + "roles": self._get_neo4j_roles(user_data["user_type"]), + "iss": "supabase", + "aud": "neo4j", + "iat": datetime.utcnow(), + "exp": datetime.utcnow() + timedelta(hours=24) + } + + if "school_uuid" in user_data: + payload["worker_db_name"] = f"cc.institutes.{user_data['school_uuid']}" + + return jwt.encode(payload, self.secret_key, algorithm=self.algorithm) + + def _get_neo4j_roles(self, user_type: str) -> List[str]: + """Map user types to Neo4j roles""" + role_mapping = { + "cc_admin": ["admin", "reader", "writer"], + "developer": ["developer", "reader", "writer"], + "email_teacher": ["teacher", "reader", "writer"], + "email_student": ["student", "reader"] + } + return role_mapping.get(user_type, ["reader"]) diff --git a/modules/database/services/neo4j_service.py b/modules/database/services/neo4j_service.py new file mode 100644 index 0000000..f1e1f0c --- /dev/null +++ b/modules/database/services/neo4j_service.py @@ -0,0 +1,198 @@ +import os +from typing import Dict, Any +from modules.logger_tool import initialise_logger +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools + +class Neo4jService: + """Service for managing Neo4j database operations""" + + def __init__(self): + self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + self.driver = driver_tools.get_driver() + + def check_database_exists(self, database_name: str) -> Dict[str, Any]: + """Check if a Neo4j database exists + + Args: + database_name (str): Name of the database to check + + Returns: + Dict[str, Any]: Result containing existence status and operation status + """ + try: + with self.driver.session() as session: + result = session.run( + "SHOW DATABASES YIELD name WHERE name = $name", + name=database_name + ) + exists = bool(result.single()) + return { + "exists": exists, + "status": "success" + } + except Exception as e: + self.logger.error(f"Error checking database {database_name}: {str(e)}") + return { + "exists": False, + "status": "error", + "message": str(e) + } + + def create_database(self, db_name: str) -> Dict[str, Any]: + """Creates a Neo4j database with the given name + + Args: + db_name (str): Name of the database to create + + Returns: + Dict[str, Any]: Result containing operation status and message + """ + try: + # First check if database exists + exists_result = self.check_database_exists(db_name) + if exists_result["status"] == "error": + return exists_result + + if not exists_result["exists"]: + with self.driver.session() as session: + session_tools.create_database(session, db_name) + self.logger.info(f"Created database {db_name}") + return { + "status": "success", + "message": f"Database {db_name} created successfully" + } + else: + self.logger.info(f"Database {db_name} already exists") + return { + "status": "success", + "message": f"Database {db_name} already exists" + } + + except Exception as e: + self.logger.error(f"Error creating database {db_name}: {str(e)}") + return {"status": "error", "message": str(e)} + + def initialize_schema(self, database_name: str) -> Dict[str, Any]: + """Initialize Neo4j schema (constraints and indexes) for a database + + Args: + database_name (str): Name of the database to initialize schema for + + Returns: + Dict[str, Any]: Result containing operation status and message + """ + try: + with self.driver.session(database=database_name) as session: + # Create constraints + constraints = [ + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:School) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Department) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Subject) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:YearGroup) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Class) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Teacher) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Student) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Calendar) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Term) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Week) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Day) REQUIRE n.unique_id IS UNIQUE", + "CREATE CONSTRAINT IF NOT EXISTS FOR (n:Period) REQUIRE n.unique_id IS UNIQUE" + ] + + # Create indexes + indexes = [ + "CREATE INDEX IF NOT EXISTS FOR (n:School) ON (n.urn)", + "CREATE INDEX IF NOT EXISTS FOR (n:Department) ON (n.department_name)", + "CREATE INDEX IF NOT EXISTS FOR (n:Subject) ON (n.subject_name)", + "CREATE INDEX IF NOT EXISTS FOR (n:YearGroup) ON (n.year_group)", + "CREATE INDEX IF NOT EXISTS FOR (n:Class) ON (n.class_name)", + "CREATE INDEX IF NOT EXISTS FOR (n:Teacher) ON (n.email)", + "CREATE INDEX IF NOT EXISTS FOR (n:Student) ON (n.email)", + "CREATE INDEX IF NOT EXISTS FOR (n:Calendar) ON (n.calendar_name)", + "CREATE INDEX IF NOT EXISTS FOR (n:Term) ON (n.term_name)", + "CREATE INDEX IF NOT EXISTS FOR (n:Week) ON (n.week_number)", + "CREATE INDEX IF NOT EXISTS FOR (n:Day) ON (n.date)", + "CREATE INDEX IF NOT EXISTS FOR (n:Period) ON (n.period_name)" + ] + + # Execute all constraints + for constraint in constraints: + session.run(constraint) + + # Execute all indexes + for index in indexes: + session.run(index) + + self.logger.info(f"Successfully initialized schema for database {database_name}") + return { + "status": "success", + "message": f"Schema initialized successfully for database {database_name}" + } + + except Exception as e: + self.logger.error(f"Error initializing schema for database {database_name}: {str(e)}") + return { + "status": "error", + "message": str(e) + } + + def delete_database(self, db_name: str) -> Dict[str, Any]: + """Deletes a Neo4j database + + Args: + db_name (str): Name of the database to delete + + Returns: + Dict[str, Any]: Result containing operation status and message + """ + try: + exists_result = self.check_database_exists(db_name) + if exists_result["status"] == "error": + return exists_result + + if exists_result["exists"]: + with self.driver.session() as session: + session_tools.reset_database_in_session(session) + self.logger.info(f"Deleted database {db_name}") + return { + "status": "success", + "message": f"Database {db_name} deleted successfully" + } + else: + return { + "status": "success", + "message": f"Database {db_name} does not exist" + } + + except Exception as e: + self.logger.error(f"Error deleting database {db_name}: {str(e)}") + return {"status": "error", "message": str(e)} + + def check_node_exists(self, database_name: str, node_label: str) -> Dict[str, Any]: + """Check if any nodes with the given label exist in the specified database + + Args: + database_name (str): Name of the database to check + node_label (str): Label of the node type to check for + + Returns: + Dict[str, Any]: Result containing count and operation status + """ + try: + with self.driver.session(database=database_name) as session: + nodes = session_tools.find_nodes_by_label(session, node_label) + count = len(nodes) + return { + "exists": count > 0, + "count": count, + "status": "success" + } + except Exception as e: + self.logger.error(f"Error checking for {node_label} nodes in database {database_name}: {str(e)}") + return { + "exists": False, + "count": 0, + "status": "error", + "message": str(e) + } diff --git a/modules/database/services/school_admin_service.py b/modules/database/services/school_admin_service.py new file mode 100644 index 0000000..ef0e767 --- /dev/null +++ b/modules/database/services/school_admin_service.py @@ -0,0 +1,412 @@ +import os +from typing import Dict, Any, BinaryIO +import json +import pandas as pd + +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools +import modules.database.schemas.nodes.schools.schools as school_nodes +import modules.database.schemas.nodes.schools.curriculum as curriculum_nodes +import modules.database.schemas.nodes.schools.pastoral as pastoral_nodes +import modules.database.schemas.nodes.structures.schools as school_structures +from modules.database.schemas.entities import entities +from modules.database.schemas.relationships import curriculum_relationships, entity_relationships, entity_curriculum_rels +from modules.database.admin.neontology_provider import NeontologyProvider +from modules.database.admin.graph_provider import GraphNamingProvider +from modules.database.supabase.utils.client import SupabaseAnonClient +from modules.database.supabase.utils.storage import StorageManager +from modules.database.services.neo4j_service import Neo4jService +from modules.logger_tool import initialise_logger + +class SchoolAdminService: + def __init__(self): + self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + self.driver = driver_tools.get_driver() + self.neontology = NeontologyProvider() + self.graph_naming = GraphNamingProvider() + self.storage = StorageManager(SupabaseAnonClient) + self.neo4j_service = Neo4jService() + + def check_database_exists(self, database_name: str) -> Dict[str, Any]: + """Check if a Neo4j database exists""" + return self.neo4j_service.check_database_exists(database_name) + + def create_database(self, db_name: str) -> Dict: + """Creates a Neo4j database with the given name""" + return self.neo4j_service.create_database(db_name) + + def create_school_node(self, school_data: Dict) -> Dict: + """Creates a school node in cc.institutes database and stores TLDraw file in Supabase""" + try: + # Convert school data to SchoolNode + school_unique_id = self.graph_naming.get_school_unique_id(school_data['urn']) + school_path = self.graph_naming.get_school_path("cc.institutes", school_data['urn']) + + school_node = entities.SchoolNode( + unique_id=school_unique_id, + path=school_path, + urn=school_data['urn'], + establishment_number=school_data['establishment_number'], + establishment_name=school_data['establishment_name'], + establishment_type=school_data['establishment_type'], + establishment_status=school_data['establishment_status'], + phase_of_education=school_data['phase_of_education'] if school_data['phase_of_education'] not in [None, ''] else None, + statutory_low_age=int(school_data['statutory_low_age']) if school_data.get('statutory_low_age') is not None else 0, + statutory_high_age=int(school_data['statutory_high_age']) if school_data.get('statutory_high_age') is not None else 0, + religious_character=school_data.get('religious_character') if school_data.get('religious_character') not in [None, ''] else None, + school_capacity=int(school_data['school_capacity']) if school_data.get('school_capacity') is not None else 0, + school_website=school_data.get('school_website', ''), + ofsted_rating=school_data.get('ofsted_rating') if school_data.get('ofsted_rating') not in [None, ''] else None + ) + + # Create default tldraw file data + tldraw_data = { + "document": { + "version": 1, + "id": school_data['urn'], + "name": school_data['establishment_name'], + "meta": { + "created_at": "", + "updated_at": "", + "creator_id": "", + "is_template": False, + "is_snapshot": False, + "is_draft": False, + "template_id": None, + "snapshot_id": None, + "draft_id": None + } + }, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": {} + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 22 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "cc-school-node": 1 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "store": { + "document:document": { + "gridSize": 10, + "name": school_data['establishment_name'], + "meta": {}, + "id": school_data['urn'], + "typeName": "document" + }, + "page:page": { + "meta": {}, + "id": "page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + "shape:school-node": { + "x": 0, + "y": 0, + "rotation": 0, + "type": "cc-school-node", + "id": school_unique_id, + "parentId": "page", + "index": "a1", + "props": school_node.to_dict(), + "typeName": "shape" + }, + "instance:instance": { + "id": "instance", + "currentPageId": "page", + "typeName": "instance" + }, + "camera:camera": { + "x": 0, + "y": 0, + "z": 1, + "id": "camera", + "typeName": "camera" + } + } + } + + # Store tldraw file in Supabase storage + file_path = f"{school_data['urn']}/tldraw.json" + file_options = { + "content-type": "application/json", + "x-upsert": "true", + "metadata": { + "establishment_urn": school_data['urn'], + "establishment_name": school_data['establishment_name'] + } + } + + # Upload file + self.storage.upload_file( + bucket_id="cc.institutes", + file_path=file_path, + file_data=json.dumps(tldraw_data).encode(), + content_type="application/json", + upsert=True + ) + + # Create node in Neo4j + with self.neontology as neo: + self.logger.info(f"Creating school node in Neo4j: {school_node.to_dict()}") + neo.create_or_merge_node(school_node, database="cc.institutes", operation="merge") + return {"status": "success", "node": school_node} + + except Exception as e: + self.logger.error(f"Error creating school node: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_private_database(self, school_data: Dict) -> Dict: + """Creates a private database for a specific school""" + try: + private_db_name = f"cc.institutes.{school_data['urn']}" + with self.driver.session() as session: + session_tools.create_database(session, private_db_name) + self.logger.info(f"Created private database {private_db_name}") + return { + "status": "success", + "message": f"Database {private_db_name} created successfully" + } + except Exception as e: + self.logger.error(f"Error creating private database: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_basic_structure(self, school_node: school_nodes.SchoolNode, database_name: str) -> Dict: + """Creates basic structural nodes in the specified database""" + try: + # Create Department Structure node + department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}" + department_structure_node = entities.DepartmentStructureNode( + unique_id=department_structure_node_unique_id, + tldraw_snapshot="" + ) + + # Create Curriculum Structure node + curriculum_node = curriculum_nodes.CurriculumStructureNode( + unique_id=f"CurriculumStructure_{school_node.unique_id}", + tldraw_snapshot="" + ) + + # Create Pastoral Structure node + pastoral_node = school_structures.PastoralStructureNode( + unique_id=f"PastoralStructure_{school_node.unique_id}", + tldraw_snapshot="" + ) + + with self.neontology as neo: + # Create nodes + neo.create_or_merge_node(department_structure_node, database=str(database_name), operation='merge') + neo.create_or_merge_node(curriculum_node, database=str(database_name), operation='merge') + neo.create_or_merge_node(pastoral_node, database=str(database_name), operation='merge') + + # Create relationships + neo.create_or_merge_relationship( + entity_relationships.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node), + database=database_name, operation='merge' + ) + neo.create_or_merge_relationship( + entity_curriculum_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node), + database=database_name, operation='merge' + ) + neo.create_or_merge_relationship( + entity_curriculum_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node), + database=database_name, operation='merge' + ) + + return { + "status": "success", + "message": "Basic structure created successfully", + "nodes": { + "department_structure": department_structure_node, + "curriculum_structure": curriculum_node, + "pastoral_structure": pastoral_node + } + } + + except Exception as e: + self.logger.error(f"Error creating basic structure: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_detailed_structure(self, school_node: school_nodes.SchoolNode, database_name: str, excel_file: BinaryIO) -> Dict: + """Creates detailed structural nodes from Excel file""" + try: + # Store Excel file in Supabase + file_path = f"{school_node.urn}/structure.xlsx" + + # Upload Excel file + self.storage.upload_file( + bucket_id="cc.institutes", + file_path=file_path, + file_data=excel_file.read(), + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + upsert=True + ) + + # Process Excel file + dataframes = pd.read_excel(excel_file, sheet_name=None) + + # Get existing basic structure nodes + with self.neontology as neo: + result = neo.cypher_read(""" + MATCH (s:School {unique_id: $school_id}) + OPTIONAL MATCH (s)-[:HAS_DEPARTMENT_STRUCTURE]->(ds:DepartmentStructure) + OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(cs:CurriculumStructure) + OPTIONAL MATCH (s)-[:HAS_PASTORAL_STRUCTURE]->(ps:PastoralStructure) + RETURN ds, cs, ps + """, {"school_id": school_node.unique_id}, database=database_name) + + if not result: + raise Exception("Basic structure not found") + + department_structure = result['ds'] + curriculum_structure = result['cs'] + pastoral_structure = result['ps'] + + # Create departments and subjects + unique_departments = dataframes['keystagesyllabuses']['Department'].dropna().unique() + + node_library = {} + + with self.neontology as neo: + for department_name in unique_departments: + + department_node = entities.DepartmentNode( + unique_id=f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}", + department_name=department_name, + tldraw_snapshot="" + ) + neo.create_or_merge_node(department_node, database=database_name, operation='merge') + node_library[f'department_{department_name}'] = department_node + + # Link to department structure + neo.create_or_merge_relationship( + entity_relationships.DepartmentStructureHasDepartment( + source=department_structure, + target=department_node + ), + database=database_name, + operation='merge' + ) + + # Create year groups + year_groups = self.sort_year_groups(dataframes['yeargroupsyllabuses'])['YearGroup'].unique() + last_year_group_node = None + + for year_group in year_groups: + numeric_year_group = pd.to_numeric(year_group, errors='coerce') + if pd.notna(numeric_year_group): + year_group_node = pastoral_nodes.YearGroupNode( + unique_id=f"YearGroup_{school_node.unique_id}_YGrp{int(numeric_year_group)}", + year_group=str(int(numeric_year_group)), + year_group_name=f"Year {int(numeric_year_group)}", + tldraw_snapshot="" + ) + neo.create_or_merge_node(year_group_node, database=database_name, operation='merge') + node_library[f'year_group_{int(numeric_year_group)}'] = year_group_node + + # Create sequential relationship + if last_year_group_node: + neo.create_or_merge_relationship( + curriculum_relationships.YearGroupFollowsYearGroup( + source=last_year_group_node, + target=year_group_node + ), + database=database_name, + operation='merge' + ) + last_year_group_node = year_group_node + + # Link to pastoral structure + neo.create_or_merge_relationship( + curriculum_relationships.PastoralStructureIncludesYearGroup( + source=pastoral_structure, + target=year_group_node + ), + database=database_name, + operation='merge' + ) + + # Create key stages + key_stages = dataframes['keystagesyllabuses']['KeyStage'].unique() + last_key_stage_node = None + + for key_stage in sorted(key_stages): + key_stage_node = curriculum_nodes.KeyStageNode( + unique_id=f"KeyStage_{curriculum_structure.unique_id}_KStg{key_stage}", + key_stage_name=f"Key Stage {key_stage}", + key_stage=str(key_stage), + tldraw_snapshot="" + ) + neo.create_or_merge_node(key_stage_node, database=database_name, operation='merge') + node_library[f'key_stage_{key_stage}'] = key_stage_node + + # Create sequential relationship + if last_key_stage_node: + neo.create_or_merge_relationship( + curriculum_relationships.KeyStageFollowsKeyStage( + source=last_key_stage_node, + target=key_stage_node + ), + database=database_name, + operation='merge' + ) + last_key_stage_node = key_stage_node + + # Link to curriculum structure + neo.create_or_merge_relationship( + curriculum_relationships.CurriculumStructureIncludesKeyStage( + source=curriculum_structure, + target=key_stage_node + ), + database=database_name, + operation='merge' + ) + + return { + "status": "success", + "message": "Detailed structure created successfully", + "node_library": node_library + } + + except Exception as e: + self.logger.error(f"Error creating detailed structure: {str(e)}") + return {"status": "error", "message": str(e)} + + def sort_year_groups(self, df: pd.DataFrame) -> pd.DataFrame: + """Helper function to sort year groups numerically""" + df = df.copy() + df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce') + return df.sort_values(by='YearGroupNumeric') + + diff --git a/modules/database/services/school_service.py b/modules/database/services/school_service.py new file mode 100644 index 0000000..44d1fdf --- /dev/null +++ b/modules/database/services/school_service.py @@ -0,0 +1,472 @@ +import os +from typing import Dict, List, Optional, BinaryIO +import json +import pandas as pd +from backend.modules.database.schemas import entities +from modules.logger_tool import initialise_logger +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools +from modules.database.admin.neontology_provider import NeontologyProvider +from modules.database.admin.graph_provider import GraphNamingProvider +from modules.database.schemas import curriculum_neo +from modules.database.schemas.relationships import curriculum_relationships, entity_relationships, entity_curriculum_rels +from modules.database.supabase.utils.storage import StorageManager + +class SchoolService: + def __init__(self): + self.logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + self.driver = driver_tools.get_driver() + self.neontology = NeontologyProvider() + self.graph_naming = GraphNamingProvider() + self.storage = StorageManager() + + def create_schools_database(self) -> Dict: + """Creates the main cc.institutes database in Neo4j""" + try: + db_name = "cc.institutes" + with self.driver.session() as session: + session_tools.create_database(session, db_name) + self.logger.info(f"Created database {db_name}") + return { + "status": "success", + "message": f"Database {db_name} created successfully" + } + except Exception as e: + self.logger.error(f"Error creating schools database: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_school_node(self, school_data: Dict) -> Dict: + """Creates a school node in cc.institutes database and stores TLDraw file in Supabase""" + try: + # Convert school data to SchoolNode + school_unique_id = self.graph_naming.get_school_unique_id(school_data['urn']) + school_path = self.graph_naming.get_school_path("cc.institutes", school_data['urn']) + + school_node = entities.SchoolNode( + unique_id=school_unique_id, + path=school_path, + urn=school_data['urn'], + establishment_number=school_data['establishment_number'], + establishment_name=school_data['establishment_name'], + establishment_type=school_data['establishment_type'], + establishment_status=school_data['establishment_status'], + phase_of_education=school_data['phase_of_education'] if school_data['phase_of_education'] not in [None, ''] else None, + statutory_low_age=int(school_data['statutory_low_age']) if school_data.get('statutory_low_age') is not None else 0, + statutory_high_age=int(school_data['statutory_high_age']) if school_data.get('statutory_high_age') is not None else 0, + religious_character=school_data.get('religious_character') if school_data.get('religious_character') not in [None, ''] else None, + school_capacity=int(school_data['school_capacity']) if school_data.get('school_capacity') is not None else 0, + school_website=school_data.get('school_website', ''), + ofsted_rating=school_data.get('ofsted_rating') if school_data.get('ofsted_rating') not in [None, ''] else None + ) + + # Create default tldraw file data + tldraw_data = { + "document": { + "version": 1, + "id": school_data['urn'], + "name": school_data['establishment_name'], + "meta": { + "created_at": "", + "updated_at": "", + "creator_id": "", + "is_template": False, + "is_snapshot": False, + "is_draft": False, + "template_id": None, + "snapshot_id": None, + "draft_id": None + } + }, + "schema": { + "schemaVersion": 1, + "storeVersion": 4, + "recordVersions": { + "asset": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": {} + }, + "camera": { + "version": 1 + }, + "document": { + "version": 2 + }, + "instance": { + "version": 22 + }, + "instance_page_state": { + "version": 5 + }, + "page": { + "version": 1 + }, + "shape": { + "version": 3, + "subTypeKey": "type", + "subTypeVersions": { + "cc-school-node": 1 + } + }, + "instance_presence": { + "version": 5 + }, + "pointer": { + "version": 1 + } + } + }, + "store": { + "document:document": { + "gridSize": 10, + "name": school_data['establishment_name'], + "meta": {}, + "id": school_data['urn'], + "typeName": "document" + }, + "page:page": { + "meta": {}, + "id": "page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + "shape:school-node": { + "x": 0, + "y": 0, + "rotation": 0, + "type": "cc-school-node", + "id": school_unique_id, + "parentId": "page", + "index": "a1", + "props": school_node.to_dict(), + "typeName": "shape" + }, + "instance:instance": { + "id": "instance", + "currentPageId": "page", + "typeName": "instance" + }, + "camera:camera": { + "x": 0, + "y": 0, + "z": 1, + "id": "camera", + "typeName": "camera" + } + } + } + + # Store tldraw file in Supabase storage + file_path = f"{school_data['urn']}/tldraw.json" + file_options = { + "content-type": "application/json", + "x-upsert": "true", + "metadata": { + "establishment_urn": school_data['urn'], + "establishment_name": school_data['establishment_name'] + } + } + + # Upload file + self.storage.upload_file( + bucket_id="cc.institutes", + file_path=file_path, + file_data=json.dumps(tldraw_data).encode(), + content_type="application/json", + upsert=True + ) + + # Create node in Neo4j + with self.neontology as neo: + self.logger.info(f"Creating school node in Neo4j: {school_node.to_dict()}") + neo.create_or_merge_node(school_node, database="cc.institutes", operation="merge") + return {"status": "success", "node": school_node} + + except Exception as e: + self.logger.error(f"Error creating school node: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_private_database(self, school_data: Dict) -> Dict: + """Creates a private database for a specific school""" + try: + private_db_name = f"cc.institutes.{school_data['urn']}" + with self.driver.session() as session: + session_tools.create_database(session, private_db_name) + self.logger.info(f"Created private database {private_db_name}") + return { + "status": "success", + "message": f"Database {private_db_name} created successfully" + } + except Exception as e: + self.logger.error(f"Error creating private database: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_basic_structure(self, school_node: entities.SchoolNode, database_name: str) -> Dict: + """Creates basic structural nodes in the specified database""" + try: + # Create filesystem paths + fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school") + + # Create Department Structure node + department_structure_node_unique_id = f"DepartmentStructure_{school_node.unique_id}" + _, department_path = fs_handler.create_school_department_directory(school_node.path, "departments") + department_structure_node = entities.DepartmentStructureNode( + unique_id=department_structure_node_unique_id, + path=department_path + ) + + # Create Curriculum Structure node + _, curriculum_path = fs_handler.create_school_curriculum_directory(school_node.path) + curriculum_node = curriculum_neo.CurriculumStructureNode( + unique_id=f"CurriculumStructure_{school_node.unique_id}", + path=curriculum_path + ) + + # Create Pastoral Structure node + _, pastoral_path = fs_handler.create_school_pastoral_directory(school_node.path) + pastoral_node = curriculum_neo.PastoralStructureNode( + unique_id=f"PastoralStructure_{school_node.unique_id}", + path=pastoral_path + ) + + with self.neontology as neo: + # Create nodes + neo.create_or_merge_node(department_structure_node, database=str(database_name), operation='merge') + fs_handler.create_default_tldraw_file(department_structure_node.path, department_structure_node.to_dict()) + + neo.create_or_merge_node(curriculum_node, database=str(database_name), operation='merge') + fs_handler.create_default_tldraw_file(curriculum_node.path, curriculum_node.to_dict()) + + neo.create_or_merge_node(pastoral_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(pastoral_node.path, pastoral_node.to_dict()) + + # Create relationships + neo.create_or_merge_relationship( + entity_relationships.SchoolHasDepartmentStructure(source=school_node, target=department_structure_node), + database=database_name, operation='merge' + ) + + neo.create_or_merge_relationship( + entity_curriculum_rels.SchoolHasCurriculumStructure(source=school_node, target=curriculum_node), + database=database_name, operation='merge' + ) + + neo.create_or_merge_relationship( + entity_curriculum_rels.SchoolHasPastoralStructure(source=school_node, target=pastoral_node), + database=database_name, operation='merge' + ) + + return { + "status": "success", + "message": "Basic structure created successfully", + "nodes": { + "department_structure": department_structure_node, + "curriculum_structure": curriculum_node, + "pastoral_structure": pastoral_node + } + } + + except Exception as e: + self.logger.error(f"Error creating basic structure: {str(e)}") + return {"status": "error", "message": str(e)} + + def create_detailed_structure(self, school_node: entities.SchoolNode, database_name: str, excel_file: BinaryIO) -> Dict: + """Creates detailed structural nodes from Excel file""" + try: + # Store Excel file in Supabase + file_path = f"{school_node.urn}/structure.xlsx" + + # Upload Excel file + self.storage.upload_file( + bucket_id="cc.institutes", + file_path=file_path, + file_data=excel_file.read(), + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + upsert=True + ) + + # Process Excel file + dataframes = pd.read_excel(excel_file, sheet_name=None) + + # Get existing basic structure nodes + with self.neontology as neo: + result = neo.cypher_read(""" + MATCH (s:School {unique_id: $school_id}) + OPTIONAL MATCH (s)-[:HAS_DEPARTMENT_STRUCTURE]->(ds:DepartmentStructure) + OPTIONAL MATCH (s)-[:HAS_CURRICULUM_STRUCTURE]->(cs:CurriculumStructure) + OPTIONAL MATCH (s)-[:HAS_PASTORAL_STRUCTURE]->(ps:PastoralStructure) + RETURN ds, cs, ps + """, {"school_id": school_node.unique_id}, database=database_name) + + if not result: + raise Exception("Basic structure not found") + + department_structure = result['ds'] + curriculum_structure = result['cs'] + pastoral_structure = result['ps'] + + # Create departments and subjects + unique_departments = dataframes['keystagesyllabuses']['Department'].dropna().unique() + + fs_handler = ClassroomCopilotFilesystem(database_name, init_run_type="school") + node_library = {} + + with self.neontology as neo: + for department_name in unique_departments: + _, department_path = fs_handler.create_school_department_directory(school_node.path, department_name) + + department_node = entities.DepartmentNode( + unique_id=f"Department_{school_node.unique_id}_{department_name.replace(' ', '_')}", + department_name=department_name, + path=department_path + ) + neo.create_or_merge_node(department_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(department_node.path, department_node.to_dict()) + node_library[f'department_{department_name}'] = department_node + + # Link to department structure + neo.create_or_merge_relationship( + entity_relationships.DepartmentStructureHasDepartment( + source=department_structure, + target=department_node + ), + database=database_name, + operation='merge' + ) + + # Create year groups + year_groups = self.sort_year_groups(dataframes['yeargroupsyllabuses'])['YearGroup'].unique() + last_year_group_node = None + + for year_group in year_groups: + numeric_year_group = pd.to_numeric(year_group, errors='coerce') + if pd.notna(numeric_year_group): + _, year_group_path = fs_handler.create_pastoral_year_group_directory( + pastoral_structure.path, + str(int(numeric_year_group)) + ) + + year_group_node = curriculum_neo.YearGroupNode( + unique_id=f"YearGroup_{school_node.unique_id}_YGrp{int(numeric_year_group)}", + year_group=str(int(numeric_year_group)), + year_group_name=f"Year {int(numeric_year_group)}", + path=year_group_path + ) + neo.create_or_merge_node(year_group_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(year_group_node.path, year_group_node.to_dict()) + node_library[f'year_group_{int(numeric_year_group)}'] = year_group_node + + # Create sequential relationship + if last_year_group_node: + neo.create_or_merge_relationship( + curriculum_relationships.YearGroupFollowsYearGroup( + source=last_year_group_node, + target=year_group_node + ), + database=database_name, + operation='merge' + ) + last_year_group_node = year_group_node + + # Link to pastoral structure + neo.create_or_merge_relationship( + curriculum_relationships.PastoralStructureIncludesYearGroup( + source=pastoral_structure, + target=year_group_node + ), + database=database_name, + operation='merge' + ) + + # Create key stages + key_stages = dataframes['keystagesyllabuses']['KeyStage'].unique() + last_key_stage_node = None + + for key_stage in sorted(key_stages): + _, key_stage_path = fs_handler.create_curriculum_key_stage_directory( + curriculum_structure.path, + str(key_stage) + ) + + key_stage_node = curriculum_neo.KeyStageNode( + unique_id=f"KeyStage_{curriculum_structure.unique_id}_KStg{key_stage}", + key_stage_name=f"Key Stage {key_stage}", + key_stage=str(key_stage), + path=key_stage_path + ) + neo.create_or_merge_node(key_stage_node, database=database_name, operation='merge') + fs_handler.create_default_tldraw_file(key_stage_node.path, key_stage_node.to_dict()) + node_library[f'key_stage_{key_stage}'] = key_stage_node + + # Create sequential relationship + if last_key_stage_node: + neo.create_or_merge_relationship( + curriculum_relationships.KeyStageFollowsKeyStage( + source=last_key_stage_node, + target=key_stage_node + ), + database=database_name, + operation='merge' + ) + last_key_stage_node = key_stage_node + + # Link to curriculum structure + neo.create_or_merge_relationship( + curriculum_relationships.CurriculumStructureIncludesKeyStage( + source=curriculum_structure, + target=key_stage_node + ), + database=database_name, + operation='merge' + ) + + return { + "status": "success", + "message": "Detailed structure created successfully", + "node_library": node_library + } + + except Exception as e: + self.logger.error(f"Error creating detailed structure: {str(e)}") + return {"status": "error", "message": str(e)} + + def sort_year_groups(self, df: pd.DataFrame) -> pd.DataFrame: + """Helper function to sort year groups numerically""" + df = df.copy() + df['YearGroupNumeric'] = pd.to_numeric(df['YearGroup'], errors='coerce') + return df.sort_values(by='YearGroupNumeric') + + def check_schools_database(self) -> Dict: + """Check if the schools database exists and has been initialized""" + try: + db_name = "cc.institutes" + with self.driver.session() as session: + # Check if database exists + databases = session_tools.list_databases(session) + if db_name not in databases: + return { + "status": "error", + "message": f"Database {db_name} does not exist" + } + + # Check if database has any nodes (indicating it's been initialized) + session.run("USE " + db_name) + result = session.run("MATCH (n) RETURN count(n) as count").single() + node_count = result["count"] if result else 0 + + if node_count == 0: + return { + "status": "error", + "message": f"Database {db_name} exists but has no nodes" + } + + return { + "status": "success", + "message": f"Database {db_name} exists and has {node_count} nodes" + } + + except Exception as e: + self.logger.error(f"Error checking schools database: {str(e)}") + return {"status": "error", "message": str(e)} diff --git a/modules/database/supabase/__init__.py b/modules/database/supabase/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/supabase/__pycache__/__init__.cpython-311.pyc b/modules/database/supabase/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..85ba1c5 Binary files /dev/null and b/modules/database/supabase/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/supabase/utils/__init__.py b/modules/database/supabase/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/supabase/utils/__pycache__/__init__.cpython-311.pyc b/modules/database/supabase/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c7933a0 Binary files /dev/null and b/modules/database/supabase/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/supabase/utils/__pycache__/client.cpython-311.pyc b/modules/database/supabase/utils/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000..deb3c8d Binary files /dev/null and b/modules/database/supabase/utils/__pycache__/client.cpython-311.pyc differ diff --git a/modules/database/supabase/utils/__pycache__/storage.cpython-311.pyc b/modules/database/supabase/utils/__pycache__/storage.cpython-311.pyc new file mode 100644 index 0000000..27fc1ad Binary files /dev/null and b/modules/database/supabase/utils/__pycache__/storage.cpython-311.pyc differ diff --git a/modules/database/supabase/utils/client.py b/modules/database/supabase/utils/client.py new file mode 100644 index 0000000..02c78bf --- /dev/null +++ b/modules/database/supabase/utils/client.py @@ -0,0 +1,77 @@ +import os +from typing import Dict, Optional, Any, TypedDict, List +from supabase import create_client, Client +from supabase.lib.client_options import SyncClientOptions +from gotrue import SyncMemoryStorage +from modules.logger_tool import initialise_logger + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +class CreateBucketOptions(TypedDict, total=False): + """Options for bucket creation, matching Supabase API requirements""" + public: bool + file_size_limit: int + allowed_mime_types: List[str] + name: str + +def _create_base_client(url: str, key: str, options: Optional[Dict[str, Any]] = None, access_token: Optional[str] = None) -> Client: + """Create a base Supabase client with given configuration.""" + client_options = SyncClientOptions( + schema="public", + storage=SyncMemoryStorage(), + headers={ + "apikey": key, + "Authorization": f"Bearer {access_token if access_token else key}" + } + ) + return create_client(url, key, options=client_options) + +class SupabaseServiceRoleClient: + """Supabase client for making authenticated requests using the service role key""" + + def __init__(self, url: Optional[str] = None, service_role_key: Optional[str] = None, access_token: Optional[str] = None): + """Initialize the Supabase client with URL and service role key""" + self.url = url or os.environ.get("SUPABASE_URL", "http://kong:8000") + self.service_role_key = service_role_key or os.environ.get("SERVICE_ROLE_KEY") + + if not self.url or not self.service_role_key: + raise ValueError("SUPABASE_URL and SERVICE_ROLE_KEY must be provided") + + # Initialize Supabase client with service role key and optional access token + self.supabase = _create_base_client(self.url, self.service_role_key, access_token=access_token) + + def create_bucket(self, id: str, options: Optional[CreateBucketOptions] = None) -> Dict[str, Any]: + """Create a storage bucket with the given ID and options""" + if options is None: + options = CreateBucketOptions() + if 'name' not in options: + options['name'] = id # Use ID as default name if not provided + return self.supabase.storage.create_bucket(id, options=options) + + @classmethod + def for_admin(cls, access_token: str) -> 'SupabaseServiceRoleClient': + """Create a client instance for the super admin using their access token""" + return cls(access_token=access_token) + +class SupabaseAnonClient: + """Supabase client for making authenticated requests using the anon key""" + + def __init__(self, url: Optional[str] = None, anon_key: Optional[str] = None, access_token: Optional[str] = None): + """Initialize the Supabase client with URL and anon key""" + self.url = url or os.environ.get("SUPABASE_URL", "http://kong:8000") + self.anon_key = anon_key or os.environ.get("ANON_KEY") + + if not self.url or not self.anon_key: + raise ValueError("SUPABASE_URL and ANON_KEY must be provided") + + # Initialize Supabase client with anon key and optional access token + self.supabase = _create_base_client(self.url, self.anon_key, access_token=access_token) + + def create_bucket(self, id: str, options: Optional[CreateBucketOptions] = None) -> Dict[str, Any]: + """Create a storage bucket with the given ID and options""" + return self.supabase.storage.create_bucket(id, options=options) + + @classmethod + def for_user(cls, access_token: str) -> 'SupabaseAnonClient': + """Create a client instance for a specific user using their access token""" + return cls(access_token=access_token) diff --git a/modules/database/supabase/utils/storage.py b/modules/database/supabase/utils/storage.py new file mode 100644 index 0000000..9d7ab19 --- /dev/null +++ b/modules/database/supabase/utils/storage.py @@ -0,0 +1,289 @@ +import os +from typing import Dict, List, Optional, Any, TypedDict +from .client import SupabaseServiceRoleClient, SupabaseAnonClient +from modules.logger_tool import initialise_logger + +class CreateBucketOptions(TypedDict, total=False): + """Options for bucket creation, matching Supabase API requirements""" + public: bool + file_size_limit: int + allowed_mime_types: List[str] + +class StorageError(Exception): + """Custom exception for storage-related errors""" + pass + +class StorageManager: + """Base storage manager class with common functionality""" + + def __init__(self, client: SupabaseServiceRoleClient | SupabaseAnonClient): + self.client = client + self.logger = initialise_logger(__name__) + + def check_bucket_exists(self, bucket_id: str) -> bool: + """Check if a storage bucket exists""" + try: + self.logger.info(f"Checking if bucket {bucket_id} exists") + buckets = self.client.supabase.storage.list_buckets() + return any(bucket.name == bucket_id for bucket in buckets) + except Exception as e: + self.logger.error(f"Error checking bucket {bucket_id}: {str(e)}") + return False + + def list_bucket_contents(self, bucket_id: str, path: str = "") -> Dict: + """List contents of a bucket at specified path""" + try: + self.logger.info(f"Listing contents of bucket {bucket_id} at path {path}") + contents = self.client.supabase.storage.from_(bucket_id).list(path) + return { + "folders": [item for item in contents if item.get("id", "").endswith("/")], + "files": [item for item in contents if not item.get("id", "").endswith("/")] + } + except Exception as e: + self.logger.error(f"Error listing bucket contents: {str(e)}") + raise StorageError(str(e)) + + def upload_file(self, bucket_id: str, file_path: str, file_data: bytes, content_type: str, upsert: bool = True) -> Any: + """Upload a file to a storage bucket""" + try: + self.logger.info(f"Uploading file to {bucket_id} at path {file_path}") + return self.client.supabase.storage.from_(bucket_id).upload( + path=file_path, + file=file_data, + file_options={ + "content-type": content_type, + "x-upsert": "true" if upsert else "false" + } + ) + except Exception as e: + self.logger.error(f"Error uploading file: {str(e)}") + raise StorageError(str(e)) + + def download_file(self, bucket_id: str, file_path: str) -> bytes: + """Download a file from a storage bucket""" + try: + self.logger.info(f"Downloading file from {bucket_id} at path {file_path}") + return self.client.supabase.storage.from_(bucket_id).download(file_path) + except Exception as e: + self.logger.error(f"Error downloading file: {str(e)}") + raise StorageError(str(e)) + + def delete_file(self, bucket_id: str, file_path: str) -> None: + """Delete a file from a storage bucket""" + try: + self.logger.info(f"Deleting file from {bucket_id} at path {file_path}") + self.client.supabase.storage.from_(bucket_id).remove([file_path]) + except Exception as e: + self.logger.error(f"Error deleting file: {str(e)}") + raise StorageError(str(e)) + + def get_public_url(self, bucket_id: str, file_path: str) -> str: + """Get public URL for a file""" + try: + self.logger.info(f"Getting public URL for file in {bucket_id} at path {file_path}") + return self.client.supabase.storage.from_(bucket_id).get_public_url(file_path) + except Exception as e: + self.logger.error(f"Error getting public URL: {str(e)}") + raise StorageError(str(e)) + + def create_signed_url(self, bucket_id: str, file_path: str, expires_in: int = 3600) -> Any: + """Create a signed URL for temporary file access""" + try: + self.logger.info(f"Creating signed URL for file in {bucket_id} at path {file_path}") + return self.client.supabase.storage.from_(bucket_id).create_signed_url(file_path, expires_in) + except Exception as e: + self.logger.error(f"Error creating signed URL: {str(e)}") + raise StorageError(str(e)) + +class StorageAdmin(StorageManager): + """Storage admin class for managing storage buckets with service role access.""" + + def __init__(self, admin_user_id: Optional[str] = None): + """Initialize StorageAdmin with service role client.""" + super().__init__(SupabaseServiceRoleClient()) + self.admin_user_id = admin_user_id + + def create_bucket( + self, + id: str, + name: Optional[str] = None, + public: bool = False, + file_size_limit: Optional[int] = None, + allowed_mime_types: Optional[List[str]] = None, + owner: Optional[str] = None, # Kept for backwards compatibility but not used + owner_id: Optional[str] = None # Kept for backwards compatibility but not used + ) -> Dict[str, Any]: + """Create a new storage bucket with supported parameters.""" + try: + self.logger.info(f"Creating bucket {id} with name {name}") + + # Prepare bucket options with only supported parameters + options: Optional[CreateBucketOptions] = {} + if public: + options["public"] = public + if file_size_limit is not None: + options["file_size_limit"] = file_size_limit + if allowed_mime_types is not None: + options["allowed_mime_types"] = allowed_mime_types + + # Create bucket with supported parameters only + bucket = self.client.supabase.storage.create_bucket( + str(id), + options=options if options else None + ) + + return bucket + + except Exception as e: + self.logger.error(f"Error creating bucket {id}: {str(e)}") + raise StorageError(str(e)) + + def initialize_core_buckets(self, admin_user_id: Optional[str] = None) -> List[Dict[str, Any]]: + """Initialize core storage buckets for the application.""" + try: + owner_id = admin_user_id or self.admin_user_id + if not owner_id: + raise ValueError("Admin user ID is required for bucket initialization") + + core_buckets = [ + { + "id": "cc.users", + "name": "CC Users", + "public": False, + "owner": owner_id, + "owner_id": "superadmin", + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + }, + { + "id": "cc.institutes", + "name": "CC Institutes", + "public": False, + "owner": owner_id, + "owner_id": "superadmin", + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + } + ] + + results = [] + for bucket in core_buckets: + try: + bucket_name = bucket.pop("name") # Remove name from options + result = self.create_bucket(name=bucket_name, **bucket) + results.append({ + "bucket": bucket["id"], + "status": "success", + "result": result + }) + except Exception as e: + self.logger.error(f"Error creating bucket {bucket['id']}: {str(e)}") + results.append({ + "bucket": bucket["id"], + "status": "error", + "error": str(e) + }) + + return results + + except Exception as e: + self.logger.error(f"Error initializing core buckets: {str(e)}") + raise StorageError(str(e)) + + def create_user_bucket(self, user_id: str, username: str) -> Dict[str, Any]: + """Create a storage bucket for a specific user.""" + try: + bucket_id = f"cc.users.admin.{username}" + bucket_name = f"User Files - {username}" + + return self.create_bucket( + id=bucket_id, + name=bucket_name, + public=False, + owner=user_id, + owner_id=username, + file_size_limit=50 * 1024 * 1024, # 50MB + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + + except Exception as e: + self.logger.error(f"Error creating user bucket for {username}: {str(e)}") + raise StorageError(str(e)) + + def create_school_buckets(self, school_id: str, school_name: str, admin_user_id: Optional[str] = None) -> Dict[str, Any]: + """Create storage buckets for a school.""" + try: + owner_id = admin_user_id or self.admin_user_id + if not owner_id: + raise ValueError("Admin user ID is required for school bucket creation") + + school_buckets = [ + { + "id": f"cc.institutes.{school_id}.public", + "name": f"{school_name} - Public Files", + "public": True, + "owner": owner_id, + "owner_id": school_id, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + }, + { + "id": f"cc.institutes.{school_id}.private", + "name": f"{school_name} - Private Files", + "public": False, + "owner": owner_id, + "owner_id": school_id, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + } + ] + + results = {} + for bucket in school_buckets: + try: + bucket_name = bucket.pop("name") # Remove name from options + result = self.create_bucket(name=bucket_name, **bucket) + results[bucket["id"]] = { + "status": "success", + "result": result + } + except Exception as e: + self.logger.error(f"Error creating school bucket {bucket['id']}: {str(e)}") + results[bucket["id"]] = { + "status": "error", + "error": str(e) + } + + return results + + except Exception as e: + self.logger.error(f"Error creating school buckets: {str(e)}") + raise StorageError(str(e)) + +class StorageUser(StorageManager): + """Storage user class for managing storage buckets with user role access.""" + + def __init__(self, user_id: Optional[str] = None): + """Initialize StorageUser with user role client.""" + super().__init__(SupabaseAnonClient()) + self.user_id = user_id \ No newline at end of file diff --git a/modules/database/tools/__init__.py b/modules/database/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/tools/__pycache__/__init__.cpython-311.pyc b/modules/database/tools/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e160e70 Binary files /dev/null and b/modules/database/tools/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/tools/__pycache__/filesystem_tools.cpython-311.pyc b/modules/database/tools/__pycache__/filesystem_tools.cpython-311.pyc new file mode 100644 index 0000000..15bfbe7 Binary files /dev/null and b/modules/database/tools/__pycache__/filesystem_tools.cpython-311.pyc differ diff --git a/modules/database/tools/__pycache__/neo4j_db_formatter.cpython-311.pyc b/modules/database/tools/__pycache__/neo4j_db_formatter.cpython-311.pyc new file mode 100644 index 0000000..835b90b Binary files /dev/null and b/modules/database/tools/__pycache__/neo4j_db_formatter.cpython-311.pyc differ diff --git a/modules/database/tools/__pycache__/neo4j_driver_tools.cpython-311.pyc b/modules/database/tools/__pycache__/neo4j_driver_tools.cpython-311.pyc new file mode 100644 index 0000000..3e162bd Binary files /dev/null and b/modules/database/tools/__pycache__/neo4j_driver_tools.cpython-311.pyc differ diff --git a/modules/database/tools/__pycache__/neo4j_session_tools.cpython-311.pyc b/modules/database/tools/__pycache__/neo4j_session_tools.cpython-311.pyc new file mode 100644 index 0000000..067f4af Binary files /dev/null and b/modules/database/tools/__pycache__/neo4j_session_tools.cpython-311.pyc differ diff --git a/modules/database/tools/__pycache__/neontology_tools.cpython-311.pyc b/modules/database/tools/__pycache__/neontology_tools.cpython-311.pyc new file mode 100644 index 0000000..05f7b82 Binary files /dev/null and b/modules/database/tools/__pycache__/neontology_tools.cpython-311.pyc differ diff --git a/modules/database/tools/__pycache__/queries.cpython-311.pyc b/modules/database/tools/__pycache__/queries.cpython-311.pyc new file mode 100644 index 0000000..2710a95 Binary files /dev/null and b/modules/database/tools/__pycache__/queries.cpython-311.pyc differ diff --git a/modules/database/tools/db_operations.py b/modules/database/tools/db_operations.py new file mode 100644 index 0000000..71865e0 --- /dev/null +++ b/modules/database/tools/db_operations.py @@ -0,0 +1,74 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_db_operations' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from fastapi.testclient import TestClient +from fastapi import HTTPException +import time +from neo4j import GraphDatabase + +class DatabaseNotFoundError(Exception): + """Exception raised when the specified database cannot be found.""" + def __init__(self, db_name): + super().__init__(f"Database '{db_name}' not found.") + +# Dev ?? +def get_client(): + from main import app # Delayed import to avoid circular dependency + return TestClient(app) + +# Ops ?? +def stop_database(db_name): + client = get_client() + try: + logging.debug(f"Stopping database {db_name}") + response = client.post("/database/admin/stop-database", json={"db_name": db_name}) + except DatabaseNotFoundError: + logging.info(f"Database {db_name} not found when attempting to stop. Skipping.") + else: + logging.info(response.text) + return response + +def drop_database(db_name): + client = get_client() + try: + response = client.post("/database/admin/drop-database", json={"db_name": db_name}) + except DatabaseNotFoundError: + logging.info(f"Database {db_name} not found when attempting to drop. Skipping.") + else: + logging.info(response.text) + return response + +def create_database(db_name): + client = get_client() + response = client.post("/database/admin/create-database", params={"db_name": db_name}) + logging.info(response.text) + return response + +def check_database_availability(db_name, retries=5, delay=5): # Increased delay + client = get_client() + attempt = 0 + while attempt < retries: + try: + logging.info(f"Attempt {attempt + 1}: Checking availability for database {db_name}") + response = client.get(f"/check-database-availability?db_name={db_name}") + if response.status_code == 200 and response.json().get('status') == "ready": + logging.info(f"Database {db_name} is ready.") + return response.json() + else: + logging.error(f"Database {db_name} is not available: {response.text}") + except Exception as e: + logging.error(f"Error checking database availability for {db_name} on attempt {attempt + 1}: {e}") + time.sleep(delay) # Increased delay before the next retry + attempt += 1 + raise HTTPException(status_code=503, detail="Database availability check failed after retries") \ No newline at end of file diff --git a/modules/database/tools/filesystem_tools.py b/modules/database/tools/filesystem_tools.py new file mode 100644 index 0000000..87c0a16 --- /dev/null +++ b/modules/database/tools/filesystem_tools.py @@ -0,0 +1,560 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_filesystem_tools' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from datetime import timedelta +import json +import re + +class ClassroomCopilotFilesystem: + def __init__(self, db_name: str, init_run_type: str = None): + logging.info(f"Initializing ClassroomCopilotFilesystem with db_name: {db_name} and init_run_type: {init_run_type}") + + self.db_name = db_name + + # Get base path from environment + self.base_path = os.getenv("NODE_FILESYSTEM_PATH") + if not self.base_path: + raise ValueError("NODE_FILESYSTEM_PATH environment variable not set") + + # Set root path based on init type + if init_run_type == "school": + self.root_path = os.path.join(self.base_path, "schools", self.db_name) + logging.debug(f"School root path: {self.root_path}") + elif init_run_type == "user": + self.root_path = os.path.join(self.base_path, "users", self.db_name) + logging.debug(f"User root path: {self.root_path}") + elif init_run_type == "multiplayer": + self.root_path = os.path.join(self.base_path, "multiplayer") + logging.debug(f"Multiplayer root path: {self.root_path}") + else: + self.root_path = os.path.join(self.base_path, self.db_name) + logging.debug(f"Default root path: {self.root_path}") + + # Ensure root directory exists + os.makedirs(self.root_path, exist_ok=True) + + logging.debug(f"Filesystem initialized with run type: {init_run_type} and root path: {self.root_path}") + + def log_directory_structure(self, start_path): + for root, dirs, files in os.walk(start_path): + level = root.replace(start_path, '').count(os.sep) + indent = ' ' * 4 * (level) + logging.info(f"{indent}{os.path.basename(root)}/") + subindent = ' ' * 4 * (level + 1) + for f in files: + logging.info(f"{subindent}{f}") + + def create_directory(self, path): + """Utility method to create a directory if it doesn't exist.""" + if not os.path.exists(path): + os.makedirs(path) + logging.info(f"Directory {path} created.") + return True + return False + + def sanitize_username(self, username): + return re.sub(r'[^\w\-_\.]', '_', username) + + def create_user_directory(self, username, user_type=None, school_path=None): + """Create a directory for a specific user.""" + sanitized_username = self.sanitize_username(username) + + if school_path: + # For school database: /schools/[school_db]/users/[user_type]/[username] + user_path = os.path.join(self.root_path, "users", user_type, sanitized_username) + else: + # For user database: /users/[user_db]/[username] + user_path = os.path.join(self.root_path, sanitized_username) + + logging.info(f"Creating user directory at {user_path}") + return self.create_directory(user_path), user_path + + def create_user_worker_directory(self, user_path, worker_code): + """Create a worker directory under the user directory.""" + # Create worker directory: [user_path]/[worker_code] + worker_path = os.path.join(user_path, worker_code) + logging.info(f"Creating worker directory at {worker_path}") + return self.create_directory(worker_path), worker_path + + def create_school_worker_directory(self, school_path, worker_type): + """Create a worker directory under the school directory.""" + worker_path = os.path.join(school_path, "workers", worker_type) + logging.info(f"Creating school worker directory at {worker_path}") + return self.create_directory(worker_path), worker_path + + def create_school_directory(self, school_uuid=None): + """Create a directory for a specific school.""" + logging.info(f"Creating school directory with school_uuid: {school_uuid}") + if school_uuid is None: + logging.debug(f"School UUID is None, creating school directory at {self.root_path}") + school_path = self.root_path + else: + logging.debug(f"School UUID is not None, creating school directory at {os.path.join(self.root_path, school_uuid)}") + school_path = os.path.join(self.root_path, school_uuid) + return self.create_directory(school_path), school_path + + def create_year_directory(self, year, calendar_path=None): + """Create a directory for a specific year.""" + if calendar_path is None: + year_path = os.path.join(self.root_path, "calendar", str(year)) + else: + year_path = os.path.join(calendar_path, "years", str(year)) + + return self.create_directory(year_path), year_path + + def create_month_directory(self, year, month, calendar_path=None): + """Create a directory for a specific month.""" + if calendar_path is None: + month_path = os.path.join(self.root_path, "calendar", str(year), "months", f"{month:02}") + else: + month_path = os.path.join(calendar_path, "years", str(year), "months", f"{month:02}") + + return self.create_directory(month_path), month_path + + def create_week_directory(self, year, week, calendar_path=None): + """Create a directory for a specific week.""" + if calendar_path is None: + week_path = os.path.join(self.root_path, "calendar", str(year), "weeks", f"{week}") + else: + week_path = os.path.join(calendar_path, "years", str(year), "weeks", f"{week}") + + return self.create_directory(week_path), week_path + + def create_day_directory(self, year, month, day, calendar_path=None): + """Create a directory for a specific day.""" + if calendar_path is None: + day_path = os.path.join(self.root_path, "calendar", str(year), "months", f"{month:02}", f"{day:02}") + else: + day_path = os.path.join(calendar_path, "years", str(year), "months", f"{month:02}", f"{day:02}") + + return self.create_directory(day_path), day_path + + def setup_calendar_directories(self, start_date, end_date, calendar_path=None): + """Setup directories for the range from start_date to end_date.""" + current_date = start_date + while current_date <= end_date: + year, month, day = current_date.year, current_date.month, current_date.day + if calendar_path is None: + _, year_path = self.create_year_directory(year) + _, month_path = self.create_month_directory(year, month) + _, week_path = self.create_week_directory(year, current_date.isocalendar()[1]) + _, day_path = self.create_day_directory(year, month, day) + else: + _, year_path = self.create_year_directory(year, calendar_path) + _, month_path = self.create_month_directory(year, month, calendar_path) + _, week_path = self.create_week_directory(year, current_date.isocalendar()[1], calendar_path) + _, day_path = self.create_day_directory(year, month, day, calendar_path) + current_date += timedelta(days=1) + return year_path, month_path, week_path, day_path + + def create_school_timetable_directory(self, school_path=None): + """Create a directory for the timetable.""" + if school_path is None: + timetable_path = os.path.join(self.root_path, "timetable") + else: + timetable_path = os.path.join(school_path, "timetable") + + return self.create_directory(timetable_path), timetable_path + + def create_school_timetable_year_directory(self, timetable_path, year): + """Create a directory for a specific academic year within the timetable.""" + year_path = os.path.join(timetable_path, "years", str(year)) + return self.create_directory(year_path), year_path + + def create_school_timetable_academic_term_directory(self, timetable_path, term_name, term_number): + """Create a directory for a specific term within an academic year.""" + term_path = os.path.join(timetable_path, "terms", f"{term_number}_{term_name.replace(' ', '_')}") + return self.create_directory(term_path), term_path + + def create_school_timetable_academic_term_break_directory(self, timetable_path, term_name): + """Create a directory for a specific term within an academic year.""" + term_path = os.path.join(timetable_path, "terms", "term_breaks", f"{term_name.replace(' ', '_')}") + return self.create_directory(term_path), term_path + + def create_school_timetable_academic_week_directory(self, timetable_path, week_number): + """Create a directory for a specific week within a term of a specific year.""" + week_path = os.path.join(timetable_path, "weeks", f"{week_number}") + return self.create_directory(week_path), week_path + + def create_school_timetable_academic_day_directory(self, timetable_path, academic_day): + """Create a directory for a specific day within a week of a term.""" + day_path = os.path.join(timetable_path, "days",f"{academic_day:02}") + return self.create_directory(day_path), day_path + + def create_school_timetable_period_directory(self, timetable_path, academic_day, period_dir): + """Create a directory for a specific period within a day.""" + period_path = os.path.join(timetable_path, "days",f"{academic_day:02}", f"{period_dir}") + return self.create_directory(period_path), period_path + + def create_school_curriculum_directory(self, school_path=None): + """Create a directory for the curriculum.""" + if school_path is None: + curriculum_path = os.path.join(self.root_path, "curriculum") + else: + curriculum_path = os.path.join(school_path, "curriculum") + + return self.create_directory(curriculum_path), curriculum_path + + def create_school_pastoral_directory(self, school_path=None): + """Create a directory for the pastoral.""" + if school_path is None: + pastoral_path = os.path.join(self.root_path, "pastoral") + else: + pastoral_path = os.path.join(school_path, "pastoral") + + return self.create_directory(pastoral_path), pastoral_path + + def create_school_department_directory(self, school_path, department): + """Create a directory for a specific department within the school.""" + department_path = os.path.join(school_path, "departments", f"{department}") + return self.create_directory(department_path), department_path + + def create_department_subject_directory(self, department_path, subject_name): + """Create a directory for a specific subject within a department.""" + subject_path = os.path.join(department_path, "subjects", f"{subject_name}") + return self.create_directory(subject_path), subject_path + + def create_curriculum_key_stage_syllabus_directory(self, curriculum_path, key_stage, subject_name, syllabus_id): + """Create a directory for a specific key stage syllabus under the curriculum structure.""" + # Replace spaces with underscores and remove any special characters from subject name + safe_subject_name = re.sub(r'[^\w\-_\.]', '_', subject_name) + syllabus_path = os.path.join(curriculum_path, "subjects", safe_subject_name, "key_stage_syllabuses", f"KS{key_stage}", f"KS{key_stage}.{safe_subject_name}") + return self.create_directory(syllabus_path), syllabus_path + + def create_pastoral_year_group_directory(self, pastoral_path, year_group): + """Create a directory for a specific year group under the pastoral structure.""" + year_group_path = os.path.join(pastoral_path, "year_groups", f"Y{year_group}") + return self.create_directory(year_group_path), year_group_path + + def create_curriculum_year_group_syllabus_directory(self, curriculum_path, subject_name, year_group, syllabus_id): + """Create a directory for a specific year group syllabus under the curriculum structure.""" + # Replace spaces with underscores and remove any special characters from subject name + safe_subject_name = re.sub(r'[^\w\-_\.]', '_', subject_name) + syllabus_path = os.path.join(curriculum_path, "subjects", safe_subject_name, "year_group_syllabuses", f"Y{year_group}", f"Y{year_group}.{safe_subject_name}") + return self.create_directory(syllabus_path), syllabus_path + + def create_curriculum_topic_directory(self, year_group_syllabus_path, topic_id): + """Create a directory for a specific topic under a year group syllabus.""" + topic_path = os.path.join(year_group_syllabus_path, "topics", f"{topic_id}") + return self.create_directory(topic_path), topic_path + + def create_curriculum_lesson_directory(self, topic_path, lesson_id): + """Create a directory for a specific lesson under a topic.""" + lesson_path = os.path.join(topic_path, "lessons", f"{lesson_id}") + return self.create_directory(lesson_path), lesson_path + + def create_curriculum_learning_statement_directory(self, lesson_path, statement_id): + """Create a directory for a specific learning statement under a lesson.""" + statement_path = os.path.join(lesson_path, "learning_statements", f"{statement_id}") + return self.create_directory(statement_path), statement_path + + # Remove or mark as deprecated the old methods + + + def create_teacher_timetable_directory(self, teacher_path): + teacher_timetable_path = os.path.join(teacher_path, "timetable") + return self.create_directory(teacher_timetable_path), teacher_timetable_path + + def create_teacher_class_directory(self, teacher_timetable_path, class_name): + class_path = os.path.join(teacher_timetable_path, "classes", class_name) + return self.create_directory(class_path), class_path + + def create_teacher_timetable_lesson_directory(self, class_path, lesson_id): + lesson_path = os.path.join(class_path, "timetabled_lessons", lesson_id) + return self.create_directory(lesson_path), lesson_path + + def create_teacher_planned_lesson_directory(self, class_path, lesson_id): + planned_lesson_path = os.path.join(class_path, "planned_lessons", lesson_id) + return self.create_directory(planned_lesson_path), planned_lesson_path + + # TLDraw File Creation + def create_default_tldraw_file(self, node_path, node_data): + """Create a tldraw file for a node.""" + logging.info(f"Creating tldraw file for node at {node_path}") + + # Ensure the directory exists + os.makedirs(node_path, exist_ok=True) + + tldraw_path = os.path.join(node_path, 'tldraw_file.json') + + # Create default tldraw content + tldraw_content = { + "document": { + "store": { + "document:document": { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + "page:page": { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + } + }, + "schema": + {"schemaVersion":2, + "sequences": { + "com.tldraw.store":4, + "com.tldraw.asset":1, + "com.tldraw.camera":1, + "com.tldraw.document":2, + "com.tldraw.instance":25, + "com.tldraw.instance_page_state":5, + "com.tldraw.page":1, + "com.tldraw.instance_presence":5, + "com.tldraw.pointer":1, + "com.tldraw.shape":4, + "com.tldraw.asset.bookmark":2, + "com.tldraw.asset.image":5, + "com.tldraw.asset.video":5, + "com.tldraw.shape.arrow":5, + "com.tldraw.shape.bookmark":2, + "com.tldraw.shape.draw":2, + "com.tldraw.shape.embed":4, + "com.tldraw.shape.frame":0, + "com.tldraw.shape.geo":9, + "com.tldraw.shape.group":0, + "com.tldraw.shape.highlight":1, + "com.tldraw.shape.image":4, + "com.tldraw.shape.line":5, + "com.tldraw.shape.note":8, + "com.tldraw.shape.text":2, + "com.tldraw.shape.video":2, + "com.tldraw.shape.youtube-embed":0, + "com.tldraw.shape.calendar":0, + "com.tldraw.shape.microphone":1, + "com.tldraw.shape.transcriptionText":0, + "com.tldraw.shape.slide":0,"com.tldraw.shape.slideshow":0, + "com.tldraw.shape.user_node":1, + "com.tldraw.shape.developer_node":1, + "com.tldraw.shape.student_node":1, + "com.tldraw.shape.teacher_node":1, + "com.tldraw.shape.calendar_node":1, + "com.tldraw.shape.calendar_year_node":1, + "com.tldraw.shape.calendar_month_node":1, + "com.tldraw.shape.calendar_week_node":1, + "com.tldraw.shape.calendar_day_node":1, + "com.tldraw.shape.calendar_time_chunk_node":1, + "com.tldraw.shape.teacher_timetable_node":1, + "com.tldraw.shape.timetable_lesson_node":1, + "com.tldraw.shape.planned_lesson_node":1, + "com.tldraw.shape.pastoral_structure_node":1, + "com.tldraw.shape.year_group_node":1, + "com.tldraw.shape.curriculum_structure_node":1, + "com.tldraw.shape.key_stage_node":1, + "com.tldraw.shape.key_stage_syllabus_node":1, + "com.tldraw.shape.year_group_syllabus_node":1, + "com.tldraw.shape.subject_node":1, + "com.tldraw.shape.topic_node":1, + "com.tldraw.shape.topic_lesson_node":1, + "com.tldraw.shape.learning_statement_node":1, + "com.tldraw.shape.science_lab_node":1, + "com.tldraw.shape.school_timetable_node":1, + "com.tldraw.shape.academic_year_node":1, + "com.tldraw.shape.academic_term_node":1, + "com.tldraw.shape.academic_week_node":1, + "com.tldraw.shape.academic_day_node":1, + "com.tldraw.shape.academic_period_node":1, + "com.tldraw.shape.registration_period_node":1, + "com.tldraw.shape.school_node":1, + "com.tldraw.shape.department_node":1, + "com.tldraw.shape.room_node":1, + "com.tldraw.shape.subject_class_node":1, + "com.tldraw.shape.general_relationship":1, + "com.tldraw.binding.arrow":0, + "com.tldraw.binding.slide-layout":0 + } + }, + "recordVersions": { + "asset": { "version": 1, "subTypeKey": "type", "subTypeVersions": {} }, + "camera": { "version": 1 }, + "document": { "version": 2 }, + "instance": { "version": 21 }, + "instance_page_state": { "version": 5 }, + "page": { "version": 1 }, + "shape": { "version": 3, "subTypeKey": "type", "subTypeVersions": {} }, + "instance_presence": { "version": 5 }, + "pointer": { "version": 1 } + }, + "rootShapeIds":[], + "bindings":[], + "assets":[] + }, + "session": { + "version": 0, + "currentPageId": "page:page", + "pageStates": [{ + "pageId": "page:page", + "camera": {"x": 0, "y": 0, "z": 1}, + "selectedShapeIds": [] + }] + }, + "node_data": node_data + } + + with open(tldraw_path, 'w') as f: + json.dump(tldraw_content, f, indent=4) + + logging.info(f"tldraw file created at {tldraw_path}") + return tldraw_path + + def create_default_tldraw_file_in_storage(self, admin_supabase, bucket_id, file_path, node_data): + """Create a tldraw file in Supabase storage.""" + logging.info(f"Creating tldraw file in storage at {file_path}") + + # Create default tldraw content + tldraw_content = { + "document": { + "store": { + "document:document": { + "gridSize": 10, + "name": "", + "meta": {}, + "id": "document:document", + "typeName": "document" + }, + "page:page": { + "meta": {}, + "id": "page:page", + "name": "Page 1", + "index": "a1", + "typeName": "page" + } + }, + "schema": + {"schemaVersion":2, + "sequences": { + "com.tldraw.store":4, + "com.tldraw.asset":1, + "com.tldraw.camera":1, + "com.tldraw.document":2, + "com.tldraw.instance":25, + "com.tldraw.instance_page_state":5, + "com.tldraw.page":1, + "com.tldraw.instance_presence":5, + "com.tldraw.pointer":1, + "com.tldraw.shape":4, + "com.tldraw.asset.bookmark":2, + "com.tldraw.asset.image":5, + "com.tldraw.asset.video":5, + "com.tldraw.shape.arrow":5, + "com.tldraw.shape.bookmark":2, + "com.tldraw.shape.draw":2, + "com.tldraw.shape.embed":4, + "com.tldraw.shape.frame":0, + "com.tldraw.shape.geo":9, + "com.tldraw.shape.group":0, + "com.tldraw.shape.highlight":1, + "com.tldraw.shape.image":4, + "com.tldraw.shape.line":5, + "com.tldraw.shape.note":8, + "com.tldraw.shape.text":2, + "com.tldraw.shape.video":2, + "com.tldraw.shape.youtube-embed":0, + "com.tldraw.shape.calendar":0, + "com.tldraw.shape.microphone":1, + "com.tldraw.shape.transcriptionText":0, + "com.tldraw.shape.slide":0,"com.tldraw.shape.slideshow":0, + "com.tldraw.shape.user_node":1, + "com.tldraw.shape.developer_node":1, + "com.tldraw.shape.student_node":1, + "com.tldraw.shape.teacher_node":1, + "com.tldraw.shape.calendar_node":1, + "com.tldraw.shape.calendar_year_node":1, + "com.tldraw.shape.calendar_month_node":1, + "com.tldraw.shape.calendar_week_node":1, + "com.tldraw.shape.calendar_day_node":1, + "com.tldraw.shape.calendar_time_chunk_node":1, + "com.tldraw.shape.teacher_timetable_node":1, + "com.tldraw.shape.timetable_lesson_node":1, + "com.tldraw.shape.planned_lesson_node":1, + "com.tldraw.shape.pastoral_structure_node":1, + "com.tldraw.shape.year_group_node":1, + "com.tldraw.shape.curriculum_structure_node":1, + "com.tldraw.shape.key_stage_node":1, + "com.tldraw.shape.key_stage_syllabus_node":1, + "com.tldraw.shape.year_group_syllabus_node":1, + "com.tldraw.shape.subject_node":1, + "com.tldraw.shape.topic_node":1, + "com.tldraw.shape.topic_lesson_node":1, + "com.tldraw.shape.learning_statement_node":1, + "com.tldraw.shape.science_lab_node":1, + "com.tldraw.shape.school_timetable_node":1, + "com.tldraw.shape.academic_year_node":1, + "com.tldraw.shape.academic_term_node":1, + "com.tldraw.shape.academic_week_node":1, + "com.tldraw.shape.academic_day_node":1, + "com.tldraw.shape.academic_period_node":1, + "com.tldraw.shape.registration_period_node":1, + "com.tldraw.shape.school_node":1, + "com.tldraw.shape.department_node":1, + "com.tldraw.shape.room_node":1, + "com.tldraw.shape.subject_class_node":1, + "com.tldraw.shape.general_relationship":1, + "com.tldraw.binding.arrow":0, + "com.tldraw.binding.slide-layout":0 + } + }, + "recordVersions": { + "asset": { "version": 1, "subTypeKey": "type", "subTypeVersions": {} }, + "camera": { "version": 1 }, + "document": { "version": 2 }, + "instance": { "version": 21 }, + "instance_page_state": { "version": 5 }, + "page": { "version": 1 }, + "shape": { "version": 3, "subTypeKey": "type", "subTypeVersions": {} }, + "instance_presence": { "version": 5 }, + "pointer": { "version": 1 } + }, + "rootShapeIds":[], + "bindings":[], + "assets":[] + }, + "session": { + "version": 0, + "currentPageId": "page:page", + "pageStates": [{ + "pageId": "page:page", + "camera": {"x": 0, "y": 0, "z": 1}, + "selectedShapeIds": [] + }] + }, + "node_data": node_data + } + + # Convert the content to JSON string + tldraw_json = json.dumps(tldraw_content, indent=4) + + try: + # Upload the file to Supabase storage + result = admin_supabase.storage.from_(bucket_id).upload( + path=file_path, + file=tldraw_json, + file_options={"content-type": "application/json"} + ) + + if result.get('error'): + logging.error(f"Error creating tldraw file in storage: {result['error']}") + raise Exception(f"Failed to create tldraw file: {result['error']}") + + logging.info(f"tldraw file created in storage at {file_path}") + return True + except Exception as e: + logging.error(f"Error creating tldraw file in storage: {str(e)}") + raise e \ No newline at end of file diff --git a/modules/database/tools/navigation/__init__.py b/modules/database/tools/navigation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/database/tools/navigation/user_navigation.py b/modules/database/tools/navigation/user_navigation.py new file mode 100644 index 0000000..d0930bb --- /dev/null +++ b/modules/database/tools/navigation/user_navigation.py @@ -0,0 +1,491 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools +from datetime import datetime, timedelta +from typing import List, Optional, Dict, Any + +def get_static_nodes(context: str, db_name: str) -> List[Dict[str, Any]]: + """Get static nodes for a specific context.""" + if context == 'workers': + # For workers context, show teacher node first, then timetables and classes + query = """ + MATCH (t:Teacher) + RETURN DISTINCT { + id: t.unique_id, + path: t.path, + label: t.teacher_name_formal, + type: 'Teacher', + isStatic: true, + order: 0, + section: 'Root' + } as node + UNION ALL + MATCH (t:UserTeacherTimetable) + RETURN DISTINCT { + id: t.unique_id, + path: t.path, + label: t.name, + type: 'UserTeacherTimetable', + isStatic: true, + order: 1, + section: 'Timetables' + } as node + UNION ALL + MATCH (t:UserTeacherTimetable)-[:HAS_CLASS]->(c:Class) + RETURN DISTINCT { + id: c.unique_id, + path: c.path, + label: c.name, + type: 'Class', + isStatic: true, + order: 2, + section: 'Classes' + } as node + """ + elif context == 'user': + # For user context, show the user node + query = """ + MATCH (u:User) + RETURN DISTINCT { + id: u.unique_id, + path: u.path, + label: u.user_name, + type: 'User', + isStatic: true, + order: 0, + section: 'Root' + } as node + """ + else: + # For calendar context, show today's calendar node first, then other calendar nodes + today = datetime.now().strftime("%Y-%m-%d") + query = """ + MATCH (n:Calendar) + WITH n, + CASE + WHEN date($today) >= date(n.start_date) AND date($today) <= date(n.end_date) + THEN 0 + ELSE 1 + END as nodeOrder + RETURN DISTINCT { + id: n.unique_id, + path: n.path, + label: n.name, + type: 'Calendar', + isStatic: true, + order: nodeOrder, + section: CASE nodeOrder + WHEN 0 THEN 'Today' + ELSE 'Calendar' + END + } as node + ORDER BY node.order, node.label + """ + + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, today=datetime.now().strftime("%Y-%m-%d")) + return [record["node"] for record in result] + except Exception as e: + logger.error(f"Error getting static nodes: {str(e)}") + return [] + +def get_today_calendar_node(db_name: str) -> Optional[Dict[str, Any]]: + """Get today's calendar node.""" + today = datetime.now().strftime("%Y-%m-%d") + query = """ + MATCH (n:Calendar) + WHERE date($today) >= date(n.start_date) AND date($today) <= date(n.end_date) + RETURN n.unique_id as id, n.path as path, n.name as label, + 'Calendar' as type + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, today=today) + record = result.single() + return dict(record) if record else None + except Exception as e: + logger.error(f"Error getting today's calendar node: {str(e)}") + return None + +def get_relative_calendar_node(day_offset: int, db_name: str) -> Optional[Dict[str, Any]]: + """Get calendar node relative to today.""" + target_date = (datetime.now() + timedelta(days=day_offset)).strftime("%Y-%m-%d") + query = """ + MATCH (n:Calendar) + WHERE date($target_date) >= date(n.start_date) AND date($target_date) <= date(n.end_date) + RETURN n.unique_id as id, n.path as path, n.name as label, + 'Calendar' as type + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, target_date=target_date) + record = result.single() + return dict(record) if record else None + except Exception as e: + logger.error(f"Error getting relative calendar node: {str(e)}") + return None + +def get_next_month_node(db_name: str) -> Optional[Dict[str, Any]]: + """Get next month's calendar node.""" + next_month_start = (datetime.now().replace(day=1) + timedelta(days=32)).replace(day=1).strftime("%Y-%m-%d") + query = """ + MATCH (n:Calendar) + WHERE date($next_month_start) >= date(n.start_date) AND date($next_month_start) <= date(n.end_date) + RETURN n.unique_id as id, n.path as path, n.name as label, + 'Calendar' as type + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, next_month_start=next_month_start) + record = result.single() + return dict(record) if record else None + except Exception as e: + logger.error(f"Error getting next month node: {str(e)}") + return None + +def get_previous_month_node(db_name: str) -> Optional[Dict[str, Any]]: + """Get previous month's calendar node.""" + prev_month_start = (datetime.now().replace(day=1) - timedelta(days=1)).replace(day=1).strftime("%Y-%m-%d") + query = """ + MATCH (n:Calendar) + WHERE date($prev_month_start) >= date(n.start_date) AND date($prev_month_start) <= date(n.end_date) + RETURN n.unique_id as id, n.path as path, n.name as label, + 'Calendar' as type + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, prev_month_start=prev_month_start) + record = result.single() + return dict(record) if record else None + except Exception as e: + logger.error(f"Error getting previous month node: {str(e)}") + return None + +def get_user_timetables(db_name: str) -> List[Dict[str, Any]]: + """Get user's timetables.""" + query = """ + MATCH (t:UserTeacherTimetable) + RETURN t.unique_id as id, t.path as path, t.name as label, + 'UserTeacherTimetable' as type + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + return [dict(record) for record in result] + except Exception as e: + logger.error(f"Error getting user timetables: {str(e)}") + return [] + +def get_timetable_classes(timetable_id: str, db_name: str) -> List[Dict[str, Any]]: + """Get classes for a timetable.""" + query = """ + MATCH (t:UserTeacherTimetable {unique_id: $timetable_id})-[:HAS_CLASS]->(c:Class) + RETURN c.unique_id as id, c.path as path, c.name as label, + 'Class' as type + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, timetable_id=timetable_id) + return [dict(record) for record in result] + except Exception as e: + logger.error(f"Error getting timetable classes: {str(e)}") + return [] + +def get_next_lesson(class_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get next lesson for a class.""" + now = datetime.now().strftime("%Y-%m-%d %H:%M") + query = """ + MATCH (c:Class {unique_id: $class_id})-[:HAS_LESSON]->(l:Lesson) + WHERE l.start_time > $now + RETURN l.unique_id as id, l.path as path, l.name as label, + 'Lesson' as type + ORDER BY l.start_time ASC + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, class_id=class_id, now=now) + record = result.single() + return dict(record) if record else None + except Exception as e: + logger.error(f"Error getting next lesson: {str(e)}") + return None + +def get_previous_lesson(class_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get previous lesson for a class.""" + now = datetime.now().strftime("%Y-%m-%d %H:%M") + query = """ + MATCH (c:Class {unique_id: $class_id})-[:HAS_LESSON]->(l:Lesson) + WHERE l.start_time < $now + RETURN l.unique_id as id, l.path as path, l.name as label, + 'Lesson' as type + ORDER BY l.start_time DESC + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, class_id=class_id, now=now) + record = result.single() + return dict(record) if record else None + except Exception as e: + logger.error(f"Error getting previous lesson: {str(e)}") + return None + +def save_shared_snapshot(path: str, room_id: str, snapshot: Dict[str, Any]) -> bool: + """Save snapshot to a shared room.""" + try: + # Save the snapshot to the shared room's storage + session_tools.save_tldraw_node_file(path, room_id, snapshot) + return True + except Exception as e: + logger.error(f"Error saving shared snapshot: {str(e)}") + return False + +def get_connected_nodes_for_workers(node_id: str, db_name: str) -> List[Dict[str, Any]]: + """Get connected nodes specific to the workers context.""" + query = """ + MATCH (n {unique_id: $node_id}) + WITH n + CALL { + WITH n + MATCH (n:UserTeacherTimetable)-[:HAS_CLASS]->(c:Class) + RETURN c.unique_id as id, c.path as path, c.name as label, + 'Class' as type + UNION + MATCH (n:Class)<-[:HAS_CLASS]-(t:UserTeacherTimetable) + RETURN t.unique_id as id, t.path as path, t.name as label, + 'UserTeacherTimetable' as type + UNION + MATCH (n:Class)-[:HAS_LESSON]->(l:Lesson) + RETURN l.unique_id as id, l.path as path, l.name as label, + 'Lesson' as type + } + RETURN DISTINCT id, path, label, type + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, node_id=node_id) + return [dict(record) for record in result] + except Exception as e: + logger.error(f"Error getting connected nodes for workers: {str(e)}") + return [] + +def get_connected_nodes(node_id: str, db_name: str, context: str = None) -> List[Dict[str, Any]]: + """Get connected nodes based on context.""" + if context == 'workers': + return get_connected_nodes_for_workers(node_id, db_name) + + # Default query for other contexts + query = """ + MATCH (n {unique_id: $node_id})-[r]-(connected) + RETURN DISTINCT connected.unique_id as id, connected.path as path, + connected.name as label, labels(connected)[0] as type + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, node_id=node_id) + return [dict(record) for record in result] + except Exception as e: + logger.error(f"Error getting connected nodes: {str(e)}") + return [] + +## Worker Navigation + +def get_worker_structure(db_name: str) -> Dict[str, Any]: + """Get the complete worker structure including schools, departments, timetables, classes, and lessons.""" + try: + query = """ + // Match all worker-related nodes + MATCH (s:School) + OPTIONAL MATCH (s)-[:HAS_DEPARTMENT]->(d:Department) + OPTIONAL MATCH (d)-[:HAS_TIMETABLE]->(t:UserTeacherTimetable) + OPTIONAL MATCH (t)-[:HAS_CLASS]->(c:Class) + OPTIONAL MATCH (c)-[:HAS_LESSON]->(l:TimetableLesson) + WITH s, d, t, c, l + ORDER BY s.school_name, d.department_code, t.name, c.class_code, l.start_time + + // Collect all nodes + RETURN { + schools: collect(DISTINCT { + id: s.unique_id, + path: s.path, + name: s.school_name, + __primarylabel__: 'School' + }), + departments: collect(DISTINCT { + id: d.unique_id, + path: d.path, + code: d.department_code, + school_id: s.unique_id, + __primarylabel__: 'Department' + }), + timetables: collect(DISTINCT { + id: t.unique_id, + path: t.path, + name: t.name, + department_id: d.unique_id, + __primarylabel__: 'UserTeacherTimetable' + }), + classes: collect(DISTINCT { + id: c.unique_id, + path: c.path, + code: c.class_code, + timetable_id: t.unique_id, + __primarylabel__: 'Class' + }), + lessons: collect(DISTINCT { + id: l.unique_id, + path: l.path, + start_time: l.start_time, + class_id: c.unique_id, + __primarylabel__: 'TimetableLesson' + }) + } as structure + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + record = result.single() + if not record: + logger.error('No worker structure found') + return None + + return { + "status": "success", + "structure": record["structure"] + } + + except Exception as e: + logger.error(f"Error getting worker structure: {str(e)}") + return None + +def get_school_node(school_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get a specific school node.""" + query = """ + MATCH (s:School {unique_id: $school_id}) + RETURN { + id: s.unique_id, + path: s.path, + name: s.school_name, + __primarylabel__: 'School' + } as node + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, school_id=school_id) + record = result.single() + return record["node"] if record else None + except Exception as e: + logger.error(f"Error getting school node: {str(e)}") + return None + +def get_department_node(dept_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get a specific department node.""" + query = """ + MATCH (d:Department {unique_id: $dept_id}) + RETURN { + id: d.unique_id, + path: d.path, + code: d.department_code, + __primarylabel__: 'Department' + } as node + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, dept_id=dept_id) + record = result.single() + return record["node"] if record else None + except Exception as e: + logger.error(f"Error getting department node: {str(e)}") + return None + +def get_timetable_node(timetable_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get a specific timetable node.""" + query = """ + MATCH (t:UserTeacherTimetable {unique_id: $timetable_id}) + RETURN { + id: t.unique_id, + path: t.path, + name: t.name, + __primarylabel__: 'UserTeacherTimetable' + } as node + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, timetable_id=timetable_id) + record = result.single() + return record["node"] if record else None + except Exception as e: + logger.error(f"Error getting timetable node: {str(e)}") + return None + +def get_class_node(class_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get a specific class node.""" + query = """ + MATCH (c:Class {unique_id: $class_id}) + RETURN { + id: c.unique_id, + path: c.path, + code: c.class_code, + __primarylabel__: 'Class' + } as node + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, class_id=class_id) + record = result.single() + return record["node"] if record else None + except Exception as e: + logger.error(f"Error getting class node: {str(e)}") + return None + +def get_lesson_node(lesson_id: str, db_name: str) -> Optional[Dict[str, Any]]: + """Get a specific lesson node.""" + query = """ + MATCH (l:TimetableLesson {unique_id: $lesson_id}) + RETURN { + id: l.unique_id, + path: l.path, + start_time: l.start_time, + __primarylabel__: 'TimetableLesson' + } as node + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, lesson_id=lesson_id) + record = result.single() + return record["node"] if record else None + except Exception as e: + logger.error(f"Error getting lesson node: {str(e)}") + return None + +def get_current_lesson(db_name: str) -> Optional[Dict[str, Any]]: + """Get the current or next upcoming lesson.""" + now = datetime.now().strftime("%Y-%m-%d %H:%M") + query = """ + MATCH (l:TimetableLesson) + WHERE l.start_time >= $now + RETURN { + id: l.unique_id, + path: l.path, + start_time: l.start_time, + __primarylabel__: 'TimetableLesson' + } as node + ORDER BY l.start_time ASC + LIMIT 1 + """ + try: + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, now=now) + record = result.single() + return record["node"] if record else None + except Exception as e: + logger.error(f"Error getting current lesson: {str(e)}") + return None \ No newline at end of file diff --git a/modules/database/tools/neo4j_db_formatter.py b/modules/database/tools/neo4j_db_formatter.py new file mode 100644 index 0000000..75c420a --- /dev/null +++ b/modules/database/tools/neo4j_db_formatter.py @@ -0,0 +1,21 @@ +def format_user_email_for_neo_db(user_email): + """Format user email for Neo4j database name. + + Neo4j database names can only contain letters, numbers, dots, and dashes. + We'll convert the email to a valid format: + example@domain.com -> ccuser-example-at-domain-com + + Args: + user_email: Email address to format + + Returns: + Formatted string suitable for Neo4j database name + """ + # Convert to lowercase and replace special characters + sanitized = user_email.lower() + sanitized = sanitized.replace('@', 'at') + sanitized = sanitized.replace('.', 'dot') + sanitized = sanitized.replace('_', 'underscore') + sanitized = sanitized.replace('-', 'dash') + + return f"{sanitized}" \ No newline at end of file diff --git a/modules/database/tools/neo4j_driver_tools.py b/modules/database/tools/neo4j_driver_tools.py new file mode 100644 index 0000000..61d48f6 --- /dev/null +++ b/modules/database/tools/neo4j_driver_tools.py @@ -0,0 +1,153 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import time +from typing import Optional, Tuple, Generator +from modules.logger_tool import initialise_logger +from neo4j import GraphDatabase as gd, Driver, Session +from contextlib import contextmanager + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +def _retry_with_backoff( + func, + max_attempts: int = 10, # Increased from 3 to 10 + initial_delay: float = 2.0, # Increased from 1 to 2 seconds + max_total_wait: float = 60.0, # Maximum total time to wait (60 seconds) + max_delay: float = 10.0 # Maximum delay between retries +) -> any: + """ + Helper function to retry operations with exponential backoff. + + Args: + func: Function to retry + max_attempts: Maximum number of retry attempts + initial_delay: Initial delay between retries in seconds + max_total_wait: Maximum total time to wait before giving up + max_delay: Maximum delay between retries + """ + attempt = 0 + delay = initial_delay + start_time = time.time() + + while attempt < max_attempts: + try: + return func() + except Exception as e: + attempt += 1 + elapsed_time = time.time() - start_time + + # Check if we've exceeded the maximum total wait time + if elapsed_time >= max_total_wait: + logger.error(f"Exceeded maximum total wait time of {max_total_wait} seconds") + raise + + if attempt == max_attempts: + logger.error(f"Final attempt {attempt} failed: {e}") + raise + + # Calculate next delay with exponential backoff, but cap it + delay = min(delay * 2, max_delay) + + # If we're in a container initialization scenario, provide more context + if "Connection refused" in str(e): + logger.warning( + f"Attempt {attempt} failed: Connection refused. " + f"This might indicate that Neo4j is still starting up. " + f"Retrying in {delay:.1f} seconds... " + f"(Total elapsed: {elapsed_time:.1f}s)" + ) + else: + logger.warning(f"Attempt {attempt} failed: {e}. Retrying in {delay:.1f} seconds...") + + time.sleep(delay) + +def get_driver(db_name: Optional[str] = None, url: Optional[str] = None, auth: Optional[Tuple[str, str]] = None) -> Optional[Driver]: + if url is None: + url = os.getenv("APP_BOLT_URL") + username = os.getenv("USER_NEO4J") + password = os.getenv("PASSWORD_NEO4J") + if not username or not password: + logger.error("Neo4j credentials not found in environment") + return None + auth = (username, password) + + if auth is None: + logger.error("No authentication credentials provided") + return None + + def create_driver(): + logger.info(f"Attempting to connect to Neo4j at {url}") + driver = gd.driver(url, auth=auth) + driver.verify_connectivity() + logger.info(f"Connected to Neo4j at {url}") + return driver + + try: + # Use more lenient retry parameters for initial connection + driver = _retry_with_backoff( + create_driver, + max_attempts=10, + initial_delay=2.0, + max_total_wait=60.0, + max_delay=10.0 + ) + except Exception as e: + logger.error(f"Failed to establish Neo4j connection after all retries: {e}") + return None + + # Test the connection with the specific database + if db_name and driver: + def verify_database(): + with driver.session(database=db_name) as session: + result = session.run("RETURN 'Connection successful' AS message") + record = result.single() + if not record or not record.get("message"): + raise Exception(f"Failed to verify database {db_name} connection") + logger.info(f"Connection to Neo4j at {url} with database {db_name} successful") + + try: + # Use more lenient retry parameters for database verification + _retry_with_backoff( + verify_database, + max_attempts=10, + initial_delay=2.0, + max_total_wait=60.0, + max_delay=10.0 + ) + except Exception as e: + logger.error(f"Failed to connect to database {db_name} after all retries: {e}") + driver.close() + return None + + return driver + +def close_driver(driver: Optional[Driver]) -> None: + if driver: + logger.info("Closing driver") + driver.close() + +# Global driver instance +_driver: Optional[Driver] = None + +def get_global_driver() -> Optional[Driver]: + """Get or create the global Neo4j driver instance.""" + global _driver + if _driver is None: + _driver = get_driver() + return _driver + +@contextmanager +def get_session(database: Optional[str] = None) -> Generator[Session, None, None]: + """Get a Neo4j session using the global driver.""" + driver = get_global_driver() + if driver is None: + raise Exception("Failed to get Neo4j driver") + + session = None + try: + session = driver.session(database=database) + yield session + finally: + if session: + session.close() \ No newline at end of file diff --git a/modules/database/tools/neo4j_http_tools.py b/modules/database/tools/neo4j_http_tools.py new file mode 100644 index 0000000..c81a27d --- /dev/null +++ b/modules/database/tools/neo4j_http_tools.py @@ -0,0 +1,64 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_neo4j_http_tools' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import requests +import base64 + +dev_mode = os.getenv('DEV_MODE', 'false') + +def send_query(query, encoded_credentials=None, params=None, method='POST', database="system", endpoint="/tx/commit"): + if encoded_credentials is None: + logging.debug(f"Sending query to Neo4j: {query}") + credentials = f"{os.getenv('USER_NEO4J')}:{os.getenv('PASSWORD_NEO4J')}" + encoded_credentials = base64.b64encode(credentials.encode()).decode('utf-8') + logging.debug(f"Encoded credentials: {encoded_credentials}") + + # Use HTTPS for production, HTTP for development + neo4j_url = f"{os.getenv('APP_GRAPH_URL')}/db/{database}{endpoint}" + logging.debug(f"URL: {neo4j_url}") + headers = {'Content-Type': 'application/json', 'Authorization': f'Basic {encoded_credentials}'} + logging.debug(f"Headers: {headers}") + data = { + "statements": [{ + "statement": query, + "parameters": params or {} + }] + } + logging.debug(f"Data: {data}") + + try: + logging.debug(f"Sending request to Neo4j...") + response = requests.request(method, neo4j_url, json=data, headers=headers) + response.raise_for_status() # Raise an HTTPError for bad responses + logging.debug(f"Response status code: {response.status_code}") + logging.debug(f"Response content: {response.content}") + return response.json() + except requests.exceptions.RequestException as e: + logging.error(f"Request to Neo4j failed: {e}") + raise + +def create_node(node_type: str, node_data: dict, db=None): + query = f"CREATE (n:{node_type} $props) RETURN id(n)" + params = {"props": node_data} + response = send_query(query, database=db, params=params) + return response['results'][0]['data'][0]['meta'][0]['id'] + +def create_relationship(relationship_data: dict, db=None): + query = """ + MATCH (a), (b) WHERE id(a) = $start_id AND id(b) = $end_id + CREATE (a)-[r:{rel_type}]->(b) + RETURN r + """ + params = {"start_id": relationship_data['start_node']['id'], "end_id": relationship_data['end_node']['id'], "rel_type": relationship_data['relationship_type'], "props": relationship_data.get('properties', {})} + return send_query("/db/neo4j/tx/commit", query, params, db=db) \ No newline at end of file diff --git a/modules/database/tools/neo4j_session_tools.py b/modules/database/tools/neo4j_session_tools.py new file mode 100644 index 0000000..8bbce5f --- /dev/null +++ b/modules/database/tools/neo4j_session_tools.py @@ -0,0 +1,504 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_neo4j_session_tools' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.queries as query +from contextlib import suppress + +def get_node_by_unique_id_and_adjacent_nodes(session, unique_id): + return session.read_transaction(_get_node_by_unique_id_and_adjacent_nodes, unique_id) + +def _get_node_by_unique_id_and_adjacent_nodes(tx, unique_id): + query = """ + MATCH (n {unique_id: $unique_id}) + OPTIONAL MATCH (n)-[r]-(adjacent) + RETURN n AS node, COLLECT(DISTINCT {node: adjacent, relationship: r}) AS connected_nodes + """ + result = tx.run(query, unique_id=unique_id) + record = result.single() + if record: + node = record["node"] + connected_nodes = record["connected_nodes"] + return {"node": node, "connected_nodes": connected_nodes} + return None + +def delete_all_nodes_and_relationships(session): + total_deleted = 0 + while True: + deleted_count = session.write_transaction(_delete_batch) + total_deleted += deleted_count + if deleted_count == 0: + break + +def _delete_batch(tx): + result_data = tx.run(query.delete_batch, batch_size=10000).single() + return 0 if result_data is None else result_data[0] + +def delete_all_constraints(session): + if show_constraints_result := session.run(query.show_constraints).data(): + for constraint in show_constraints_result: + constraint_name = constraint['name'] + session.run(query.drop_constraint(constraint_name)) + +def reset_all_indexes(session): + indexes = session.run(query.show_indexes).data() + for index in indexes: + index_name = index['name'] + session.run(query.drop_index(index_name)) + +def reset_databases(session): + delete_all_nodes_and_relationships(session) + delete_all_constraints(session) + reset_all_indexes(session) + +def close_session(session): + if session: + with suppress(Exception): + session.close() + +def create_node(session, label, properties, returns=False): + """ + Function to create a node in Neo4j database. + + Args: + driver (neo4j.Driver): The Neo4j driver. + session (str): The Neo4j session. + label (str): The label of the node. + properties (dict): A dictionary of properties for the node. + + Example usage: + create_node(neo4j_driver, "Topic", {"TopicID": "AP.PAG10", "Title": "Topic 10"}) + + Returns: + None + """ + transaction = session.write_transaction(_create_node, label, properties) + if returns: + transaction_id = transaction.id + # logging.database(f"Created {label} node with transaction ID {transaction_id} and properties {properties}") + print(f"Created {label} node with transaction ID {transaction_id} and properties {properties}") + return find_node_by_transaction_id(session, transaction_id) + else: + # logging.warning(f"Failed to create {label} node with properties {properties}") + print(f"Failed to create {label} node with properties {properties}") + return None + +def _create_node(tx, label, properties): + query = f""" + CREATE (n:{label} $properties) + RETURN n + """ + # logging.query(f"Running query: {query}") + print(f"Running query: {query}") + result = tx.run(query, properties=properties) + return result.single()[0] if result.single() is not None else None # Handle no record found + +# Function to find a node by its element ID +def find_node_by_transaction_id(session, transaction_id): + """ + Function to find a node in Neo4j database by its element ID. + + Args: + driver (neo4j.Driver): The Neo4j driver. + element_id (str): The element ID of the node to find. + + Returns: + The matched node. + """ + return session.read_transaction(_find_node_by_element_id, transaction_id) + +def _find_node_by_element_id(tx, transaction_id): + query = """ + MATCH (n) + WHERE id(n) = $transaction_id + RETURN n + """ + # logging.query(f"Running query: {query}") + result = tx.run(query, transaction_id=transaction_id) + record = result.single() # Get the single result record, if any + return record[0] if record is not None else None # Handle no record found + +# Function to create a relationship between two nodes in Neo4j +def create_relationship(session, start_node, end_node, label, properties=None, returns=False): + """ + Function to create a relationship between two nodes in Neo4j database. + + Args: + driver (neo4j.Driver): The Neo4j driver. + session (str): The Neo4j session. + start_node (str): The ID of the start node. + end_node (str): The ID of the end node. + rel_type (str): The type of the relationship. + properties (dict): A dictionary of properties for the relationship. + + Example usage: + create_relationship(neo4j_driver, "AP.PAG10", "AP.PAG11", "HAS_NEXT") + + Returns: + None + """ + relationship = session.write_transaction(_create_relationship, start_node, end_node, label, properties) + if returns: + relationship_id = relationship.id + return find_relationship_by_relationship_id(session, relationship_id) + else: + return None + +def _create_relationship(tx, start_node, end_node, label, properties): + query = f""" + MATCH (a), (b) + WHERE ID(a) = $start_node_id AND ID(b) = $end_node_id + CREATE (a)-[r:{label}]->(b) + RETURN r + """ + # logging.query(f"Running query: {query}") + result = tx.run(query, start_node_id=start_node.id, end_node_id=end_node.id, properties=properties) + single_result = result.single() + return single_result[0] if single_result is not None else None + +def order_list_of_nodes_by_property(session, label, property_name, order="ASC"): + """ + Function to order a list of nodes in Neo4j database by a property. + + Args: + driver (neo4j.Driver): The Neo4j driver. + label (str): The label of the nodes to find. + property_name (str): The name of the property to order by. + order (str): The order of the sorting (ASC or DESC). + + Returns: + List of matched nodes. + """ + return session.read_transaction(_order_list_of_nodes_by_property, label, property_name, order) + +def _order_list_of_nodes_by_property(tx, label, property_name, order): + query = f""" + MATCH (n:{label}) + RETURN n + ORDER BY n.{property_name} {order} + """ + # logging.query(f"Running query: {query}") + result = tx.run(query) + return [record["n"] for record in result] + +def find_relationship_by_relationship_id(session, relationship_id): + """ + Function to find a relationship in Neo4j database by its relationship ID. + + Args: + driver (neo4j.Driver): The Neo4j driver. + relationship_id (str): The relationship ID of the relationship to find. + + Returns: + The matched relationship. + """ + return session.read_transaction(_find_relationship_by_relationship_id, relationship_id) + +def _find_relationship_by_relationship_id(tx, relationship_id): + query = """ + MATCH ()-[r]->() + WHERE id(r) = $relationship_id + """ + # logging.query(f"Running query: {query}") + print(f"Running query: {query}") + result = tx.run(query, relationship_id=relationship_id) + record = result.single() # Get the single result record, if any + return record[0] if record is not None else None # Handle no record found + +# Function to find nodes in Neo4j database by label +def find_nodes_by_label(session, label): + """ + Function to find nodes in Neo4j database by label. + + Args: + driver (neo4j.Driver): The Neo4j driver. + label (str): The label of the nodes to find. + + Example usage: + find_nodes_by_label(neo4j_driver, "Topic") + + Returns: + List of matched nodes. + """ + return session.read_transaction(_find_nodes_by_label, label) + +def _find_nodes_by_label(tx, label): + query = f""" + MATCH (n:{label}) + RETURN n + """ + # logging.query(f"Running query: {query}") + print(f"Running query: {query}") + result = tx.run(query) + return [record["n"] for record in result] + +def get_node_by_unique_id(session, unique_id): + return session.read_transaction(_get_node_by_unique_id, unique_id) + +def _get_node_by_unique_id(tx, unique_id): + query = f""" + MATCH (n) + WHERE n.unique_id = $unique_id + RETURN n + """ + logging.debug(f"Executing query with unique_id: {unique_id}") + result = tx.run(query, unique_id=unique_id) + record = result.single() + if record is None: + logging.warning(f"No node found with unique_id: {unique_id}") + return None + return record[0] + +# Function to find nodes in Neo4j database by label and properties +def find_nodes_by_label_and_properties(session, label, properties): + """ + Function to find nodes in Neo4j database by label and properties. + + Args: + session (neo4j.Session): The Neo4j session. + label (str): The label of the nodes to find. + properties (dict): A dictionary of properties to match. + + Returns: + List of matched nodes. + """ + logging.debug(f"Finding nodes with label: {label} and properties: {properties}") + with session: + response = session.read_transaction(_find_nodes_by_label_and_properties, label, properties) + logging.debug(f"Response: {response}") + return response + +def _find_nodes_by_label_and_properties(tx, label, properties): + query = f""" + MATCH (n:{label}) + WHERE {' AND '.join([f'n.{key} = ${key}' for key in properties.keys()])} + RETURN n + """ + logging.debug(f"Running query: {query}") + result = tx.run(query, **properties) + logging.debug(f"Result: {result}") + return [record["n"] for record in result] + +# Function to find relationships in Neo4j database by type +def find_relationships_by_type(session, rel_type): + """ + Function to find relationships in Neo4j database by type. + + Args: + driver (neo4j.Driver): The Neo4j driver. + rel_type (str): The type of the relationships to find. + + Returns: + List of matched relationships. + """ + return session.read_transaction(_find_relationships_by_type, rel_type) + +def _find_relationships_by_type(tx, rel_type): + query = f""" + MATCH ()-[r:{rel_type}]->() + RETURN r + """ + # logging.query(f"Running query: {query}") + print(f"Running query: {query}") + result = tx.run(query) + return [record["r"] for record in result] + +# Function to find relationships in Neo4j database by type and properties +def find_relationships_by_type_and_properties(session, label, properties): + """ + Function to find relationships in Neo4j database by type and properties. + + Args: + driver (neo4j.Driver): The Neo4j driver. + rel_type (str): The type of the relationships to find. + properties (dict): A dictionary of properties to match. + + Returns: + List of matched relationships. + """ + return session.read_transaction(_find_relationships_by_type_and_properties, label, properties) + + +def _find_relationships_by_type_and_properties(tx, label, properties): + query = f""" + MATCH (a)-[r:{label}]->(b) + WHERE {' AND '.join([f'r.{key} = ${key}' for key in properties.keys()])} + RETURN r + """ + # logging.query(f"Running query: {query}") + print(f"Running query: {query}") + result = tx.run(query, **properties) + return [record["r"] for record in result] + +# Function to find nodes and relationships in Neo4j database by label and properties +def find_nodes_and_relationships_by_label_and_properties(session, label, properties): + """ + Function to find nodes and relationships in Neo4j database by label and properties. + + Args: + driver (neo4j.Driver): The Neo4j driver. + label (str): The label of the nodes to find. + properties (dict): A dictionary of properties to match. + + Returns: + List of matched nodes and relationships. + """ + return session.read_transaction(_find_nodes_and_relationships_by_label_and_properties, label, properties) + +def _find_nodes_and_relationships_by_label_and_properties(tx, label, properties): + query = f""" + MATCH (n:{label}) + WHERE {' AND '.join([f'n.{key} = ${key}' for key in properties.keys()])} + RETURN n + """ + # logging.query(f"Running query: {query}") + result = tx.run(query, **properties) + return [record["n"] for record in result] + +# Function to delete nodes in Neo4j based on given criteria +def delete_nodes(session, criteria, delete_related=False): + """ + Function to delete nodes in Neo4j based on given criteria. + + Args: + driver (neo4j.Driver): The Neo4j driver. + criteria (dict): A dictionary containing the properties to match for deletion. + delete_related (bool): If True, deletes related nodes and relationships; otherwise, deletes only the matched nodes. + + Example usage: + # Delete only the nodes matching the criteria + delete_nodes(neo4j_driver, {'TopicID': 'AP.PAG10'}) + + # Delete the nodes and their related relationships + delete_nodes(neo4j_driver, {'TopicID': 'AP.PAG10'}, delete_related=True) + """ + session.write_transaction(_delete_nodes, criteria, delete_related) + +def _delete_nodes(tx, criteria, delete_related=False): + """ + Internal function to execute a Cypher query to delete nodes based on criteria. + + Args: + tx (neo4j.Transaction): The Neo4j transaction. + criteria (dict): A dictionary containing the properties to match for deletion. + delete_related (bool): Specifies whether to delete related nodes and relationships. + """ + condition_str = " AND ".join([f"n.{key} = ${key}" for key in criteria]) + if delete_related: + query = f""" + MATCH (n)-[r]-() + WHERE {condition_str} + DELETE n, r + """ + else: + query = f""" + MATCH (n) + WHERE {condition_str} + DELETE n + """ + # logging.query(f"Running query: {query}") + tx.run(query, **criteria) + +# Function to delete all nodes and relationships in the Neo4j database in batches +def delete_lots_of_nodes_and_relationships(session): + """ + Function to delete all nodes and relationships in the Neo4j database in batches. + + Args: + driver (neo4j.Driver): The Neo4j driver. + session (str): The Neo4j session. + """ + total_deleted = 0 + while True: + deleted_count = session.write_transaction(_delete_batch) + total_deleted += deleted_count + if deleted_count == 0: + break # Exit the loop if no more nodes are deleted + # logging.prod(f"All nodes and relationships have been deleted. Total deleted: {total_deleted}") + print(f"Neo4j: All nodes and relationships have been deleted. Total deleted: {total_deleted}") + +def _delete_batch(tx): + """ + Function to execute a Cypher query to delete a batch of nodes and relationships. + + Args: + tx (neo4j.Transaction): The Neo4j transaction. + """ + batch_size = 10000 # Adjust the batch size according to your needs + query = """ + MATCH (n) + WITH n LIMIT $batch_size + DETACH DELETE n + RETURN count(*) + """ + # logging.query(f"Running query: {query}") + result = tx.run(query, batch_size=batch_size) + result_data = result.single() + + if result_data is None: + return 0 + deleted_count = result_data[0] + if deleted_count is None: # This check might be redundant, but kept for clarity + return 0 + if deleted_count > 0: + # logging.database(f"Deleted {deleted_count} nodes.") + print(f"Neo4j: Deleted {deleted_count} nodes.") + return deleted_count + +def delete_all_constraints(session): + # Correct command to fetch all constraints for Neo4j 4.x and later + constraints_query = "SHOW CONSTRAINTS" + # logging.query(f"Running query: {constraints_query}") + if constraints_query_result := session.run(constraints_query).data(): + for constraint in constraints_query_result: + # Ensure correct key is used to extract constraint name + constraint_name = constraint['name'] # Adjust this if necessary + drop_query = f"DROP CONSTRAINT {constraint_name}" + # logging.query(f"Running query: {drop_query}") + session.run(drop_query) + # logging.database(f"Dropped constraint: {constraint_name}") + print(f"Neo4j: Dropped constraint: {constraint_name}") + else: + # logging.warning("No constraints found to delete.") + print("Neo4j: No constraints found to delete.") + +def reset_all_indexes(session): + indexes = session.run("SHOW INDEXES").data() + for index in indexes: + index_name = index['name'] + session.run(f"DROP INDEX {index_name}") + # logging.info(f"Deleted index: {index_name}") + print(f"Neo4j: Deleted index: {index_name}") + +def reset_database_in_session(session): + logging.debug("Neo4j: Resetting database") + delete_lots_of_nodes_and_relationships(session) + delete_all_constraints(session) + reset_all_indexes(session) + logging.info("Neo4j: Database reset") + +def create_database(session, db_name): + """ + Creates a new database in Neo4j if it does not already exist. + + Args: + session (neo4j.Session): The Neo4j session. + db_name (str): The name of the database to create. + """ + logging.debug(f"Neo4j: Creating database {db_name}") + query = f"CREATE DATABASE `{db_name}` IF NOT EXISTS" + try: + session.run(query) + logging.info(f"Neo4j: Database {db_name} created successfully.") + except Exception as e: + logging.error(f"Neo4j: Failed to create database {db_name}: {str(e)}") diff --git a/modules/database/tools/neontology/__init__.py b/modules/database/tools/neontology/__init__.py new file mode 100644 index 0000000..dcefa25 --- /dev/null +++ b/modules/database/tools/neontology/__init__.py @@ -0,0 +1,18 @@ +# flake8: noqa + +from .basenode import BaseNode +from .baserelationship import BaseRelationship +from .graphconnection import GraphConnection, init_neontology +from .utils import auto_constrain + +__all__ = [ + # BaseNode + "BaseNode", + # BaseRelationship + "BaseRelationship", + # GraphConnection + "init_neontology", + "GraphConnection", + # utils + "auto_constrain", +] diff --git a/modules/database/tools/neontology/__pycache__/__init__.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f45e8b1 Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/__pycache__/basenode.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/basenode.cpython-311.pyc new file mode 100644 index 0000000..93a6eac Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/basenode.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/__pycache__/baserelationship.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/baserelationship.cpython-311.pyc new file mode 100644 index 0000000..d89a4d5 Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/baserelationship.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/__pycache__/commonmodel.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/commonmodel.cpython-311.pyc new file mode 100644 index 0000000..cd88a3e Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/commonmodel.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/__pycache__/graphconnection.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/graphconnection.cpython-311.pyc new file mode 100644 index 0000000..5ba929c Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/graphconnection.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/__pycache__/result.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/result.cpython-311.pyc new file mode 100644 index 0000000..ff0a140 Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/result.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/__pycache__/utils.cpython-311.pyc b/modules/database/tools/neontology/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..fedd301 Binary files /dev/null and b/modules/database/tools/neontology/__pycache__/utils.cpython-311.pyc differ diff --git a/modules/database/tools/neontology/basenode.py b/modules/database/tools/neontology/basenode.py new file mode 100644 index 0000000..3e3076a --- /dev/null +++ b/modules/database/tools/neontology/basenode.py @@ -0,0 +1,315 @@ +from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union + +import numpy as np +import pandas as pd + +from .commonmodel import CommonModel +from .graphconnection import GraphConnection + +B = TypeVar("B", bound="BaseNode") + +class BaseNode(CommonModel): # pyre-ignore[13] + __primaryproperty__: ClassVar[str] + __primarylabel__: ClassVar[Optional[str]] + __secondarylabels__: ClassVar[Optional[list]] = [] + + def __init__(self, **data: dict): + super().__init__(**data) + + # we can define 'abstract' nodes which don't have a label + # these are to provide common properties to be used by subclassed nodes + # but shouldn't be put in the graph or even instantiated + if self.__primarylabel__ is None: + raise NotImplementedError( + "Nodes to be used in the graph must define a primary label." + ) + + def _get_merge_parameters(self) -> Dict[str, Any]: + """ + + Returns: + Dict[str, Any]: a dictionary of key/value pairs. + """ + + params = { + "pp": self.neo4j_dict()[self.__primaryproperty__], + "always_set": self._get_prop_values(self._always_set), + "set_on_match": self._get_prop_values(self._set_on_match), + "set_on_create": self._get_prop_values(self._set_on_create), + } + + return params + + def get_primary_property_value(self) -> Union[str, int]: + return self._get_merge_parameters()["pp"] + + def create(self, database: str = 'neo4j') -> None: + """Create this node in the graph.""" + + params = self.neo4j_dict() + + all_props = self.neo4j_dict() + + pp_value = all_props.pop(self.__primaryproperty__) + + params = {"pp": pp_value, "all_props": all_props} + + all_labels = [self.__primarylabel__] + self.__secondarylabels__ + + cypher = f""" + CREATE (n:{":".join(all_labels)} {{ {self.__primaryproperty__}: $pp }}) + SET n += $all_props + RETURN n + """ + graph = GraphConnection() + with graph.driver.session(database=database) as session: + result = session.run(cypher, params).single() + if result: + return self.__class__(**dict(result["n"])) + return None + + def merge(self, database: str = 'neo4j') -> None: + """Merge this node into the graph.""" + + params = self._get_merge_parameters() + + all_labels = [self.__primarylabel__] + self.__secondarylabels__ + + cypher = f""" + MERGE (n:{":".join(all_labels)} {{ {self.__primaryproperty__}: $pp }}) + ON MATCH SET n += $set_on_match + ON CREATE SET n += $set_on_create + SET n += $always_set + RETURN n + """ + + graph = GraphConnection() + with graph.driver.session(database=database) as session: + result = session.run(cypher, params).single() + if result: + return self.__class__(**dict(result["n"])) + return None + + @classmethod + def create_nodes(cls: Type[B], nodes: List[B]) -> List[Union[str, int]]: + """Create the given nodes in the database. + + Args: + nodes (List[B]): A list of nodes to create. + + Returns: + list: A list of the primary property values + + Raises: + TypeError: Raised if one of the nodes isn't of this type. + """ + + for node in nodes: + if isinstance(node, cls) is False: + raise TypeError("Node was incorrect type.") + + node_list = [ + {"props": x.neo4j_dict(), "pp": x.neo4j_dict()[cls.__primaryproperty__]} + for x in nodes + ] + + all_labels = [cls.__primarylabel__] + cls.__secondarylabels__ + + cypher = f""" + UNWIND $node_list AS node + create (n:{":".join(all_labels)} {{{cls.__primaryproperty__}: node.pp}}) + SET n = node.props + RETURN n + """ + + graph = GraphConnection() + results = graph.cypher_write_many( + cypher=cypher, params={"node_list": node_list} + ) + + matched_nodes = [cls(**dict(x["n"])) for x in results] + + return matched_nodes + + @classmethod + def merge_nodes(cls: Type[B], nodes: List[B]) -> List[B]: + """Merge multiple nodes into the database. + + Args: + nodes (List[B]): A list of nodes to merge. + + Returns: + list: A list of the primary property values + + Raises: + TypeError: Raised if any of the nodes provided don't match this class. + """ + + for node in nodes: + if isinstance(node, cls) is False: + raise TypeError("Node was incorrect type.") + + node_list = [x._get_merge_parameters() for x in nodes] + + all_labels = [cls.__primarylabel__] + cls.__secondarylabels__ + + cypher = f""" + UNWIND $node_list AS node + MERGE (n:{":".join(all_labels)} {{{cls.__primaryproperty__}: node.pp}}) + ON MATCH SET n += node.set_on_match + ON CREATE SET n += node.set_on_create + SET n += node.always_set + RETURN n + """ + + graph = GraphConnection() + results = graph.cypher_write_many( + cypher=cypher, params={"node_list": node_list} + ) + + matched_nodes = [cls(**dict(x["n"])) for x in results] + + return matched_nodes + + @classmethod + def merge_records(cls: Type[B], records: dict) -> List[B]: + """Take a list of dictionaries and use them to merge in nodes in the graph. + + Each dictionary will be used to merge a node where dictionary key/value pairs + represent properties to be applied. + + Returns: + list: A list of the primary property values + + Args: + records (List[Dict[str, Any]]): a list of dictionaries of node properties + """ + + nodes = [cls(**x) for x in records] + + return cls.merge_nodes(nodes) + + @classmethod + def merge_df(cls: Type[B], df: pd.DataFrame, deduplicate: bool = True) -> pd.Series: + """Merge in new nodes based on data in a dataframe. + + The dataframe columns must correspond to the Node properties. + + Returns: + pd.Series: A list of the primary property values + + Args: + df (pd.DataFrame): A pandas dataframe of node properties + + """ + + if df.empty is True: + return pd.Series(dtype=object) + + input_df = df.replace([np.nan], None).copy() + + if deduplicate is True: + # we don't wan't to waste time attempting to merge identical records + unique_df = input_df.drop_duplicates(ignore_index=True).copy() + else: + unique_df = input_df + + records = unique_df.to_dict(orient="records") + + unique_df["generated_nodes"] = pd.Series(cls.merge_records(records)) + + # now we need to get the mapping from unique id to primary property + # so that we can return the data in the same shape it was received + input_df.insert(0, "ontolocy_merging_order", range(0, len(input_df))) + merge_cols = list(input_df.columns) + merge_cols.remove("ontolocy_merging_order") + output_df = input_df.merge( + unique_df, + how="inner", + on=merge_cols, + ).sort_values("ontolocy_merging_order", ignore_index=True) + + return output_df.generated_nodes + + @classmethod + def match(cls: Type[B], pp: str) -> Optional[B]: + """MATCH a single node of this type with the given primary property. + + Args: + pp (str): The value of the primary property (pp) to match on. + + Returns: + Optional[B]: If the node exists, return it as an instance. + """ + + cypher = f""" + MATCH (n:{cls.__primarylabel__}) + WHERE n.{cls.__primaryproperty__} = $pp + RETURN n + """ + + params = {"pp": pp} + + graph = GraphConnection() + + result = graph.cypher_read(cypher, params) + + if result: + return cls(**dict(result["n"])) + + else: + return None + + @classmethod + def delete(cls, pp: str) -> None: + """Delete a node from the graph. + + Match on label and the pp value provided. + If the node exists, delete it and any relationships it has. + + Args: + pp (str): Primary property value to match on. + """ + + cypher = f""" + MATCH (n:{cls.__primarylabel__}) + WHERE n.{cls.__primaryproperty__} = $pp + DETACH DELETE n + """ + + params = {"pp": pp} + + graph = GraphConnection() + + graph.cypher_write(cypher, params) + + @classmethod + def match_nodes(cls: Type[B], limit: int = 100, skip: int = 0) -> List[B]: + """Get nodes of this type from the database. + + Run a MATCH cypher query to retrieve any Nodes with the label of this class. + + Args: + limit (int, optional): Maximum number of results to return. Defaults to 100. + skip (int, optional): Skip through this many results (for pagination). Defaults to 0. + + Returns: + Optional[List[B]]: A list of node instances. + """ + + cypher = f""" + MATCH(n:{cls.__primarylabel__}) + RETURN n{{.*}} + ORDER BY n.created DESC + SKIP $skip + LIMIT $limit + """ + + params = {"skip": skip, "limit": limit} + + graph = GraphConnection() + records = graph.cypher_read_many(cypher, params) + + nodes = [cls(**dict(x["n"])) for x in records] + + return nodes diff --git a/modules/database/tools/neontology/baserelationship.py b/modules/database/tools/neontology/baserelationship.py new file mode 100644 index 0000000..321c528 --- /dev/null +++ b/modules/database/tools/neontology/baserelationship.py @@ -0,0 +1,305 @@ +"""Defines the BaseRelationship class. + +The BaseRelationship class is used for creating and matching on relationships in the graph. + + Typical usage example: + + class MyRel(BaseRelationship): + + __relationshiptype__: ClassVar[Optional[str]] = "MY_REL" + + source: SourceNode + target: TargetNode + + my_rel = MyRel(source=source_node, target=target_node) + my_rel.merge() + +""" + +from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar + +import numpy as np +import pandas as pd +from pydantic import PrivateAttr + +from modules.database.tools.neontology.graphconnection import GraphConnection + +from .basenode import BaseNode +from .commonmodel import CommonModel + +R = TypeVar("R", bound="BaseRelationship") + + +class BaseRelationship(CommonModel): # pyre-ignore[13] + source: BaseNode + target: BaseNode + + __relationshiptype__: ClassVar[Optional[str]] = None + + _merge_on: List[ + str + ] = PrivateAttr() # what relationship properties should we merge on + + def __init__(self, **data: dict): + super().__init__(**data) + + self._merge_on = self._get_prop_usage("merge_on") + + # we can define 'abstract' relationships which don't have a label + # these are to provide common properties to be used by subclassed relationships + # but shouldn't be put in the graph or even instantiated + if self.__relationshiptype__ is None: + raise NotImplementedError( + "Nodes to be used in the graph must define a primary label." + ) + + @classmethod + def get_relationship_type(cls) -> str: + """Get the relationship type to use for creating and matching this relationship. + + If __relationship__ has been specified, use that. + + Otherwise use the class name in uppercase + + Returns: + str: the string to use for creating and matching this relationship + """ + return cls.__relationshiptype__ # pyre-ignore[7] + + def _get_merge_parameters( + self, source_prop: str, target_prop: str + ) -> Dict[str, Any]: + """ + + Returns: + Dict[str, Any]: a dictionary of key/value pairs. + """ + + exclusions = {"source", "target"} + + # these properties will be referenced individually + merge_props = self._get_prop_values(self._merge_on, exclude=exclusions) + + params = { + "source_prop": self.source.neo4j_dict()[source_prop], + "target_prop": self.target.neo4j_dict()[target_prop], + "always_set": self._get_prop_values(self._always_set, exclude=exclusions), + "set_on_match": self._get_prop_values( + self._set_on_match, exclude=exclusions + ), + "set_on_create": self._get_prop_values( + self._set_on_create, exclude=exclusions + ), + **merge_props, + } + + return params + + def merge( + self, + database: Optional[str] = 'neo4j' # default to 'neo4j' if not specified + ) -> None: + """Merge this relationship into the database.""" + source_label = self.source.__primarylabel__ + target_label = self.target.__primarylabel__ + + source_pp = self.source.__primaryproperty__ + target_pp = self.target.__primaryproperty__ + + params = self._get_merge_parameters( + source_prop=source_pp, target_prop=target_pp + ) + + rel_type = self.get_relationship_type() + + # build a string of properties to merge on "prop_name: $prop_name" + merge_props = ", ".join([f"{x}: ${x}" for x in self._merge_on]) + + cypher = f""" + MATCH (source:{source_label} {{ {source_pp}: $source_prop }}), + (target:{target_label} {{ {target_pp}: $target_prop }}) + MERGE (source)-[r:{rel_type} {{ {merge_props} }}]->(target) + ON MATCH SET r += $set_on_match + ON CREATE SET r += $set_on_create + SET r += $always_set + """ + + graph = GraphConnection() + # Use session with database instead of USE statement + with graph.driver.session(database=database) as session: + session.run(cypher, params) + + @classmethod + def merge_relationships( + cls: Type[R], + rels: List[R], + source_type: Optional[Type[BaseNode]] = None, + target_type: Optional[Type[BaseNode]] = None, + source_prop: Optional[str] = None, + target_prop: Optional[str] = None, + database: Optional[str] = 'neo4j' # Add database parameter + ) -> None: + """Merge multiple relationships (of this type) into the database. + + Sometimes the source and target label may be ambiguous (e.g. where we have subclassed nodes) + In this case you can explicitly pass in the relevant types + + Sometimes we want to match nodes on a property which isn't the primary property, + so we can specify what property to use. + + Args: + cls (Type[R]): this class + rels (List[R]): a list of relationships which are instances of this class + database (Optional[str]): database to use for the operation + + Raises: + TypeError: If relationships are provided which aren't of this class + """ + + if source_type is None: + source_type = cls.model_fields["source"].annotation + + if target_type is None: + target_type = cls.model_fields["target"].annotation + + for rel in rels: + if isinstance(rel, cls) is False: + raise TypeError("Relationship was incorrect type.") + if type(rel.source) is not source_type: + raise TypeError("Received an inappropriate kind of source node.") + if type(rel.target) is not target_type: + raise TypeError("Received an inappropriate kind of target node.") + + if source_prop is None: + source_prop = source_type.__primaryproperty__ + + if target_prop is None: + target_prop = target_type.__primaryproperty__ + + source_label = source_type.__primarylabel__ + target_label = target_type.__primarylabel__ + + # build a string of properties to merge on "prop_name: $prop_name" + # we need to instantiate the class so that _merge_on is generated as part of __init__ + merge_props = ", ".join([f"{x}: ${x}" for x in cls._get_prop_usage("merge_on")]) + + rel_list: List[Dict[str, Any]] = [ + x._get_merge_parameters(source_prop, target_prop) for x in rels + ] + + rel_type = cls.get_relationship_type() + + cypher = f""" + UNWIND $rel_list AS rel + MATCH (source:{source_label}) + WHERE source.{source_prop} = rel.source_prop + MATCH (target:{target_label}) + WHERE target.{target_prop} = rel.target_prop + MERGE (source)-[r:{rel_type} {{ {merge_props} }}]->(target) + ON MATCH SET r += rel.set_on_match + ON CREATE SET r += rel.set_on_create + SET r += rel.always_set + """ + + graph = GraphConnection() + # Use session with database instead of USE statement + with graph.driver.session(database=database) as session: + session.run(cypher=cypher, parameters={"rel_list": rel_list}) + + @classmethod + def merge_records( + cls: Type[R], + records: List[Dict[str, Any]], + source_type: Optional[Type[BaseNode]] = None, + target_type: Optional[Type[BaseNode]] = None, + source_prop: Optional[str] = None, + target_prop: Optional[str] = None, + ) -> None: + """Take a list of dictionaries and use them to merge in relationships in the graph. + + Sometimes, a relationship can accept nodes which subclass a particular node type. + In these instances, it may be necessary to explicitly state what type of node should be used. + + Each record should have a source and target key where the value is the primary property + value of the respective nodes. + + Args: + records (List[Dict[str, Any]]): a list of dictionaries used to populate relationships + source_type: explicitly state the class to use for source node + target_type: explicitly state the class to use for target node + """ + + hydrated_list = [] + + if source_type is None: + source_type = cls.model_fields["source"].annotation + + if target_type is None: + target_type = cls.model_fields["target"].annotation + + if source_prop is None: + source_prop = source_type.__primaryproperty__ + + if target_prop is None: + target_prop = target_type.__primaryproperty__ + + for record in records: + hydrated = dict(record) + + hydrated["source"] = source_type.model_construct( + **{source_prop: record["source"]} + ) + hydrated["target"] = target_type.model_construct( + **{target_prop: record["target"]} + ) + + hydrated_list.append(hydrated) + + rels = [cls(**x) for x in hydrated_list] + + cls.merge_relationships( + rels, + source_type=source_type, + source_prop=source_prop, + target_type=target_type, + target_prop=target_prop, + ) + + @classmethod + def merge_df( + cls: Type[R], + df: pd.DataFrame, + source_type: Optional[Type[BaseNode]] = None, + target_type: Optional[Type[BaseNode]] = None, + source_prop: Optional[str] = None, + target_prop: Optional[str] = None, + ) -> None: + """Merge in relationships based on data in a pandas data frame + + Expects columns named 'source' and 'target' with the primary property value + for the source and target nodes. + + Then additional fields should have a corresponding column. + + Args: + df (pd.DataFrame): pandas dataframe where each row represents a relationship to merge + """ + + if df.empty is False: + records = df.replace([np.nan], None).to_dict(orient="records") + cls.merge_records( + records, + source_type=source_type, + source_prop=source_prop, + target_type=target_type, + target_prop=target_prop, + ) + + @classmethod + def to_dict(cls): + return { + "source": cls.source.to_dict(), + "target": cls.target.to_dict(), + "relationship_type": cls.__relationshiptype__ + } + diff --git a/modules/database/tools/neontology/commonmodel.py b/modules/database/tools/neontology/commonmodel.py new file mode 100644 index 0000000..7173f10 --- /dev/null +++ b/modules/database/tools/neontology/commonmodel.py @@ -0,0 +1,196 @@ +from abc import ABC, abstractmethod +from datetime import date, datetime, time, timedelta +from typing import Any, ClassVar, Dict, List, Optional, Set + +from neo4j.time import Date as Neo4jDate +from neo4j.time import DateTime as Neo4jDateTime +from neo4j.time import Time as Neo4jTime +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PrivateAttr, + field_validator, + model_validator, +) + + +class CommonModel(BaseModel, ABC): + model_config = ConfigDict( + validate_assignment=True, + extra="forbid", + arbitrary_types_allowed=True, + ) + + created: datetime = Field( + default_factory=datetime.now, json_schema_extra={"set_on_create": True} + ) + merged: Optional[datetime] = Field(default=None, validate_default=True) + + _set_on_match: List[str] = PrivateAttr() + _set_on_create: List[str] = PrivateAttr() + _always_set: List[str] = PrivateAttr() + + _neo4j_supported_types: ClassVar[Any] = ( + list, + bool, + int, + bytearray, + float, + str, + bytes, + date, + time, + datetime, + timedelta, + ) + + def __init__(self, **data: dict): + super().__init__(**data) + + self._set_on_match = self._get_prop_usage("set_on_match") + self._set_on_create = self._get_prop_usage("set_on_create") + self._always_set = [ + x + for x in self.model_dump().keys() + if x not in self._set_on_match + self._set_on_create + ["source", "target"] + ] + + @classmethod + def _get_prop_usage(cls, usage_type: str) -> List[str]: + all_props = cls.model_json_schema()["properties"] + + selected_props = [] + + for prop, entry in all_props.items(): + if entry.get(usage_type) is True: + selected_props.append(prop) + + return selected_props + + def _get_prop_values( + self, props: List[str], exclude: Set[str] = set() + ) -> Dict[str, Any]: + """ + + Returns: + Dict[str, Any]: a dictionary of key/value pairs. + """ + + prop_values = { + k: v for k, v in self.neo4j_dict(exclude=exclude).items() if k in props + } + + return prop_values + + @abstractmethod + def _get_merge_parameters(self) -> Dict[str, Any]: + raise NotImplementedError + + @classmethod + def export_type_converter(cls, value: Any) -> Any: + if isinstance(value, dict): + raise TypeError("Neo4j doesn't support dict types for properties.") + + elif isinstance(value, (tuple, set)): + new_value = list(value) + return cls.export_type_converter(new_value) + + elif isinstance(value, list): + # items in a list must all be the same type + item_type = type(value[0]) + for item in value: + if isinstance(item, item_type) is False: + raise TypeError( + "For neo4j, all items in a list must be of the same type." + ) + + return [cls.export_type_converter(x) for x in value] + + elif isinstance(value, cls._neo4j_supported_types) is False: + return str(value) + + else: + return value + + @classmethod + def _export_dict_converter(cls, original_dict: Dict[str, Any]) -> Dict[str, Any]: + """_summary_ + + Args: + export_dict (Dict[str, Any]): _description_ + + Returns: + Dict[str, Any]: _description_ + """ + + export_dict = original_dict.copy() + + for k, v in export_dict.items(): + export_dict[k] = cls.export_type_converter(v) + + return export_dict + + def neo4j_dict(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]: + """Return a dict made up of only types compatible with neo4j + + Returns: + dict: a dictionary export of this model instance + """ + + export_dict = self.model_dump(exclude_none=True, **kwargs) + + export_dict = self._export_dict_converter(export_dict) + + return export_dict + + # + # validators + # + + @field_validator("merged") + def set_merged_to_created( + cls, value: Optional[datetime], values: Dict[str, Any] + ) -> datetime: + """By default, set the 'merged' time equal to the 'created' time. + + If the 'merged' value has been explicitly set, this is preserved. + + Args: + value (Optional[datetime]): the value of the field. + values (Dict[str, Any]): a dictionary of field/value pairs set so far. + + Returns: + datetime: The merged datetime value. + """ + + if value is None: + return values.data["created"] + else: + return value + + @model_validator(mode="before") + @classmethod + def neo4j_datetime_to_native(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """Datetimes come back from Neo4j as a non standard DateTime type. + + We check for any values where that is the case and convert them to + native Python datetimes. + + See https://neo4j.com/docs/api/python-driver/4.4/temporal_types.html for further info. + + Args: + values (Dict[str, Any]): Dictionary of field/value pairs from pydantic. + + Returns: + Dict[str, Any]: Returns the dictionary, with any Neo4jDateTimes updated. + """ + + if not isinstance(values, dict): + raise ValueError + + for key in values: + if isinstance(values[key], (Neo4jDateTime, Neo4jDate, Neo4jTime)): + values[key] = values[key].to_native() + + return values diff --git a/modules/database/tools/neontology/graphconnection.py b/modules/database/tools/neontology/graphconnection.py new file mode 100644 index 0000000..9cc81c9 --- /dev/null +++ b/modules/database/tools/neontology/graphconnection.py @@ -0,0 +1,253 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_neontology_graphconnection' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from typing import Any, Dict, List, Optional + +from neo4j import GraphDatabase, Neo4jDriver +from neo4j import Record as Neo4jRecord +from neo4j import Result as Neo4jResult +from neo4j import Transaction as Neo4jTransaction + +from .result import NeontologyResult, neo4j_records_to_neontology_records + + +class GraphConnection(object): + """Class for managing connections to Neo4j.""" + + _instance = None + + def __new__( + cls, + neo4j_uri: Optional[str] = None, + neo4j_username: Optional[str] = None, + neo4j_password: Optional[str] = None, + ) -> "GraphConnection": + """Make sure we only have a single connection to the GraphDatabase. + + This connection then gets used by all instances. + + Args: + neo4j_uri (Optional[str], optional): Neo4j URI to connect to. Defaults to None. + neo4j_username (Optional[str], optional): Neo4j username. Defaults to None. + neo4j_password (Optional[str], optional): Neo4j password. Defaults to None. + + Returns: + GraphConnection: Instance of the connection + """ + + if cls._instance is None: + cls._instance = object.__new__(cls) + + if GraphConnection._instance: + try: + driver = GraphConnection._instance.driver = GraphDatabase.driver( # type: ignore + neo4j_uri, auth=(neo4j_username, neo4j_password) + ) + driver.verify_connectivity() + + from .utils import get_node_types, get_rels_by_type + + # capture all possible types of node and relationship + cls.global_nodes = get_node_types() + cls.global_rels = get_rels_by_type() + + except Exception as error: + logging.error( + "Error: connection not established. Have you run init_neontology? {}".format( + error + ) + ) + GraphConnection._instance = None + + else: + GraphConnection._instance = None + + return cls._instance + + def __del__(self) -> None: + """Close the driver gracefully when the class gets deleted.""" + + self.driver.close() + + def __init__( + self, + neo4j_uri: Optional[str] = None, + neo4j_username: Optional[str] = None, + neo4j_password: Optional[str] = None, + ) -> None: + if self._instance: + self.driver: Neo4jDriver = self._instance.driver + + def run_transaction_single( + self, tx: Neo4jTransaction, query: str, params: Dict[str, Any] + ) -> Optional[Neo4jRecord]: + """Run a transaction which is expected to return a single result. + + Args: + tx (Neo4jTransaction): Neo4j Transaction object + query (str): cypher query to run + params (Dict[str, Any]): Parameters to pass to the query + + Returns: + Optional[Neo4jRecord]: The result + """ + + return tx.run(query, **params).single() + + def run_transaction_many( + self, tx: Neo4jTransaction, query: str, params: Dict[str, Any] + ) -> List[Neo4jRecord]: + """Run a transation which is expected to return multiple nodes. + + Args: + tx (Neo4jTransaction): Neo4j Transaction object + query (str): cypher query to run + params (Dict[str, Any]): parameters to pass the query + + Returns: + List[Neo4jRecord]: a list of the results + """ + + return [record for record in tx.run(query, **params)] + + def cypher_write(self, cypher: str, params: Dict[str, Any] = {}) -> None: + """Execute a write transaction. + + Args: + cypher (str): cypher query + params (Dict[str, Any]): parameters to pass to the query + """ + + with self.driver.session() as session: + session.execute_write(self.run_transaction_single, cypher, params) + + def cypher_write_single(self, cypher: str, params: Dict[str, Any] = {}) -> None: + """Execute a write transaction. + + Args: + cypher (str): cypher query + params (Dict[str, Any]): parameters to pass to the query + """ + + with self.driver.session() as session: + return session.execute_write(self.run_transaction_single, cypher, params) + + def cypher_write_many(self, cypher: str, params: Dict[str, Any] = {}) -> None: + """Execute a write transaction. + + Args: + cypher (str): cypher query + params (Dict[str, Any]): parameters to pass to the query + """ + + with self.driver.session() as session: + return session.execute_write(self.run_transaction_many, cypher, params) + + def cypher_read( + self, cypher: str, params: Dict[str, Any] = {} + ) -> Optional[Neo4jRecord]: + """Run a cypher read only query which is expected to return a single result. + + Args: + cypher (str): cypher query string + params (Dict[str, Any]): parameters to pass to the query + + Returns: + Neo4jRecord: the resulting Neo4j 'Record', or None + """ + + with self.driver.session() as session: + return session.execute_read(self.run_transaction_single, cypher, params) + + def cypher_read_many( + self, cypher: str, params: Dict[str, Any] = {} + ) -> List[Neo4jRecord]: + """Run a cypher read query which will return multiple records. + + Args: + cypher (str): cypher string to run + params (Dict[str, Any]): parameters to pass to the query + + Returns: + List[Neo4jRecord]: A list of Neo4j 'Records' returned by the query. + """ + + with self.driver.session() as session: + return session.execute_read(self.run_transaction_many, cypher, params) + + def apply_constraint(self, label: str, property: str) -> None: + cypher = f""" + CREATE CONSTRAINT IF NOT EXISTS + FOR (n:{label}) + REQUIRE n.{property} IS UNIQUE + """ + + self.cypher_write(cypher) + + def evaluate_query_single(self, cypher, params={}): + result = self.driver.execute_query( + cypher, parameters_=params, result_transformer_=Neo4jResult.single + ) + + if result: + return result.value() + + else: + return None + + def evaluate_query(self, cypher, params={}): + result = self.driver.execute_query(cypher, parameters_=params) + + neo4j_records = result.records + neontology_records = neo4j_records_to_neontology_records( + neo4j_records, self.global_nodes, self.global_rels + ) + + return NeontologyResult( + records=neo4j_records, neontology_records=neontology_records + ) + + +def init_neontology( + neo4j_uri: Optional[str] = None, + neo4j_username: Optional[str] = None, + neo4j_password: Optional[str] = None, +) -> None: + """Initialise neontology. + + If connection properties are explicitly passed in, use these. + If not, attempt to load from enviornment variables (optionally in a .env file.) + + Args: + neo4j_uri (Optional[str], optional): Neo4j URI to connect to. Defaults to None. + neo4j_username (Optional[str], optional): Neo4j username. Defaults to None. + neo4j_password (Optional[str], optional): Neo4j password. Defaults to None. + """ + + # try to load environment variables from .env file + load_dotenv() + + if neo4j_uri is None: + neo4j_uri = os.getenv("NEO4J_URI") + + if neo4j_password is None: + neo4j_password = os.getenv("PASSWORD_NEO4J") + + if neo4j_username is None: + neo4j_username = os.getenv("USER_NEO4J") + + GraphConnection(neo4j_uri, neo4j_username, neo4j_password) + +def close_neontology(): + GraphConnection().__del__() \ No newline at end of file diff --git a/modules/database/tools/neontology/result.py b/modules/database/tools/neontology/result.py new file mode 100644 index 0000000..3caaaad --- /dev/null +++ b/modules/database/tools/neontology/result.py @@ -0,0 +1,114 @@ +import itertools +import warnings +from typing import List + +from neo4j import Record as Neo4jRecord +from neo4j.graph import Node as Neo4jNode +from neo4j.graph import Relationship as Neo4jRelationship +from pydantic import BaseModel, computed_field + + +def neo4j_records_to_neontology_records( + records: List[Neo4jRecord], node_classes: list, rel_classes: list +) -> list: + new_records = [] + + for record in records: + new_record = {"nodes": {}, "relationships": {}} + for key, entry in record.items(): + if isinstance(entry, Neo4jNode): + node_label = list(entry.labels)[0] + + # gracefully handle cases where we don't have a class defined + # for the identified label + try: + node = node_classes[node_label](**dict(entry)) + new_record["nodes"][key] = node + except KeyError: + warnings.warn( + ( + f"Could not find a class for {node_label} label." + " Did you define the class before initializing Neontology?" + ) + ) + pass + + elif isinstance(entry, Neo4jRelationship): + rel_type = entry.type + + rel_dict = rel_classes[rel_type] + + if not rel_dict: + warnings.warn( + ( + f"Could not find a class for {rel_type} relationship type." + " Did you define the class before initializing Neontology?" + ) + ) + continue + + src_label = list(entry.nodes[0].labels)[0] + tgt_label = list(entry.nodes[1].labels)[0] + + src_node = node_classes[src_label](**dict(entry.nodes[0])) + tgt_node = node_classes[tgt_label](**dict(entry.nodes[1])) + + rel_props = dict(entry) + rel_props["source"] = src_node + rel_props["target"] = tgt_node + + rel = rel_dict["rel_class"](**rel_props) + + new_record["relationships"][key] = rel + + new_records.append(new_record) + + return new_records + + +class NeontologyResult(BaseModel): + records: list + neontology_records: list + + @computed_field + @property + def nodes(self) -> list: + nodes_list_of_lists = [x["nodes"].values() for x in self.neontology_records] + return list(itertools.chain.from_iterable(nodes_list_of_lists)) + + @computed_field + @property + def relationships(self) -> list: + nodes_list_of_lists = [ + x["relationships"].values() for x in self.neontology_records + ] + return list(itertools.chain.from_iterable(nodes_list_of_lists)) + + @computed_field + @property + def node_link_data(self) -> dict: + nodes = [ + { + "id": x.get_primary_property_value(), + "label": x.__primarylabel__, + "name": str(x), + } + for x in self.nodes + ] + + links = [ + { + "source": x.source.get_primary_property_value(), + "target": x.target.get_primary_property_value(), + } + for x in self.relationships + ] + + unique_nodes = list({frozenset(item.items()): item for item in nodes}.values()) + unique_links = list({frozenset(item.items()): item for item in links}.values()) + data = { + "nodes": unique_nodes, + "links": unique_links, + } + + return data diff --git a/modules/database/tools/neontology/utils.py b/modules/database/tools/neontology/utils.py new file mode 100644 index 0000000..893c62e --- /dev/null +++ b/modules/database/tools/neontology/utils.py @@ -0,0 +1,116 @@ +from collections import defaultdict +from typing import Dict, Set, Type + +from .basenode import BaseNode +from .baserelationship import BaseRelationship +from .graphconnection import GraphConnection + + +def get_node_types(base_type: Type[BaseNode] = BaseNode) -> Dict[str, Type[BaseNode]]: + node_types = {} + + for subclass in base_type.__subclasses__(): + # we can define 'abstract' nodes which don't have a label + # these are to provide common properties to be used by subclassed nodes + # but shouldn't be put in the graph + if ( + hasattr(subclass, "__primarylabel__") + and subclass.__primarylabel__ is not None + ): + node_types[subclass.__primarylabel__] = subclass + + if subclass.__subclasses__(): + subclass_node_types = get_node_types(subclass) + + node_types.update(subclass_node_types) + + return node_types + + +def get_rels_by_type( + base_type: Type[BaseRelationship] = BaseRelationship, +) -> Dict[str, dict]: + rel_types: dict = defaultdict(dict) + + for rel_subclass in base_type.__subclasses__(): + # we can define 'abstract' relationships which don't have a label + # these are to provide common properties to be used by subclassed relationships + # but shouldn't be put in the graph + if ( + hasattr(rel_subclass, "__relationshiptype__") + and rel_subclass.__relationshiptype__ is not None + ): + rel_types[rel_subclass.__relationshiptype__] = { + "rel_class": rel_subclass, + "source_class": rel_subclass.model_fields["source"].annotation, + "target_class": rel_subclass.model_fields["target"].annotation, + } + + if rel_subclass.__subclasses__(): + subclass_rel_types = get_rels_by_type(rel_subclass) + + rel_types.update(subclass_rel_types) + + return rel_types + + +def all_subclasses(cls: type) -> set: + return set(cls.__subclasses__()).union( + [s for c in cls.__subclasses__() for s in all_subclasses(c)] + ) + + +def get_rels_by_node( + base_type: Type[BaseRelationship] = BaseRelationship, by_source: bool = True +) -> Dict[str, Set[str]]: + if by_source is True: + node_dir = "source_class" + + else: + node_dir = "target_class" + + all_rels = get_rels_by_type(base_type) + + by_node: Dict[str, Set[str]] = defaultdict(set) + + for rel_type, entry in all_rels.items(): + try: + node_label = entry[node_dir].__primarylabel__ + except AttributeError: + node_label = None + + if node_label is not None: + by_node[node_label].add(rel_type) + + for node_subclass in all_subclasses(entry[node_dir]): + subclass_label = node_subclass.__primarylabel__ + if subclass_label is not None: + by_node[subclass_label].add(rel_type) + + return by_node + + +def get_rels_by_source( + base_type: Type[BaseRelationship] = BaseRelationship, +) -> Dict[str, Set[str]]: + return get_rels_by_node(by_source=True) + + +def get_rels_by_target( + base_type: Type[BaseRelationship] = BaseRelationship, +) -> Dict[str, Set[str]]: + return get_rels_by_node(by_source=False) + + +def auto_constrain() -> None: + """Automatically apply constraints + + Get information about all the defined nodes in the current environment. + + Apply constraints based on the primary label and primary property for each node. + """ + + graph = GraphConnection() + + for node_label, node_type in get_node_types().items(): + graph.apply_constraint(node_label, node_type.__primaryproperty__) diff --git a/modules/database/tools/neontology_tools.py b/modules/database/tools/neontology_tools.py new file mode 100644 index 0000000..87695e8 --- /dev/null +++ b/modules/database/tools/neontology_tools.py @@ -0,0 +1,121 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_database_tools_neontology_tools' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from modules.database.tools.neontology.graphconnection import init_neontology, close_neontology +from modules.database.tools.neontology.basenode import BaseNode +from modules.database.tools.neontology.baserelationship import BaseRelationship +from pydantic import ValidationError +import os +import neo4j + +# Initialize Neontology with the Neo4j database details +def init_neontology_connection(uri=None, user=None, password=None): + uri = uri or os.getenv("APP_BOLT_URL") + user = user or os.getenv("USER_NEO4J") # Add default value + password = password or os.getenv("PASSWORD_NEO4J") + + if not all([uri, user, password]): + raise ValueError("Missing required Neo4j connection parameters") + + try: + logging.info(f"Initializing Neontology with URI: {uri}") + init_neontology( + neo4j_uri=uri, + neo4j_username=user, + neo4j_password=password + ) + logging.info(f"Neontology connection initialized with URI: {uri}, user: {user}") + except Exception as e: + raise ValueError(f"Failed to initialize Neontology connection: {str(e)}") + +def close_neontology_connection(): + logging.debug(f"Attempting to terminate Neontology connection") + close_neontology() + logging.info(f"Neontology connection terminated") + +# Terminates the Neo4j connection +def close_neo4j_connection(): + logging.debug(f"Attempting to terminate Neo4j connection") + neo4j.close() + logging.info(f"Neo4j connection terminated") + +# Create a Neontology node in the Neo4j database +def create_or_merge_neontology_node(node: BaseNode, database: str = 'neo4j', operation: str = "merge"): + """ + Create or merge a Neontology node in the Neo4j database. + + Args: + node (BaseNode): A Neontology node object. + operation (str): The operation to perform ('create' or 'merge'). Defaults to 'merge'. + """ + try: + if operation == "create": + node.create(database=database) + elif operation == "merge": + node.merge(database=database) + else: + logging.error(f"Invalid operation: {operation}") + except Exception as e: + logging.error(f"Error in processing node: {e}") + +# Create or merge a Neontology node in the Neo4j database. If a ValidationError occurs +# due to a NaN value, replace it with a default value and retry. +def create_or_merge_neontology_node_with_default(driver, node: BaseNode, database: str = 'neo4j', operation: str = "merge", default_values: dict = {}): + """ + Create or merge a Neontology node in the Neo4j database. If a ValidationError occurs + due to a NaN value, replace it with a default value and retry. + + Args: + node (BaseNode): A Neontology node object. + operation (str): The operation to perform ('create' or 'merge'). Defaults to 'merge'. + default_values (dict): A dictionary of default values for fields that might contain NaN. + """ + try: + # Attempt to create or merge the node + if operation == "create": + node.create(database=database) + else: # "merge" by default + node.merge(database=database) + except ValidationError as e: + # Handle ValidationError due to NaN value + for field, error in e.errors(): + if field in default_values and 'type' in error and error['type'] == 'value_error.nan': + setattr(node, field, default_values[field]) + logging.warning(f"Warning: Replacing NaN in {field} with default value '{default_values[field]}' and retrying.") + create_or_merge_neontology_node_with_default(driver, node, database, operation, default_values) + break + else: + # If the error is not due to a NaN value or field not in default_values, re-raise the error + logging.error(f"Error in processing node: {e}") + raise + except Exception as e: + logging.error(f"Error in processing node: {e}") + +def create_or_merge_neontology_relationship(relationship: BaseRelationship, database: str = 'neo4j', operation: str = "merge"): + """ + Create or merge a Neontology relationship in the Neo4j database. + + Args: + relationship (BaseRelationship): A Neontology relationship object. + operation (str): The operation to perform ('create' or 'merge'). Defaults to 'merge'. + """ + try: + if operation == "create": + relationship.create(database=database) + elif operation == "merge": + relationship.merge(database=database) + else: + logging.error(f"Invalid operation: {operation}") + except Exception as e: + logging.error(f"Error in processing relationship: {e}") \ No newline at end of file diff --git a/modules/database/tools/queries.py b/modules/database/tools/queries.py new file mode 100644 index 0000000..c01d111 --- /dev/null +++ b/modules/database/tools/queries.py @@ -0,0 +1,25 @@ +def create_database(db_name): + return f"CREATE DATABASE `{db_name}` IF NOT EXISTS" + +def stop_database(db_name): + return f"STOP DATABASE `{db_name}`" + +def drop_database(db_name): + return f"DROP DATABASE `{db_name}`" + +show_constraints = "SHOW CONSTRAINTS" + +show_indexes = "SHOW INDEXES" + +def drop_index(index_name): + f"DROP INDEX {index_name}" + +def drop_constraint(constraint_name): + f"DROP CONSTRAINT {constraint_name}" + +delete_batch = """ + MATCH (n) + WITH n LIMIT $batch_size + DETACH DELETE n + RETURN count(*) + """ \ No newline at end of file diff --git a/modules/document_processor.py b/modules/document_processor.py new file mode 100644 index 0000000..dbe1ae6 --- /dev/null +++ b/modules/document_processor.py @@ -0,0 +1,94 @@ +from pathlib import Path +import subprocess +import tempfile +import os +from typing import Dict, List, Optional + +class DocumentProcessor: + def __init__(self): + self.supported_extensions = { + 'doc': 'libreoffice', + 'docx': 'libreoffice', + 'odt': 'libreoffice', + 'rtf': 'libreoffice', + 'txt': 'libreoffice', + 'html': 'libreoffice', + 'htm': 'libreoffice', + 'xls': 'libreoffice', + 'xlsx': 'libreoffice', + 'ppt': 'libreoffice', + 'pptx': 'libreoffice', + 'pdf': 'pdf' + } + + def convert_to_pdf(self, input_file: Path) -> bytes: + """ + Convert a document to PDF format + """ + if not input_file.exists(): + raise FileNotFoundError(f"Input file not found: {input_file}") + + input_extension = input_file.suffix.lower()[1:] # Remove the dot + if input_extension not in self.supported_extensions: + raise ValueError(f"Unsupported file extension: {input_extension}") + + if input_extension == 'pdf': + # If it's already a PDF, just read and return it + with open(input_file, 'rb') as f: + return f.read() + + # Use LibreOffice for conversion + with tempfile.TemporaryDirectory() as temp_dir: + output_file = Path(temp_dir) / f"{input_file.stem}.pdf" + + # Convert using LibreOffice + cmd = [ + 'libreoffice', + '--headless', + '--convert-to', 'pdf', + '--outdir', str(temp_dir), + str(input_file) + ] + + try: + subprocess.run(cmd, check=True, capture_output=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Conversion failed: {e.stderr.decode()}") + + if not output_file.exists(): + raise RuntimeError("Conversion failed: Output file not created") + + # Read and return the PDF content + with open(output_file, 'rb') as f: + return f.read() + + def batch_convert_directory(self, directory: str) -> List[Dict]: + """ + Convert all documents in a directory to PDF format + """ + directory_path = Path(directory) + if not directory_path.exists(): + raise FileNotFoundError(f"Directory not found: {directory}") + + results = [] + for file_path in directory_path.glob('*'): + if file_path.is_file() and file_path.suffix.lower()[1:] in self.supported_extensions: + try: + pdf_content = self.convert_to_pdf(file_path) + output_file = file_path.with_suffix('.pdf') + with open(output_file, 'wb') as f: + f.write(pdf_content) + + results.append({ + "source_file": str(file_path), + "output_file": str(output_file), + "status": "success" + }) + except Exception as e: + results.append({ + "source_file": str(file_path), + "status": "error", + "error": str(e) + }) + + return results \ No newline at end of file diff --git a/modules/langchain/__init__.py b/modules/langchain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/langchain/__pycache__/__init__.cpython-311.pyc b/modules/langchain/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..18e7ef6 Binary files /dev/null and b/modules/langchain/__pycache__/__init__.cpython-311.pyc differ diff --git a/modules/langchain/__pycache__/interactive_langgraph_query.cpython-311.pyc b/modules/langchain/__pycache__/interactive_langgraph_query.cpython-311.pyc new file mode 100644 index 0000000..7d07dd6 Binary files /dev/null and b/modules/langchain/__pycache__/interactive_langgraph_query.cpython-311.pyc differ diff --git a/modules/langchain/interactive_langgraph_query.py b/modules/langchain/interactive_langgraph_query.py new file mode 100644 index 0000000..d1a3fe1 --- /dev/null +++ b/modules/langchain/interactive_langgraph_query.py @@ -0,0 +1,510 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_interactive_langgraph_query' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from langchain_core.messages import BaseMessage, HumanMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from typing import Annotated, Sequence, List, TypedDict +from langchain_core.pydantic_v1 import BaseModel, Field +import operator +from langchain_openai import ChatOpenAI +import openai +import aiohttp +from urllib.parse import urlencode +from bs4 import BeautifulSoup +import asyncio +import re +from datetime import datetime +from extruct import extract +from w3lib.html import get_base_url +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity +from modules.redis_config import get_cached_results, set_cached_results + +# Explicitly set the OpenAI API key +openai_api_key = os.getenv("OPENAI_API_KEY") +if not openai_api_key or openai_api_key.lower() == "null": + raise ValueError("OPENAI_API_KEY is not set or is set to NULL in the environment variables") + +openai.api_key = openai_api_key +logging.info(f"OpenAI API Key: {openai_api_key[:5]}...{openai_api_key[-5:]}") + +from langgraph.constants import END, Send +from langgraph.graph import StateGraph +from langgraph.checkpoint.memory import MemorySaver + +simple_model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0, streaming=True) +advanced_model = ChatOpenAI(model="gpt-4o", temperature=0, streaming=True) + +class Queries(BaseModel): + """List of search queries""" + queries: List[str] = Field( + description="List of the generated search queries" + ) + +class SummaryState(TypedDict): + content: str + query: str + +class PerplexityClone: + ADDITIONAL_QUESTION_PROMPT = """You are tasked with analyzing a message to determine if it requires additional input from the user. + Your goal is to be conservative in asking for additional input, only indicating that more information is needed if it is relevant to answering the question or fulfilling the request in the message. + Use the following criteria to guide your decision: + - Is the core question or request clearly stated? + - Are all necessary details provided to understand the context? + - Would additional information significantly change or improve the response? + - Is the missing information essential or helpful? + + Before giving your final answer, think through your analysis in a scratchpad: + + + Analyze the message here, considering the criteria above. Think step-by-step about whether additional input is truly necessary or if the message can be responded to with the given information. + + + After your analysis, provide your final answer in the following format: + + + [YES/NO]: (Choose YES if additional input is required, NO if it is not) + Justification: (Briefly explain your reasoning) + + Remember to be conservative in asking for additional input. Only say YES if the additional information is highly relevant and necessary to properly address the message.""" + + SEARCH_QUERY_PROMPT = """You are a helpful AI assistant, create a list of 2-3 search queries based on the message""" + + FINAL_NODE_SYSTEM_PROMPT = """You are a helpful AI assistant, answer the given question based on the context. Clearly cite the sources for your answer including the links for the sources next to each point""" + + FINAL_NODE_PROMPT = """Question: {question} + Context: {context} + Answer:""" + + @staticmethod + def prioritize_content(extracted_text, query): + """Prioritize content based on relevance to the query.""" + documents = [extracted_text, query] + tfidf_vectorizer = TfidfVectorizer().fit_transform(documents) + cosine_matrix = cosine_similarity(tfidf_vectorizer[0:1], tfidf_vectorizer) + score = cosine_matrix[0][1] # Similarity score with the query + logging.debug(f"Content prioritization score: {score}") + return score + + # Define OverallState with class methods for graph nodes + class OverallState(TypedDict): + messages: Annotated[Sequence[BaseMessage], operator.add] + next: str + search_queries: list[str] + search_results: list[str] + page_content: list[str] + page_summaries: Annotated[list, operator.add] + needs_more_info: bool = False + + @classmethod + def additional_questions_node(cls, state): + logging.debug("Entering additional_questions_node") + messages = state['messages'] + last_message = messages[-1] + + prompt = ChatPromptTemplate.from_messages( + [ + ("system", PerplexityClone.ADDITIONAL_QUESTION_PROMPT), + MessagesPlaceholder(variable_name="messages"), + MessagesPlaceholder(variable_name="agent_scratchpad") + ] + ) + chain = prompt | simple_model + input_data = { + "messages": messages, + "agent_scratchpad": [] + } + logging.chat(f"additional_questions_node is sending data to model: {input_data}") + result = chain.invoke(input_data) + logging.chat(f"additional_questions_node received data from model: {result.content}") + needs_more_info = "YES" in result.content.upper() + return {"next": result.content, "needs_more_info": needs_more_info} + + @classmethod + def where_to_go(cls, state): + next = state['next'] + if "NO" in next: + return "proceed" + else: + return "ask" + + @classmethod + def ask_node(cls, state): + messages = state['messages'] + user_question = messages[0] + + prompt = f"Ask any additional questions that are required to answer the question: {user_question.content}" + logging.chat(f"ask_node is sending data to model: {prompt}") + question = simple_model.invoke(prompt) + logging.chat(f"ask_node received data from model: {question.content}") + return {"messages": [question]} + + @classmethod + def new_question_node(cls, state): + messages = state['messages'] + initial_question = messages[0] + + prompt = f"Reframe the initial question: {initial_question.content} based on the messages: {messages}" + logging.chat(f"new_question_node is sending data to model: {prompt}") + response = simple_model.invoke(prompt) + logging.chat(f"new_question_node received data from model: {response.content}") + new_question = HumanMessage(content=response.content) + return {"messages": [new_question]} + + @classmethod + def search_query_node(cls, state): + messages = state['messages'] + last_message = messages[-1] + + prompt = ChatPromptTemplate.from_messages( + [ + ("system", PerplexityClone.SEARCH_QUERY_PROMPT), + MessagesPlaceholder(variable_name="messages"), + ] + ) + chain = prompt | simple_model + logging.chat(f"search_query_node is sending data to model: {messages}") + result = chain.invoke(messages) + logging.chat(f"search_query_node received data from model: {result.content}") + + queries = [q.strip() for q in result.content.split('\n') if q.strip()] + return {"search_queries": queries} + + @classmethod + async def search_results_node(cls, state): + logging.debug("Entering search_results_node") + queries = state['search_queries'] + logging.debug(f"Queries: {queries}") + try: + results = await cls.search(queries) + logging.debug(f"Search results: {results}") + except Exception as e: + logging.error(f"Error in search_results_node: {str(e)}") + raise + return {"search_results": results} + + @classmethod + async def web_scrape_node(cls, state): + logging.debug("Entering web_scrape_node") + search_results = state['search_results'] + crawled_results = [] + + logging.debug(f"Search results: {search_results}") + for result_list in search_results: + for result in result_list: + logging.debug(f"Result: {result}") + url = result.get('url', 'No URL') + content = result.get('content', result.get('title', 'No content')) + logging.debug(f"Using crawler on: {url} with content: {content}") + + try: + crawled_content = await cls.crawl_page(result, state['messages'][-1].content, retries=3, timeout=20) + if crawled_content: + logging.debug(f"Crawled content: {crawled_content}") + crawled_results.append(crawled_content) + else: + logging.error(f"No content found for {url}") + except Exception as e: + logging.error(f"Error crawling {url}: {str(e)}") + + if not crawled_results: + logging.error("No crawled results found") + return {"page_content": [{"page_content": "No relevant content found.", "metadata": {}}]} # Adjusted to ensure it returns a dictionary + else: + logging.debug(f"Crawled results: {crawled_results}") + return {"page_content": [{"page_content": cr['content'], "metadata": {"source": cr['url']}} for cr in crawled_results]} + + @classmethod + async def crawl_page(cls, result, query, retries=3, timeout=20): + """Crawl a page with retries and timeouts.""" + for attempt in range(retries): + try: + async with aiohttp.ClientSession() as session: + async with session.get(result['url'], timeout=timeout) as response: + if response.status != 200: + raise Exception(f"HTTP status {response.status}") + html = await response.text() + + # Extract structured data + structured_data = await cls.extract_structured_data(html, result['url']) + if structured_data: + logging.debug(f"Structured data extracted: {structured_data}") + else: + logging.debug("No structured data found") + + soup = BeautifulSoup(html, 'html.parser') + + # Remove unnecessary elements + for element in soup(['script', 'style', 'nav', 'header', 'footer']): + element.decompose() + + main_content = ( + soup.find('main') or + soup.find('article') or + soup.find('.content') or + soup.find(id='content') or + soup.body + ) + + if main_content: + # Prioritize specific content elements + priority_elements = main_content.find_all(['h1', 'h2', 'h3', 'p']) + extracted_text = '\n\n'.join(el.get_text().strip() for el in priority_elements if el.get_text().strip()) + + # If not enough content, fall back to other elements + if len(extracted_text) < 500: + content_elements = main_content.find_all(['h4', 'h5', 'h6', 'li', 'td', 'th', 'blockquote', 'pre', 'code']) + extracted_text += '\n\n' + '\n\n'.join(el.get_text().strip() for el in content_elements if el.get_text().strip()) + + # Prioritize the extracted content based on relevance to the query + relevance_score = PerplexityClone.prioritize_content(extracted_text, query) + logging.debug(f"Content relevance score: {relevance_score}") + + # Extract metadata + meta_description = soup.find('meta', attrs={'name': 'description'}) + meta_keywords = soup.find('meta', attrs={'name': 'keywords'}) + og_title = soup.find('meta', property='og:title') + og_description = soup.find('meta', property='og:description') + + # Combine metadata with extracted text + metadata = [ + result['title'], + og_title['content'] if og_title else '', + meta_description['content'] if meta_description else '', + og_description['content'] if og_description else '', + meta_keywords['content'] if meta_keywords else '', + ] + extracted_text = '\n\n'.join(filter(None, metadata + [extracted_text])) + + # Limit the extracted text to 10000 characters + extracted_text = extracted_text[:10000] + + # Highlight query terms in the content + highlighted_content = cls.highlight_query_terms(extracted_text, query) + + # Extract publication date + published_date = cls.extract_publication_date(soup) + + return { + 'title': result['title'], + 'url': result['url'], + 'content': highlighted_content, + 'structured_data': structured_data, + 'relevance_score': relevance_score, + 'publishedDate': published_date.isoformat() if published_date else None + } + else: + logging.debug("No main content found") + + except asyncio.TimeoutError: + logging.warning(f"Timeout occurred while crawling {result['url']}. Attempt {attempt + 1} of {retries}") + except Exception as error: + logging.error(f"Error crawling {result['url']}: {str(error)}. Attempt {attempt + 1} of {retries}") + + # If all retries fail, return a default response + logging.debug("All retries failed. Returning default response.") + return { + 'title': result['title'], + 'url': result['url'], + 'content': result.get('content', 'Content unavailable due to crawling error.'), + 'structured_data': None, + 'relevance_score': None, + 'publishedDate': None + } + + @staticmethod + async def extract_structured_data(html, url): + """Extract structured data (e.g., JSON-LD) from a webpage.""" + logging.debug(f"Extracting structured data from: {url}") + base_url = get_base_url(html, url) + data = extract(html, base_url=base_url) + logging.debug(f"Structured data extracted: {data}") + return data + + @staticmethod + def highlight_query_terms(text, query): + words = query.lower().split() + for word in words: + pattern = re.compile(r'\b' + re.escape(word) + r'\b', re.IGNORECASE) + text = pattern.sub(f'**{word.upper()}**', text) + logging.debug(f"Highlighted text: {text}") + return text + + @staticmethod + def extract_publication_date(soup): + date_meta = soup.find('meta', property='article:published_time') + if date_meta: + logging.debug(f"Extracted publication date: {date_meta['content']}") + return datetime.fromisoformat(date_meta['content'].split('+')[0]) + + date_meta = soup.find('meta', attrs={'name': 'pubdate'}) + if date_meta: + logging.debug(f"Extracted publication date: {date_meta['content']}") + return datetime.fromisoformat(date_meta['content'].split('+')[0]) + + date_tag = soup.find(['time', 'span'], attrs={'datetime': True}) + if date_tag: + logging.debug(f"Extracted publication date: {date_tag['datetime']}") + return datetime.fromisoformat(date_tag['datetime'].split('+')[0]) + + return None + + @classmethod + def generate_summary(cls, state: SummaryState): + content_item = state['content'] + logging.debug(f"Content item received in generate_summary: {content_item} (type: {type(content_item)})") + + if isinstance(content_item, dict): + logging.debug(f"Content item is a dictionary: {content_item}") + content = content_item.get('page_content', '') + source = content_item.get('metadata', {}).get('source', 'Unknown Source') + else: + logging.error(f"Expected a dictionary for content_item but got {type(content_item)}: {content_item}") + raise TypeError("Expected a dictionary for content_item in generate_summary.") + + query = state['query'] + logging.debug(f"Generating summary for source: {source}") + prompt = f"Summarize the following content to answer the question: {query}, mention the source: {source} \n\n {content[:500]}... " + logging.chat(f"generate_summary is sending data to model: {prompt}") + page_summary = simple_model.invoke(prompt) # May need advanced model + logging.chat(f"generate_summary received data from model: {page_summary.content}") + logging.debug(f"Summary generated (first 1000 characters): {page_summary.content[:1000]}...") + return {"page_summaries": [page_summary.content]} + + @classmethod + def continue_to_summarise_node(cls, state): + logging.debug("Entering continue_to_summarise_node") + if 'page_content' not in state or not state['page_content']: + logging.error("page_content is missing or empty in state") + return [] + logging.debug(f"Page content before summarization: {state['page_content']}") + + return [Send("Generate Summary", { + "content": { + "page_content": p['page_content'], + "metadata": p.get("metadata", {}) + }, + "query": state['messages'][0].content + }) for p in state['page_content'] if isinstance(p, dict)] + + @classmethod + def final_result_node(cls, state): + logging.debug("Entering final_result_node") + messages = state['messages'] + question = messages[-1] + context = state['page_summaries'] + logging.debug(f"Question: {question}") + logging.debug(f"Number of context summaries: {len(context)}") + prompt = ChatPromptTemplate.from_messages( + [ + ("system", PerplexityClone.FINAL_NODE_SYSTEM_PROMPT), + ("human", PerplexityClone.FINAL_NODE_PROMPT), + ] + ) + input = {"question": question, "context": context} + formatted_prompt = prompt.format_messages(**input) + logging.debug(f"Formatted prompt for final response: {formatted_prompt}") + response = advanced_model.invoke(formatted_prompt) + logging.debug(f"Final response generated (first 500 characters): {response.content[:500]}...") + return {"messages": [response]} + + @staticmethod + async def search(queries): + logging.debug("Entering search method") + apiUrl = os.getenv("SEARXNG_API_URL_DEV") + if not apiUrl: + raise ValueError("SEARXNG_API_URL_DEV is not set in the environment variables") + + use_cache = os.getenv("DEV_MODE", "true").lower() == "false" + + async with aiohttp.ClientSession() as session: + results = [] + for query in queries: + logging.debug(f"Searching for query: {query}") + + # Check cache for existing results only if DEV_MODE is false + if use_cache: + cache_key = f"searxng_search:{query}" + cached_result = get_cached_results(cache_key) + if cached_result: + logging.info(f"Found cached search result for query: {query}") + results.append(cached_result) + continue + + try: + params = { + 'q': query, + 'format': 'json', + 'categories': 'general', + 'engines': os.getenv('SEARXNG_ENGINES', 'google,bing,duckduckgo'), + 'time_range': os.getenv('SEARXNG_TIME_RANGE', ''), + 'safesearch': os.getenv('SEARXNG_SAFESEARCH', '0'), + } + url = f"{apiUrl}/search?{urlencode(params)}" + async with session.get(url) as response: + if response.status != 200: + raise Exception(f"SearXNG API error: {response.status}") + data = await response.json() + if 'results' not in data: + logging.warning(f"No results found for query: {query}") + search_results = [] + else: + search_results = data['results'][:3] # Limit to top 3 results + + results.append(search_results) + + # Cache the result only if DEV_MODE is false + if use_cache: + set_cached_results(cache_key, search_results) + + logging.debug(f"Raw API response for query '{query}': {data}") + except Exception as e: + logging.error(f"Error in search for query {query}: {str(e)}") + results.append([]) # Add an empty list for failed queries + + return results + +# Create an instance of PerplexityClone +perplexity_clone_instance = PerplexityClone() + +# Construct the graph using the class methods +perplexity_clone = StateGraph(PerplexityClone.OverallState) +perplexity_clone.add_node('Additional Questions', PerplexityClone.OverallState.additional_questions_node) +perplexity_clone.add_node('Ask', PerplexityClone.OverallState.ask_node) +perplexity_clone.add_node('New Question', PerplexityClone.OverallState.new_question_node) +perplexity_clone.add_node('Query Generator', PerplexityClone.OverallState.search_query_node) +perplexity_clone.add_node('Search Results', PerplexityClone.OverallState.search_results_node) +perplexity_clone.add_node('Web Scraper', PerplexityClone.OverallState.web_scrape_node) +perplexity_clone.add_node('Generate Summary', PerplexityClone.OverallState.generate_summary) +perplexity_clone.add_node('Final Result', PerplexityClone.OverallState.final_result_node) + +perplexity_clone.set_entry_point('Additional Questions') +perplexity_clone.set_finish_point('Final Result') + +perplexity_clone.add_conditional_edges('Additional Questions', PerplexityClone.OverallState.where_to_go, {'proceed': 'Query Generator', 'ask': 'Ask'}) +perplexity_clone.add_edge('Ask', 'New Question') +perplexity_clone.add_edge('New Question', 'Query Generator') +perplexity_clone.add_edge('Query Generator', 'Search Results') +perplexity_clone.add_edge('Search Results', 'Web Scraper') +perplexity_clone.add_conditional_edges('Web Scraper', PerplexityClone.OverallState.continue_to_summarise_node, ['Generate Summary']) +perplexity_clone.add_edge('Generate Summary', 'Final Result') + +# Compile the graph +perplexity_clone_graph = perplexity_clone.compile(checkpointer=MemorySaver(), interrupt_after=["Ask"]) + +# Export the graph and OverallState +OverallState = PerplexityClone.OverallState + +# Export the graph and OverallState +__all__ = ["perplexity_clone_graph", "OverallState"] diff --git a/modules/langchain/neo4j_graph_qa.py b/modules/langchain/neo4j_graph_qa.py new file mode 100644 index 0000000..2a31130 --- /dev/null +++ b/modules/langchain/neo4j_graph_qa.py @@ -0,0 +1,61 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_langchain_graph_qa' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +def test_query_graph(database, prompt, top_k=20, model="gpt-4o", temperature=0, verbose=False, return_intermediate_steps=True, exclude_types=None, include_types=None, return_direct=False, validate_cypher=False, model_type="openai"): + url = f"{os.environ['APP_API_URL']}/langchain/graph_qa/prompt" + params = { + "database": database, + "prompt": prompt, + "top_k": top_k, + "model": model, + "temperature": temperature, + "verbose": verbose, + "return_intermediate_steps": return_intermediate_steps, + "exclude_types": exclude_types or [], + "include_types": include_types or [], + "return_direct": return_direct, + "validate_cypher": validate_cypher, + "model_type": model_type + } + + try: + response = requests.get(url, params=params) + response.raise_for_status() # Raise an error for bad status codes + data = response.json() + logging.info("==================================================") + logging.info("= =") + logging.info("= Test Execution =") + logging.info("= =") + logging.info("==================================================") + logging.info(f"= Prompt: {data.get('query', 'N/A')}") + logging.info("= =") + logging.info(f"= Query: \n{data.get('intermediate_steps', [{'query': 'N/A'}])[0].get('query', 'N/A')}") + logging.info("= =") + logging.info("==================================================") + + # Determine if the test passed or failed + response_text = data.get('result', 'N/A') + context = data.get('intermediate_steps', [{'context': 'N/A'}])[1].get('context', 'N/A') + if "I don't know" in response_text or not context: + return False, context, response_text + else: + return True, context, response_text + except requests.exceptions.RequestException as e: + logging.error("==================================================") + logging.error("= ERROR =") + logging.error("==================================================") + logging.error(f"Error: {e}") + logging.error("==================================================") + return False, None, None \ No newline at end of file diff --git a/modules/logger_tool.py b/modules/logger_tool.py new file mode 100644 index 0000000..7aa7907 --- /dev/null +++ b/modules/logger_tool.py @@ -0,0 +1,174 @@ +import os +import sys +import logging +import datetime +import pytest +from dotenv import load_dotenv, find_dotenv + +# Load environment variables +load_dotenv(find_dotenv()) + +# Define a global format string for log alignment +LOG_FORMAT = "%(asctime)s %(levelname)-8s: %(filename)-20s:%(funcName)-20s:%(lineno)-4d >>> %(message)s" + +# Define custom logging levels +SUCCESS_LOG_LEVEL = 21 +APP_LOG_LEVEL = 15 +PROD_LOG_LEVEL = 14 +DATABASE_LOG_LEVEL = 13 +CHAT_LOG_LEVEL = 12 +QUERY_LOG_LEVEL = 11 +TESTING_LOG_LEVEL = 9 +VARIABLES_LOG_LEVEL = 2 +PEDANTIC_LOG_LEVEL = 1 + +# Register custom log levels +CUSTOM_LEVELS = { + SUCCESS_LOG_LEVEL: "SUCCESS", + APP_LOG_LEVEL: "APP", + PROD_LOG_LEVEL: "PROD", + DATABASE_LOG_LEVEL: "DATABASE", + CHAT_LOG_LEVEL: "CHAT", + QUERY_LOG_LEVEL: "QUERY", + TESTING_LOG_LEVEL: "TESTING", + VARIABLES_LOG_LEVEL: "VARIABLES", + PEDANTIC_LOG_LEVEL: "PEDANTIC" +} +for level, name in CUSTOM_LEVELS.items(): + logging.addLevelName(level, name) + +# ANSI escape sequences for colors +class LogColors: + """ANSI escape sequences for various log levels to colorize log output.""" + DARK_RED, BRIGHT_RED = '\033[31m', '\033[91m' + ORANGE, GOLD, YELLOW = '\033[33m', '\033[93m', '\033[93m' + GREEN, DARKGREEN, LIGHTGREEN = '\033[92m', '\033[32m', '\033[92m' + LIGHTBLUE, BLUE, INDIGO, VIOLET = '\033[94m', '\033[94m', '\033[34m', '\033[35m' + RESET, WHITE, BLACK = '\033[0m', '\033[37m', '\033[30m' + +# Custom Formatter +class ColoredFormatter(logging.Formatter): + """Custom formatter to add color and emojis to log messages for console output.""" + COLORS = { + logging.CRITICAL: f"{LogColors.DARK_RED}🚨 ", + logging.ERROR: f"{LogColors.BRIGHT_RED}❌ ", + logging.WARNING: f"{LogColors.ORANGE}⚠️ ", + logging.INFO: f"{LogColors.GREEN}ℹ️ ", + logging.DEBUG: f"{LogColors.BLUE}🐛 ", + SUCCESS_LOG_LEVEL: f"{LogColors.LIGHTGREEN}✅ ", + APP_LOG_LEVEL: f"{LogColors.INDIGO}🚀 ", + PROD_LOG_LEVEL: f"{LogColors.VIOLET}🏭 ", + DATABASE_LOG_LEVEL: f"{LogColors.DARKGREEN}🗄️ ", + CHAT_LOG_LEVEL: f"{LogColors.LIGHTBLUE}💬 ", + QUERY_LOG_LEVEL: f"{LogColors.GOLD}🔍 ", + TESTING_LOG_LEVEL: f"{LogColors.YELLOW}🧪 ", + VARIABLES_LOG_LEVEL: f"{LogColors.WHITE}🔢 ", + PEDANTIC_LOG_LEVEL: f"{LogColors.BLACK}🔬 ", + } + + def format(self, record): + log_fmt = self.COLORS.get(record.levelno, self.COLORS[logging.INFO]) + self._fmt + formatter = logging.Formatter(log_fmt) + message = formatter.format(record) + LogColors.RESET + try: + return message + except UnicodeEncodeError: + return message.encode('ascii', 'replace').decode('ascii') + + def formatException(self, ei): + try: + return super().formatException(ei) + except UnicodeEncodeError: + return super().formatException(ei).encode('ascii', 'replace').decode('ascii') + +class FileFormatter(logging.Formatter): + def format(self, record): + return logging.Formatter(LOG_FORMAT).format(record) + +class PytestFormatter: + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_makereport(self, item, call): + outcome = yield + report = outcome.get_result() + if report.when == "call": + if report.passed: + logging.success(f"✅ Test passed: {item.name}") + elif report.failed: + logging.error(f"❌ Test failed: {item.name}") + elif report.skipped: + logging.warning(f"⏭️ Test skipped: {item.name}") + +# Custom logger methods +def _add_custom_log_methods(): + def create_log_method(level): + def log_method(self, message, *args, **kws): + if self.isEnabledFor(level): + self._log(level, message, args, **kws) + return log_method + + custom_methods = { + "chat": CHAT_LOG_LEVEL, "variables": VARIABLES_LOG_LEVEL, + "pedantic": PEDANTIC_LOG_LEVEL, "prod": PROD_LOG_LEVEL, + "query": QUERY_LOG_LEVEL, "database": DATABASE_LOG_LEVEL, + "testing": TESTING_LOG_LEVEL, "app": APP_LOG_LEVEL, "success": SUCCESS_LOG_LEVEL + } + for method_name, level in custom_methods.items(): + setattr(logging.Logger, method_name, create_log_method(level)) + +_add_custom_log_methods() + +# Set an environment directory +def set_log_path(env_var): + env_path = os.getenv(env_var.upper()) + if env_path and not os.path.exists(env_path): + os.makedirs(env_path) + return env_path + +# Get a logger with specified settings +def get_logger(name=None, log_level=None, log_path=None, log_file=None, delimiter='_', runtime=False, log_format=None): + logger = logging.getLogger(name or __name__) + log_levels = { + 'CRITICAL': logging.CRITICAL, 'ERROR': logging.ERROR, 'WARNING': logging.WARNING, + 'SUCCESS': SUCCESS_LOG_LEVEL, 'APP': APP_LOG_LEVEL, 'PROD': PROD_LOG_LEVEL, + 'DATABASE': DATABASE_LOG_LEVEL, 'INFO': logging.INFO, 'DEBUG': logging.DEBUG, + 'TESTING': TESTING_LOG_LEVEL, 'CHAT': CHAT_LOG_LEVEL, 'QUERY': QUERY_LOG_LEVEL, + 'VARIABLES': VARIABLES_LOG_LEVEL, 'PEDANTIC': PEDANTIC_LOG_LEVEL, + } + desired_level = log_levels.get(log_level, logging.INFO) if log_level else logger.level or logging.INFO + logger.setLevel(desired_level) + logger.propagate = False + + console_formatter = ColoredFormatter(LOG_FORMAT if log_format in (None, "default") else log_format) + file_formatter = FileFormatter(LOG_FORMAT if log_format in (None, "default") else log_format) + + if not logger.handlers: + if runtime: + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.DEBUG) + ch.setFormatter(console_formatter) + logger.addHandler(ch) + + if log_path and log_file: + os.makedirs(log_path, exist_ok=True) + log_file_path = os.path.join(log_path, f"{log_file}{'_' if runtime else delimiter}{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') if not runtime else ''}.log") + fh = logging.FileHandler(log_file_path) + fh.setLevel(desired_level) + fh.setFormatter(file_formatter) + logger.addHandler(fh) + else: + for handler in logger.handlers: + handler.setLevel(desired_level) + handler.setFormatter(file_formatter if isinstance(handler, logging.FileHandler) else console_formatter) + + return logger + +# Retrieve all loggers +def get_loggers(): + """Return a list of all logger instances.""" + return [logging.getLogger(name) for name in logging.Logger.manager.loggerDict] + +# Initialize logger using provided settings +def initialise_logger(log_name='backend', log_level=None, log_dir=None, log_format='default', runtime=True): + log_level = log_level or os.getenv("LOG_LEVEL", "DEBUG") + log_dir = log_dir or os.getenv("LOG_PATH", "/logs") + return get_logger(name=log_name, log_level=log_level, log_path=log_dir, log_file=log_name, runtime=runtime, log_format=log_format) diff --git a/modules/msgraph/__init__.py b/modules/msgraph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/msgraph/msapi_config.py b/modules/msgraph/msapi_config.py new file mode 100644 index 0000000..6a44e10 --- /dev/null +++ b/modules/msgraph/msapi_config.py @@ -0,0 +1,41 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_msgraph_config' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from msal import ConfidentialClientApplication + +CLIENT_ID = os.getenv("VITE_MICROSOFT_CLIENT_ID") +CLIENT_SECRET = os.getenv("VITE_MICROSOFT_CLIENT_SECRET") +TENANT_ID = os.getenv("VITE_MICROSOFT_TENANT_ID") +AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}" +SCOPE = ["https://graph.microsoft.com/.default"] + +# Create an MSAL confidential client application +def get_ms_access_token(): + app = ConfidentialClientApplication( + client_id=CLIENT_ID, + client_credential=CLIENT_SECRET, + authority=AUTHORITY, + ) + + # For a confidential client application, we don't use user-specific accounts + # Instead, we directly acquire a token for the application + result = app.acquire_token_for_client(scopes=SCOPE) + + if 'access_token' in result: + logging.info("Token acquired successfully") + return result['access_token'] + else: + error_message = f"Failed to acquire token: {result.get('error')}, {result.get('error_description')}" + logging.error(error_message) + raise Exception(error_message) diff --git a/modules/msgraph/msgraph_client.py b/modules/msgraph/msgraph_client.py new file mode 100644 index 0000000..90cb4c2 --- /dev/null +++ b/modules/msgraph/msgraph_client.py @@ -0,0 +1,40 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_msgraph_client' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import requests + +GRAPH_API_ENDPOINT = "https://graph.microsoft.com/v1.0" + +class MSGraphClient: + def __init__(self, access_token: str): + self.access_token = access_token + + def get_headers(self): + return { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json", + } + + def get_onenote_notebooks(self): + url = f"{GRAPH_API_ENDPOINT}/me/onenote/notebooks" + response = requests.get(url, headers=self.get_headers()) + + if response.status_code == 200: + return response.json().get('value', []) + else: + raise Exception(f"Error fetching notebooks: {response.status_code}, {response.text}") + +# Function to initialize the MSGraph client +def get_msgraph_client(access_token: str): + return MSGraphClient(access_token) diff --git a/modules/pdf_utils.py b/modules/pdf_utils.py new file mode 100644 index 0000000..5c49839 --- /dev/null +++ b/modules/pdf_utils.py @@ -0,0 +1,43 @@ +from pathlib import Path +import PyPDF2 +from typing import Optional + +class PDFUtils: + @staticmethod + def extract_text_from_pdf(pdf_file: Path) -> str: + """ + Extract text content from a PDF file + """ + if not pdf_file.exists(): + raise FileNotFoundError(f"PDF file not found: {pdf_file}") + + text = "" + with open(pdf_file, 'rb') as file: + pdf_reader = PyPDF2.PdfReader(file) + for page in pdf_reader.pages: + text += page.extract_text() + "\n" + + return text + + @staticmethod + def get_pdf_metadata(pdf_file: Path) -> dict: + """ + Get metadata from a PDF file + """ + if not pdf_file.exists(): + raise FileNotFoundError(f"PDF file not found: {pdf_file}") + + with open(pdf_file, 'rb') as file: + pdf_reader = PyPDF2.PdfReader(file) + metadata = pdf_reader.metadata or {} + + # Convert metadata to a dictionary + result = {} + for key, value in metadata.items(): + if value: + result[key] = str(value) + + # Add additional information + result['num_pages'] = len(pdf_reader.pages) + + return result \ No newline at end of file diff --git a/modules/redis_config.py b/modules/redis_config.py new file mode 100644 index 0000000..a1c7d5d --- /dev/null +++ b/modules/redis_config.py @@ -0,0 +1,38 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_redis_config' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from redis import Redis + +REDIS_URL = os.getenv("LOCAL_REDIS_URL", "redis://localhost:6379") +CACHE_TTL = 3600 # Cache time-to-live in seconds (1 hour) + +redis_client = Redis.from_url(REDIS_URL, decode_responses=True) + +def get_cached_results(cache_key): + try: + cached_data = redis_client.get(cache_key) + if cached_data: + logging.info(f"Cached data: {cached_data}") + return eval(cached_data) + return None + except Exception as e: + print(f"Redis cache error: {e}") + return None + +def set_cached_results(cache_key, results): + try: + redis_client.setex(cache_key, CACHE_TTL, str(results)) + logging.info(f"Cached results: {results}") + except Exception as e: + print(f"Redis cache error: {e}") \ No newline at end of file diff --git a/modules/services/__init__.py b/modules/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/services/textgen/__init__.py b/modules/services/textgen/__init__.py new file mode 100644 index 0000000..3810b91 --- /dev/null +++ b/modules/services/textgen/__init__.py @@ -0,0 +1,23 @@ +""" +TextGen service module for interacting with the Text Generation WebUI API. +""" + +from .client import TextGenClient +from .models import ( + ChatMessage, + ChatCompletionRequest, + ChatCompletionResponse, + CompletionRequest, + CompletionResponse, + ModelInfo, +) + +__all__ = [ + "TextGenClient", + "ChatMessage", + "ChatCompletionRequest", + "ChatCompletionResponse", + "CompletionRequest", + "CompletionResponse", + "ModelInfo", +] diff --git a/modules/services/textgen/client.py b/modules/services/textgen/client.py new file mode 100644 index 0000000..524b955 --- /dev/null +++ b/modules/services/textgen/client.py @@ -0,0 +1,366 @@ +""" +Client for interacting with the Text Generation WebUI API. +""" + +import json +import time +import uuid +import logging +import asyncio +import aiohttp +from typing import Dict, List, Optional, Union, Any, AsyncGenerator, cast +from urllib.parse import urljoin + +from .models import ( + ChatMessage, + ChatCompletionRequest, + ChatCompletionResponse, + CompletionRequest, + CompletionResponse, + ModelInfo, + ModelListResponse, + ModelLoadRequest, + LogitsRequest, +) + +logger = logging.getLogger(__name__) + + +class TextGenClient: + """Client for interacting with the Text Generation WebUI API.""" + + def __init__( + self, + base_url: str = "http://textgen.localhost/v1", + api_key: Optional[str] = None, + timeout: int = 120, + ): + """ + Initialize the TextGen client. + + Args: + base_url: Base URL for the TextGen API + api_key: API key for authentication (optional) + timeout: Request timeout in seconds + """ + self.base_url = base_url + self.api_key = api_key + self.timeout = timeout + self._session = None + + async def _ensure_session(self) -> aiohttp.ClientSession: + """Ensure that an aiohttp session exists.""" + if self._session is None or self._session.closed: + self._session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) + return self._session + + async def close(self): + """Close the client session.""" + if self._session and not self._session.closed: + await self._session.close() + self._session = None + + def _get_headers(self) -> Dict[str, str]: + """Get headers for API requests.""" + headers = { + "Content-Type": "application/json", + } + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + return headers + + async def _make_request( + self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Make a request to the TextGen API. + + Args: + method: HTTP method (GET, POST, etc.) + endpoint: API endpoint + data: Request data + + Returns: + API response as a dictionary + """ + session = await self._ensure_session() + url = urljoin(self.base_url, endpoint) + + try: + async with session.request( + method=method, + url=url, + headers=self._get_headers(), + json=data, + raise_for_status=True, + ) as response: + return await response.json() + except aiohttp.ClientResponseError as e: + logger.error(f"API request failed: {e.status} {e.message}") + raise + except aiohttp.ClientError as e: + logger.error(f"Request error: {str(e)}") + raise + except asyncio.TimeoutError: + logger.error(f"Request timed out after {self.timeout} seconds") + raise + + async def _stream_request( + self, endpoint: str, data: Dict[str, Any] + ) -> AsyncGenerator[Dict[str, Any], None]: + """ + Make a streaming request to the TextGen API. + + Args: + endpoint: API endpoint + data: Request data + + Yields: + Chunks of the API response + """ + session = await self._ensure_session() + url = urljoin(self.base_url, endpoint) + + try: + async with session.post( + url=url, + headers=self._get_headers(), + json=data, + raise_for_status=True, + ) as response: + async for line in response.content: + line = line.strip() + if not line or line == b"data: [DONE]": + continue + if line.startswith(b"data: "): + line = line[6:] # Remove "data: " prefix + try: + yield json.loads(line) + except json.JSONDecodeError: + logger.error(f"Failed to parse SSE data: {line}") + except aiohttp.ClientResponseError as e: + logger.error(f"API request failed: {e.status} {e.message}") + raise + except aiohttp.ClientError as e: + logger.error(f"Request error: {str(e)}") + raise + except asyncio.TimeoutError: + logger.error(f"Request timed out after {self.timeout} seconds") + raise + + async def list_models(self) -> List[ModelInfo]: + """ + List available models. + + Returns: + List of available models + """ + response = await self._make_request("GET", "internal/model/list") + model_list = ModelListResponse(**response) + return model_list.data + + async def load_model(self, model_name: str, **kwargs) -> Dict[str, Any]: + """ + Load a model. + + Args: + model_name: Name of the model to load + **kwargs: Additional arguments for loading the model + + Returns: + Response from the API + """ + request = ModelLoadRequest(model_name=model_name, args=kwargs) + return await self._make_request("POST", "internal/model/load", request.dict()) + + async def chat_completion( + self, request: ChatCompletionRequest + ) -> Union[ChatCompletionResponse, AsyncGenerator[Dict[str, Any], None]]: + """ + Create a chat completion. + + Args: + request: Chat completion request + + Returns: + Chat completion response or a stream of responses + """ + request_data = request.dict(exclude_none=True) + + if request.stream: + return self._stream_request("chat/completions", request_data) + + response = await self._make_request("POST", "chat/completions", request_data) + return ChatCompletionResponse(**response) + + async def completion( + self, request: CompletionRequest + ) -> Union[CompletionResponse, AsyncGenerator[Dict[str, Any], None]]: + """ + Create a text completion. + + Args: + request: Completion request + + Returns: + Completion response or a stream of responses + """ + request_data = request.dict(exclude_none=True) + + if request.stream: + return self._stream_request("completions", request_data) + + response = await self._make_request("POST", "completions", request_data) + return CompletionResponse(**response) + + async def get_logits(self, request: LogitsRequest) -> Dict[str, Any]: + """ + Get logits for a prompt. + + Args: + request: Logits request + + Returns: + Logits response + """ + request_data = request.dict(exclude_none=True) + return await self._make_request("POST", "internal/logits", request_data) + + async def simple_chat( + self, + messages: List[Dict[str, str]], + model: Optional[str] = None, + temperature: float = 0.7, + top_p: float = 0.9, + max_tokens: Optional[int] = None, + stop: Optional[Union[str, List[str]]] = None, + presence_penalty: float = 0.0, + frequency_penalty: float = 0.0, + stream: bool = False, + mode: str = "instruct", + character: Optional[str] = None, + instruction_template: Optional[str] = None, + seed: Optional[int] = None, + ) -> Union[str, AsyncGenerator[str, None]]: + """ + Simple interface for chat completions. + + Args: + messages: List of message dictionaries with 'role' and 'content' + model: Model to use + temperature: Sampling temperature + top_p: Nucleus sampling parameter + max_tokens: Maximum tokens to generate + stop: Stop sequences + presence_penalty: Presence penalty + frequency_penalty: Frequency penalty + stream: Whether to stream the response + mode: Mode (chat or instruct) + character: Character to use (for chat mode) + instruction_template: Instruction template (for instruct mode) + seed: Random seed for reproducibility + + Returns: + Generated text or a stream of text chunks + """ + chat_messages = [ChatMessage(**msg) for msg in messages] + request = ChatCompletionRequest( + messages=chat_messages, + model=model, + temperature=temperature, + top_p=top_p, + max_tokens=max_tokens, + stop=stop, + presence_penalty=presence_penalty, + frequency_penalty=frequency_penalty, + stream=stream, + mode=mode, + character=character, + instruction_template=instruction_template, + seed=seed, + ) + + if stream: + + async def text_stream() -> AsyncGenerator[str, None]: + stream_response = await self.chat_completion(request) + if isinstance(stream_response, AsyncGenerator): + async for chunk in stream_response: + if "choices" in chunk and chunk["choices"]: + if ( + "delta" in chunk["choices"][0] + and "content" in chunk["choices"][0]["delta"] + ): + yield chunk["choices"][0]["delta"]["content"] + + return text_stream() + else: + response = await self.chat_completion(request) + if isinstance(response, ChatCompletionResponse): + return response.choices[0].message.content + # This should never happen due to the if/else structure, but satisfies the type checker + raise TypeError("Expected ChatCompletionResponse but got stream response") + + async def simple_completion( + self, + prompt: str, + model: Optional[str] = None, + temperature: float = 0.7, + top_p: float = 0.9, + max_tokens: Optional[int] = None, + stop: Optional[Union[str, List[str]]] = None, + presence_penalty: float = 0.0, + frequency_penalty: float = 0.0, + stream: bool = False, + seed: Optional[int] = None, + ) -> Union[str, AsyncGenerator[str, None]]: + """ + Simple interface for text completions. + + Args: + prompt: Text prompt + model: Model to use + temperature: Sampling temperature + top_p: Nucleus sampling parameter + max_tokens: Maximum tokens to generate + stop: Stop sequences + presence_penalty: Presence penalty + frequency_penalty: Frequency penalty + stream: Whether to stream the response + seed: Random seed for reproducibility + + Returns: + Generated text or a stream of text chunks + """ + request = CompletionRequest( + prompt=prompt, + model=model, + temperature=temperature, + top_p=top_p, + max_tokens=max_tokens, + stop=stop, + presence_penalty=presence_penalty, + frequency_penalty=frequency_penalty, + stream=stream, + seed=seed, + ) + + if stream: + + async def text_stream() -> AsyncGenerator[str, None]: + stream_response = await self.completion(request) + if isinstance(stream_response, AsyncGenerator): + async for chunk in stream_response: + if "choices" in chunk and chunk["choices"]: + if "text" in chunk["choices"][0]: + yield chunk["choices"][0]["text"] + + return text_stream() + else: + response = await self.completion(request) + if isinstance(response, CompletionResponse): + return response.choices[0].text + # This should never happen due to the if/else structure, but satisfies the type checker + raise TypeError("Expected CompletionResponse but got stream response") diff --git a/modules/services/textgen/example.py b/modules/services/textgen/example.py new file mode 100644 index 0000000..fade2b4 --- /dev/null +++ b/modules/services/textgen/example.py @@ -0,0 +1,119 @@ +""" +Example usage of the TextGen client. +""" + +import asyncio +import logging +from typing import AsyncGenerator + +from .client import TextGenClient + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +async def chat_example(): + """Example of using the chat completion API.""" + client = TextGenClient() + + try: + # Simple chat example + messages = [{"role": "user", "content": "Hello! Who are you?"}] + + # Non-streaming response + logger.info("Sending chat request (non-streaming)...") + response = await client.simple_chat( + messages=messages, temperature=0.7, max_tokens=500, mode="instruct" + ) + logger.info(f"Response: {response}") + + # Streaming response + logger.info("Sending chat request (streaming)...") + stream_response = await client.simple_chat( + messages=messages, + temperature=0.7, + max_tokens=500, + stream=True, + mode="instruct", + ) + + # Check if the response is a stream + if isinstance(stream_response, AsyncGenerator): + logger.info("Streaming response:") + async for chunk in stream_response: + print(chunk, end="", flush=True) + print() + else: + logger.info(f"Expected stream but got: {stream_response}") + + except Exception as e: + logger.error(f"Error: {str(e)}") + finally: + await client.close() + + +async def completion_example(): + """Example of using the text completion API.""" + client = TextGenClient() + + try: + prompt = "This is a cake recipe:\n\n1." + + # Non-streaming response + logger.info("Sending completion request (non-streaming)...") + response = await client.simple_completion( + prompt=prompt, temperature=0.7, max_tokens=200 + ) + logger.info(f"Response: {response}") + + # Streaming response + logger.info("Sending completion request (streaming)...") + stream_response = await client.simple_completion( + prompt=prompt, temperature=0.7, max_tokens=200, stream=True + ) + + # Check if the response is a stream + if isinstance(stream_response, AsyncGenerator): + logger.info("Streaming response:") + async for chunk in stream_response: + print(chunk, end="", flush=True) + print() + else: + logger.info(f"Expected stream but got: {stream_response}") + + except Exception as e: + logger.error(f"Error: {str(e)}") + finally: + await client.close() + + +async def list_models_example(): + """Example of listing available models.""" + client = TextGenClient() + + try: + logger.info("Listing available models...") + models = await client.list_models() + for model in models: + logger.info(f"Model: {model.id}") + except Exception as e: + logger.error(f"Error: {str(e)}") + finally: + await client.close() + + +async def main(): + """Run all examples.""" + logger.info("Running TextGen client examples") + + await list_models_example() + await chat_example() + await completion_example() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/modules/services/textgen/models.py b/modules/services/textgen/models.py new file mode 100644 index 0000000..1c07e47 --- /dev/null +++ b/modules/services/textgen/models.py @@ -0,0 +1,140 @@ +""" +Data models for the TextGen API. +""" + +from typing import Dict, List, Optional, Union, Any +from pydantic import BaseModel, Field + + +class ChatMessage(BaseModel): + """A chat message in a conversation.""" + + role: str = Field( + ..., description="The role of the message sender (user, assistant, system)" + ) + content: str = Field(..., description="The content of the message") + name: Optional[str] = Field(None, description="The name of the sender (optional)") + + +class ChatCompletionRequest(BaseModel): + """Request model for chat completions.""" + + messages: List[ChatMessage] = Field( + ..., description="The messages in the conversation" + ) + model: Optional[str] = Field(None, description="The model to use for completion") + temperature: Optional[float] = Field(0.7, description="Sampling temperature") + top_p: Optional[float] = Field(0.9, description="Nucleus sampling parameter") + max_tokens: Optional[int] = Field( + None, description="Maximum number of tokens to generate" + ) + stream: Optional[bool] = Field(False, description="Whether to stream the response") + stop: Optional[Union[str, List[str]]] = Field(None, description="Stop sequences") + presence_penalty: Optional[float] = Field(0.0, description="Presence penalty") + frequency_penalty: Optional[float] = Field(0.0, description="Frequency penalty") + mode: Optional[str] = Field("chat", description="Mode (chat or instruct)") + character: Optional[str] = Field( + None, description="Character to use (for chat mode)" + ) + instruction_template: Optional[str] = Field( + None, description="Instruction template (for instruct mode)" + ) + seed: Optional[int] = Field(None, description="Random seed for reproducibility") + + +class ChatCompletionResponseChoice(BaseModel): + """A choice in a chat completion response.""" + + index: int = Field(..., description="Index of the choice") + message: ChatMessage = Field(..., description="The message") + finish_reason: Optional[str] = Field(None, description="Reason for finishing") + + +class ChatCompletionResponse(BaseModel): + """Response model for chat completions.""" + + id: str = Field(..., description="Unique identifier for the completion") + object: str = Field("chat.completion", description="Object type") + created: int = Field(..., description="Unix timestamp of creation") + model: str = Field(..., description="Model used for completion") + choices: List[ChatCompletionResponseChoice] = Field( + ..., description="Completion choices" + ) + usage: Dict[str, int] = Field(..., description="Token usage information") + + +class CompletionRequest(BaseModel): + """Request model for text completions.""" + + prompt: str = Field(..., description="The prompt to complete") + model: Optional[str] = Field(None, description="The model to use for completion") + temperature: Optional[float] = Field(0.7, description="Sampling temperature") + top_p: Optional[float] = Field(0.9, description="Nucleus sampling parameter") + max_tokens: Optional[int] = Field( + None, description="Maximum number of tokens to generate" + ) + stream: Optional[bool] = Field(False, description="Whether to stream the response") + stop: Optional[Union[str, List[str]]] = Field(None, description="Stop sequences") + presence_penalty: Optional[float] = Field(0.0, description="Presence penalty") + frequency_penalty: Optional[float] = Field(0.0, description="Frequency penalty") + seed: Optional[int] = Field(None, description="Random seed for reproducibility") + + +class CompletionResponseChoice(BaseModel): + """A choice in a completion response.""" + + text: str = Field(..., description="The generated text") + index: int = Field(..., description="Index of the choice") + logprobs: Optional[Any] = Field(None, description="Log probabilities") + finish_reason: Optional[str] = Field(None, description="Reason for finishing") + + +class CompletionResponse(BaseModel): + """Response model for text completions.""" + + id: str = Field(..., description="Unique identifier for the completion") + object: str = Field("text_completion", description="Object type") + created: int = Field(..., description="Unix timestamp of creation") + model: str = Field(..., description="Model used for completion") + choices: List[CompletionResponseChoice] = Field( + ..., description="Completion choices" + ) + usage: Dict[str, int] = Field(..., description="Token usage information") + + +class ModelInfo(BaseModel): + """Information about a model.""" + + id: str = Field(..., description="Model identifier") + object: str = Field("model", description="Object type") + created: int = Field(..., description="Unix timestamp of creation") + owned_by: str = Field("user", description="Owner of the model") + permission: List[Dict[str, Any]] = Field([], description="Permissions") + root: str = Field(..., description="Root model") + parent: Optional[str] = Field(None, description="Parent model") + + +class LogitsRequest(BaseModel): + """Request model for logits.""" + + prompt: str = Field(..., description="The prompt to get logits for") + use_samplers: bool = Field( + False, description="Whether to apply sampling parameters" + ) + top_k: Optional[int] = Field(None, description="Top-k sampling parameter") + top_p: Optional[float] = Field(None, description="Top-p sampling parameter") + temperature: Optional[float] = Field(None, description="Sampling temperature") + + +class ModelListResponse(BaseModel): + """Response model for model list.""" + + object: str = Field("list", description="Object type") + data: List[ModelInfo] = Field(..., description="List of models") + + +class ModelLoadRequest(BaseModel): + """Request model for loading a model.""" + + model_name: str = Field(..., description="Name of the model to load") + args: Dict[str, Any] = Field({}, description="Arguments for loading the model") diff --git a/modules/test_analyzer.py b/modules/test_analyzer.py new file mode 100644 index 0000000..64c1dc5 --- /dev/null +++ b/modules/test_analyzer.py @@ -0,0 +1,130 @@ +from pathlib import Path +import PyPDF2 +from typing import Dict, List, Optional +from pydantic import BaseModel +from modules.pdf_utils import PDFUtils + +class TestAnalysis(BaseModel): + overall_score: float + section_scores: Dict[str, float] + feedback: str + recommendations: List[str] + detailed_analysis: Optional[Dict] = None + +class TestAnalyzer: + def __init__(self, api_key: str): + self.api_key = api_key + + def extract_text_from_pdf(self, pdf_file: Path) -> str: + """ + Extract text content from a PDF file + """ + if not pdf_file.exists(): + raise FileNotFoundError(f"PDF file not found: {pdf_file}") + + text = "" + with open(pdf_file, 'rb') as file: + pdf_reader = PyPDF2.PdfReader(file) + for page in pdf_reader.pages: + text += page.extract_text() + "\n" + + return text + + def analyze_test(self, pdf_content: str, marks_data: Dict, mode: str = 'detailed') -> TestAnalysis: + """ + Analyze a test and generate feedback based on marks data + """ + # Calculate overall score + total_marks = sum(marks_data.values()) + max_marks = len(marks_data) * 100 # Assuming each question is out of 100 + overall_score = (total_marks / max_marks) * 100 + + # Calculate section scores (group by first part of question number) + section_scores = {} + for question, marks in marks_data.items(): + section = question.split('.')[0] + if section not in section_scores: + section_scores[section] = [] + section_scores[section].append(marks) + + # Calculate average for each section + for section, marks in section_scores.items(): + section_scores[section] = sum(marks) / len(marks) + + # Generate feedback + feedback = self._generate_feedback(overall_score, section_scores) + + # Generate recommendations + recommendations = self._generate_recommendations(section_scores) + + # Create detailed analysis if requested + detailed_analysis = None + if mode == 'detailed': + detailed_analysis = { + 'question_analysis': self._analyze_questions(pdf_content, marks_data), + 'strengths': self._identify_strengths(section_scores), + 'weaknesses': self._identify_weaknesses(section_scores) + } + + return TestAnalysis( + overall_score=overall_score, + section_scores=section_scores, + feedback=feedback, + recommendations=recommendations, + detailed_analysis=detailed_analysis + ) + + def _generate_feedback(self, overall_score: float, section_scores: Dict[str, float]) -> str: + """ + Generate feedback based on overall score and section scores + """ + if overall_score >= 90: + return "Excellent performance! You have demonstrated a strong understanding of the material." + elif overall_score >= 80: + return "Very good performance. You have a solid grasp of most concepts." + elif overall_score >= 70: + return "Good performance. You understand the main concepts but could improve in some areas." + elif overall_score >= 60: + return "Satisfactory performance. You have a basic understanding but need to work on several areas." + else: + return "Needs improvement. Focus on understanding the fundamental concepts better." + + def _generate_recommendations(self, section_scores: Dict[str, float]) -> List[str]: + """ + Generate recommendations based on section scores + """ + recommendations = [] + for section, score in section_scores.items(): + if score < 70: + recommendations.append(f"Focus on improving your understanding of Section {section}") + elif score < 80: + recommendations.append(f"Review Section {section} to strengthen your knowledge") + + if not recommendations: + recommendations.append("Continue practicing to maintain your strong performance") + + return recommendations + + def _analyze_questions(self, pdf_content: str, marks_data: Dict) -> Dict: + """ + Analyze individual questions + """ + question_analysis = {} + for question, marks in marks_data.items(): + question_analysis[question] = { + 'score': marks, + 'performance': 'excellent' if marks >= 90 else 'good' if marks >= 70 else 'needs_improvement' + } + return question_analysis + + def _identify_strengths(self, section_scores: Dict[str, float]) -> List[str]: + """ + Identify strong sections + """ + return [f"Section {section}" for section, score in section_scores.items() if score >= 80] + + def _identify_weaknesses(self, section_scores: Dict[str, float]) -> List[str]: + """ + Identify weak sections + """ + return [f"Section {section}" for section, score in section_scores.items() if score < 70] \ No newline at end of file diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..f2f9393 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,9 @@ +{ + "venv": "classroomcopilot-backend", + "venvPath": "/opt/anaconda3/envs", + "pythonVersion": "3.11", + "typeCheckingMode": "basic", + "extraPaths": [ + "/opt/anaconda3/envs/classroomcopilot-backend/lib/python3.11/site-packages" + ] +} \ No newline at end of file diff --git a/requirements.conda.yml b/requirements.conda.yml new file mode 100644 index 0000000..d60f290 --- /dev/null +++ b/requirements.conda.yml @@ -0,0 +1,74 @@ +name: classroomcopilot-backend +channels: + - conda-forge + - nodefaults +dependencies: + - python=3.11 + # Core dependencies + - pip + - setuptools + - wheel + # Server dependencies + - fastapi + - uvicorn + - python-dotenv + - python-multipart + - python-jose + - pyjwt + - jinja2 + # Database and Auth + - sqlalchemy + - sqlalchemy-utils + - asyncpg + - redis-py + # Neo4j + - neo4j-python-driver + # HTTP and Async + - aiohttp + # Data Processing + - pandas + - scipy + - requests + - openpyxl + - scikit-learn + # Testing + - pytest + - pytest-html + # Database clients + - postgresql + - libpq + # LibreOffice for document conversion + - libreoffice + # Additional dependencies via pip + - pip: + # Supabase + - supabase + # Neo4j specific + - neontology + # HTTP and Async + - sseclient-py + # Document Processing + - python-pptx + - python-docx + - pdfminer.six + - Pillow + - psutil + - PyPDF2>=3.0.0 + # Web Scraping and Processing + - emoji + - extruct + - w3lib + # Google APIs + - youtube-transcript-api + - google-api-python-client + - google-auth-oauthlib + # LangChain Ecosystem + - "langchain[llms]" + - langchain-community + - langchain-openai + - langgraph + # OpenAI + - openai + - ollama + # Microsoft Authentication + - msal diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..39e023c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,79 @@ +# This file is kept for compatibility with pip-only environments +# For the primary dependency management, please use requirements.conda.yml +# This file may not be updated as frequently as requirements.conda.yml + +# FastAPI and Server +fastapi +uvicorn +python-dotenv +python-multipart +python-jose +pyjwt +jinja2 + +# Database and Auth +supabase +sqlalchemy +sqlalchemy-utils +asyncpg + +# Neo4j and Databases +neo4j +neontology + +# Redis +redis + +# Data Processing and Analysis +pandas +scipy +requests +openpyxl + +# HTTP and Async +aiohttp +sseclient-py + +# OpenAI and related tools +openai +ollama + +# Providers +msal + +# Testing +pytest +pytest-html + +# Miscellaneous +emoji +extruct +w3lib +scikit-learn + +# Google APIs +youtube-transcript-api +google-api-python-client +google-auth-oauthlib + +# LangChain Ecosystem +#langchain[all] +langchain[llms] +langchain-community +#langchain-cli +#langchain-core +langchain-openai +#langchain-text-splitters +#langchainhub +#langchainplus-sdk +langgraph +#langgraph-checkpoint +#langgraph-checkpoint-postgres + +# Document Processing +python-pptx +python-docx +pdfminer.six +Pillow +psutil +PyPDF2 \ No newline at end of file diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/__pycache__/__init__.cpython-311.pyc b/routers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..64b5c55 Binary files /dev/null and b/routers/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/__pycache__/admin_routes.cpython-311.pyc b/routers/__pycache__/admin_routes.cpython-311.pyc new file mode 100644 index 0000000..3cf302f Binary files /dev/null and b/routers/__pycache__/admin_routes.cpython-311.pyc differ diff --git a/routers/__pycache__/auth.cpython-311.pyc b/routers/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..313a94e Binary files /dev/null and b/routers/__pycache__/auth.cpython-311.pyc differ diff --git a/routers/__pycache__/health.cpython-311.pyc b/routers/__pycache__/health.cpython-311.pyc new file mode 100644 index 0000000..64b43ae Binary files /dev/null and b/routers/__pycache__/health.cpython-311.pyc differ diff --git a/routers/admin/admin_panel.py b/routers/admin/admin_panel.py new file mode 100644 index 0000000..8b20e09 --- /dev/null +++ b/routers/admin/admin_panel.py @@ -0,0 +1,2769 @@ +import os +from modules.logger_tool import initialise_logger + +from supabase import create_client, Client +from modules.auth.supabase_bearer import SupabaseBearer +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + Form, + Response, + File, + UploadFile, +) +from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from pydantic import BaseModel +from typing import Optional +import json +import csv +import io + +from modules.database.tools import neontology_tools as neon +import modules.database.schemas.entities as entities +from modules.database.admin.school_manager import SchoolManager +from modules.database.admin.graph_provider import GraphProvider + +# Initialize graph provider +graph_provider = GraphProvider() + +logger = initialise_logger( + __name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), "default", True +) + +# Initialize Supabase client with service role key for admin operations +supabase_url = os.getenv("SUPABASE_URL") +service_role_key = os.getenv("SERVICE_ROLE_KEY") +anon_key = os.getenv("ANON_KEY") + +logger.info(f"Initializing admin Supabase client with URL: {supabase_url}") +logger.debug(f"Service role key present: {bool(service_role_key)}") + +# Create admin client +admin_supabase: Client = create_client( + supabase_url=supabase_url, supabase_key=service_role_key +) + +# Set headers for admin operations +admin_supabase.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + "X-Client-Info": "supabase-py/0.0.1", +} + +# Set storage client headers explicitly +admin_supabase.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } +) + +# Regular client for non-admin operations +logger.info(f"Initializing regular Supabase client with URL: {supabase_url}") +supabase: Client = create_client(supabase_url=supabase_url, supabase_key=anon_key) + +# Set headers for regular operations +supabase.headers = {"apiKey": anon_key, "Authorization": f"Bearer {anon_key}"} + +# Use the existing SupabaseBearer for authentication +supabase_auth = SupabaseBearer() + + +# Admin authentication dependency +async def verify_admin(request: Request): + """Verify admin status and return admin data""" + try: + # Get access token from cookie + access_token = request.cookies.get("access_token") + if not access_token: + raise HTTPException(status_code=401, detail="No access token") + + logger.debug("Verifying admin access token") + + # Create a fresh service role client for this request + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + + try: + # Get user from token using service role client + user_response = service_client.auth.get_user(access_token) + user_id = user_response.user.id + + logger.debug(f"Verifying admin for user_id: {user_id}") + + # Use service role client to check admin profile + admin_result = ( + service_client.table("admin_profiles") + .select("*") + .eq("id", user_id) + .single() + .execute() + ) + + if not admin_result.data: + logger.error(f"No admin profile found for user {user_id}") + raise HTTPException(status_code=403, detail="Not an admin user") + + # Log admin data for debugging + logger.debug(f"Admin data: {admin_result.data}") + + # Create a new client with the user's access token for subsequent operations + user_client = create_client(supabase_url, service_role_key) + user_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + # Update storage client headers explicitly + user_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + ) + + # Store the client in the request state for use in other endpoints + request.state.supabase = user_client + + return admin_result.data + + except Exception as e: + logger.error(f"Error verifying admin token: {str(e)}") + if hasattr(e, "response"): + logger.error( + f"Response details: {e.response.text if hasattr(e.response, 'text') else e.response}" + ) + raise HTTPException( + status_code=401, detail="Invalid authentication credentials" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Admin verification failed: {str(e)}") + raise HTTPException( + status_code=401, detail="Invalid authentication credentials" + ) + + +# Models for request/response +class UserProfileBase(BaseModel): + email: str + display_name: Optional[str] = None + user_role: Optional[str] = "user" + is_active: Optional[bool] = True + metadata: Optional[dict] = {} + + +class AdminProfileBase(BaseModel): + email: str + display_name: Optional[str] = None + admin_role: Optional[str] = "admin" + is_super_admin: Optional[bool] = False + metadata: Optional[dict] = {} + + +# Setup templates +templates = Jinja2Templates(directory="templates") + +# Admin router +router = APIRouter(prefix="/api/admin", tags=["Admin Panel"]) + +# Serve static files +router.mount("/static", StaticFiles(directory="static"), name="static") + + +# Admin dashboard +@router.get("/", response_class=HTMLResponse) +async def admin_dashboard(request: Request): + access_token = request.cookies.get("access_token") + if not access_token: + return RedirectResponse(url="/api/admin/login", status_code=302) + + try: + # Verify token and get user claims using admin client + user_response = admin_supabase.auth.get_user(access_token) + user_id = user_response.user.id + + # Get admin profile using admin client + admin = ( + admin_supabase.table("admin_profiles") + .select("*") + .eq("id", user_id) + .single() + .execute() + ) + if not admin.data: + logger.error(f"No admin profile found for user {user_id}") + response = RedirectResponse(url="/api/admin/login", status_code=302) + response.delete_cookie("access_token") + return response + + # Process admin data + admin_data = admin.data + # Ensure updated_at is a string + if admin_data.get("updated_at") and not isinstance( + admin_data["updated_at"], str + ): + admin_data["updated_at"] = admin_data["updated_at"].isoformat() + + logger.debug(f"Admin dashboard data: {admin_data}") + + return templates.TemplateResponse( + "/dashboard/index.html", {"request": request, "admin": admin_data, "os": os} + ) + except Exception as e: + logger.error(f"Dashboard error: {str(e)}") + response = RedirectResponse(url="/api/admin/login", status_code=302) + response.delete_cookie("access_token") + return response + + +@router.get("/login", response_class=HTMLResponse) +async def login_page(request: Request, error: str = None, success: str = None): + """Render the login page""" + # Check if super admin initialization is needed + init_super_admin = os.getenv("INIT_SUPER_ADMIN", "false").lower() == "true" + + if init_super_admin: + # Check if any admin exists + try: + logger.info(f"Checking admin count using Supabase at URL: {supabase_url}") + admin_count = len( + admin_supabase.table("admin_profiles").select("id").execute().data + ) + logger.debug(f"Found {admin_count} admins in database") + if admin_count > 0: + init_super_admin = False + except Exception as e: + logger.error( + f"Error checking admin count using Supabase at {supabase_url}: {str(e)}" + ) + # Continue with the page load even if check fails + + return templates.TemplateResponse( + "admin/login.html", + { + "request": request, + "error": error, + "success": success, + "init_super_admin": init_super_admin, + "expected_super_admin_email": os.getenv("VITE_SUPER_ADMIN_EMAIL"), + }, + ) + + +@router.post("/login") +async def login( + request: Request, + response: Response, + email: str = Form(...), + password: str = Form(...), +): + """Handle login form submission""" + try: + logger.info( + f"Attempting login for email: {email} using Supabase at URL: {supabase_url}" + ) + + # Attempt to sign in with Supabase using service role client + try: + auth_response = admin_supabase.auth.sign_in_with_password( + {"email": email, "password": password} + ) + logger.debug("Successfully authenticated with Supabase auth") + except Exception as auth_error: + logger.error( + f"Authentication failed with Supabase at {supabase_url}: {str(auth_error)}" + ) + raise HTTPException(status_code=401, detail="Authentication failed") + + # Get the user's session + session = auth_response.session + user_id = session.user.id + + logger.debug(f"Successfully authenticated user: {user_id}") + + # Update admin_supabase client headers with the new session token + admin_supabase.headers.update( + { + "Authorization": f"Bearer {session.access_token}", + "apiKey": anon_key, # Use anon key for authenticated requests + } + ) + + # Update storage client headers explicitly + admin_supabase.storage._client.headers.update( + {"Authorization": f"Bearer {session.access_token}", "apiKey": anon_key} + ) + + logger.debug("Updated Supabase client headers with new session token") + + # Verify the user is an admin using service role client + try: + logger.info( + f"Checking admin profile for user {user_id} using Supabase at {supabase_url}" + ) + admin_result = ( + admin_supabase.table("admin_profiles") + .select("*") + .eq("id", user_id) + .single() + .execute() + ) + logger.debug(f"Admin profile query result: {admin_result}") + except Exception as profile_error: + logger.error( + f"Error checking admin profile at {supabase_url}: {str(profile_error)}" + ) + raise HTTPException(status_code=500, detail="Failed to verify admin status") + + if not admin_result.data: + logger.error(f"User {user_id} attempted to log in but is not an admin") + raise HTTPException(status_code=403, detail="Not an admin user") + + admin_data = admin_result.data + logger.debug(f"Admin profile found: {admin_data}") + + # Set the session cookie and redirect + response = RedirectResponse(url="/api/admin/", status_code=302) + response.set_cookie( + key="access_token", + value=session.access_token, + httponly=True, + secure=True, + samesite="lax", + max_age=3600, # 1 hour + ) + + # Update last login time + try: + logger.info( + f"Updating last login time for admin {user_id} at {supabase_url}" + ) + admin_supabase.table("admin_profiles").update({"updated_at": "now()"}).eq( + "id", user_id + ).execute() + except Exception as update_error: + logger.warning( + f"Failed to update last login time at {supabase_url}: {str(update_error)}" + ) + + return response + + except HTTPException: + raise + except Exception as e: + logger.error(f"Login error with Supabase at {supabase_url}: {str(e)}") + return templates.TemplateResponse( + "admin/login.html", + {"request": request, "error": "Invalid email or password"}, + status_code=401, + ) + + +@router.post("/logout") +async def logout(response: Response): + """Handle logout""" + response = RedirectResponse(url="/api/admin/login", status_code=303) + response.delete_cookie(key="access_token") + return response + + +# User management endpoints +@router.get("/users") +async def list_users(request: Request, admin: dict = Depends(verify_admin)): + """List all users with pagination""" + try: + # All admins can view users, no need for super admin check + users = admin_supabase.table("user_profiles").select("*").execute() + return templates.TemplateResponse( + "admin/users.html", + {"request": request, "users": users.data, "admin": admin}, + ) + except Exception as e: + logger.error(f"Error listing users: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/users/{user_id}") +async def get_user(request: Request, user_id: str, admin: dict = Depends(verify_admin)): + """Get user details""" + try: + user = ( + admin_supabase.table("user_profiles") + .select("*") + .eq("id", user_id) + .single() + .execute() + ) + return templates.TemplateResponse( + "admin/user_detail.html", + {"request": request, "user": user.data, "admin": admin}, + ) + except Exception as e: + logger.error(f"Error getting user {user_id}: {str(e)}") + raise HTTPException(status_code=404, detail="User not found") + + +@router.post("/users/{user_id}") +async def update_user( + user_id: str, user: UserProfileBase, admin: dict = Depends(verify_admin) +): + """Update user details""" + try: + # All admins can update basic user details + # But only super admins can modify user roles + if not admin.get("is_super_admin") and user.user_role != "user": + raise HTTPException( + status_code=403, detail="Only super admins can modify user roles" + ) + + updated_user = ( + admin_supabase.table("user_profiles") + .update(user.dict(exclude_unset=True)) + .eq("id", user_id) + .execute() + ) + return {"status": "success", "data": updated_user.data} + except Exception as e: + logger.error(f"Error updating user {user_id}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# Admin management endpoints (only accessible by super admins) +@router.get("/admins") +async def list_admins(request: Request, admin: dict = Depends(verify_admin)): + """List all admins""" + try: + logger.debug(f"Checking admin permissions for admin list. Admin data: {admin}") + + # Check if the admin is a super admin + if not admin.get("is_super_admin"): + logger.error( + f"Non-super admin attempted to access admin list. Admin data: {admin}" + ) + raise HTTPException( + status_code=403, detail="Only super admins can view admin list" + ) + + admins = admin_supabase.table("admin_profiles").select("*").execute() + return templates.TemplateResponse( + "admin/admins.html", + {"request": request, "admins": admins.data, "admin": admin}, + ) + except Exception as e: + logger.error(f"Error listing admins: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/admins") +async def create_admin( + admin_data: AdminProfileBase, current_admin: dict = Depends(verify_admin) +): + """Create a new admin""" + try: + # Special case for first super admin (only if no admins exist) + admin_count = len( + admin_supabase.table("admin_profiles").select("id").execute().data + ) + is_first_admin = admin_count == 0 + + # Only allow super admin creation by existing super admins (except for first admin) + if ( + admin_data.is_super_admin + and not is_first_admin + and not current_admin.get("is_super_admin") + ): + raise HTTPException( + status_code=403, detail="Only super admins can create super admins" + ) + + # For security, ensure email matches the expected super admin email for first admin + if is_first_admin: + expected_super_admin_email = os.getenv("VITE_SUPER_ADMIN_EMAIL") + if ( + not expected_super_admin_email + or admin_data.email != expected_super_admin_email + ): + raise HTTPException(status_code=403, detail="Invalid super admin email") + admin_data.is_super_admin = True # Force first admin to be super admin + elif not current_admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can create new admins" + ) + + # Create auth user with admin metadata + user_data = { + "email": admin_data.email, + "password": os.urandom(16).hex(), # Generate random password + "email_confirm": True, + "user_metadata": { + "is_admin": True, + "is_super_admin": admin_data.is_super_admin, + }, + } + + # Use supabase auth admin API with service role key for admin creation + service_role_client = create_client(supabase_url, os.getenv("SERVICE_ROLE_KEY")) + auth_user = service_role_client.auth.admin.create_user(user_data) + + # Create admin profile + admin_profile = admin_data.dict() + admin_profile["id"] = auth_user.user.id + + new_admin = ( + admin_supabase.table("admin_profiles").insert(admin_profile).execute() + ) + + # Send password reset email to new admin + service_role_client.auth.admin.generate_link( + {"type": "recovery", "email": admin_data.email} + ) + + return {"status": "success", "data": new_admin.data} + except Exception as e: + logger.error(f"Error creating admin: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/setup-super-admin/{user_id}") +async def setup_super_admin(user_id: str): + """Set up the initial super admin user""" + try: + # Get the expected super admin email from environment + expected_email = os.getenv("VITE_SUPER_ADMIN_EMAIL") + + # Get user details + user_response = admin_supabase.auth.admin.get_user_by_id(user_id) + user = user_response.user + + if not user or user.email != expected_email: + raise HTTPException( + status_code=403, detail="Unauthorized to become super admin" + ) + + # Update user metadata using auth admin API + updated_user = admin_supabase.auth.admin.update_user_by_id( + user_id, + user_attributes={ + "user_metadata": {"is_admin": True, "is_super_admin": True}, + "app_metadata": {"roles": ["admin", "super_admin"]}, + }, + ) + + # Create or update admin profile + admin_profile = { + "id": user_id, + "email": user.email, + "display_name": "Super Admin", + "admin_role": "admin", + "is_super_admin": True, + "metadata": {}, + } + + profile_result = ( + admin_supabase.table("admin_profiles").upsert(admin_profile).execute() + ) + + return { + "status": "success", + "message": "Super admin setup completed", + "data": profile_result.data, + } + + except Exception as e: + logger.error(f"Error setting up super admin: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/initialize-super-admin") +async def initialize_super_admin(admin_data: dict): + """Initialize the first super admin user with form data""" + try: + # Get the expected super admin email from environment + expected_email = os.getenv("VITE_SUPER_ADMIN_EMAIL") + is_dev_mode = os.getenv("DEV_MODE", "false").lower() == "true" + + logger.debug(f"Initializing super admin with data: {admin_data}") + logger.info(f"Using Supabase URL: {supabase_url}") + + if not expected_email: + raise HTTPException( + status_code=400, detail="Super admin email not configured" + ) + + # Check if any admin exists + try: + logger.info(f"Checking existing admins at {supabase_url}") + admin_count = len( + admin_supabase.table("admin_profiles").select("id").execute().data + ) + logger.debug(f"Found {admin_count} existing admins") + if admin_count > 0: + raise HTTPException( + status_code=400, detail="Super admin already exists" + ) + except Exception as count_error: + logger.error( + f"Error checking admin count at {supabase_url}: {str(count_error)}" + ) + raise HTTPException( + status_code=500, + detail=f"Failed to check existing admins: {str(count_error)}", + ) + + # Create the user with admin metadata + try: + logger.info(f"Creating auth user at {supabase_url}") + auth_user = admin_supabase.auth.admin.create_user( + { + "email": admin_data["email"], + "password": admin_data["password"], + "email_confirm": True, + "user_metadata": { + "is_admin": True, + "is_super_admin": True, + "display_name": admin_data["display_name"], + }, + "app_metadata": {"roles": ["admin", "super_admin"]}, + } + ) + user_id = auth_user.user.id + logger.debug(f"Created auth user with ID: {user_id}") + except Exception as auth_error: + logger.error( + f"Error creating auth user at {supabase_url}: {str(auth_error)}" + ) + raise HTTPException( + status_code=500, detail=f"Failed to create auth user: {str(auth_error)}" + ) + + # Create admin profile + try: + logger.info(f"Creating admin profile at {supabase_url}") + profile_result = ( + admin_supabase.table("admin_profiles") + .insert( + { + "id": user_id, + "email": admin_data["email"], + "display_name": admin_data["display_name"], + "admin_role": "admin", + "is_super_admin": True, + "metadata": {}, + } + ) + .execute() + ) + logger.debug(f"Admin profile creation result: {profile_result.data}") + except Exception as profile_error: + logger.error( + f"Error creating admin profile at {supabase_url}: {str(profile_error)}" + ) + raise HTTPException( + status_code=500, + detail=f"Failed to create admin profile: {str(profile_error)}", + ) + + return { + "status": "success", + "message": "Super admin initialized successfully. Please log in.", + "data": { + "email": admin_data["email"], + "display_name": admin_data["display_name"], + }, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error initializing super admin at {supabase_url}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# School management endpoints (only accessible by super admins) +@router.get("/schools/manage", response_class=HTMLResponse) +async def manage_schools(request: Request, admin: dict = Depends(verify_admin)): + """School management interface""" + try: + # Verify super admin status + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can manage schools" + ) + + # Get list of schools + schools = ( + admin_supabase.table("schools") + .select("*") + .order("establishment_name") + .execute() + ) + + return templates.TemplateResponse( + "admin/schools_manage.html", + {"request": request, "admin": admin, "schools": schools.data}, + ) + except Exception as e: + logger.error(f"Error in school management: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/schools/import") +async def import_schools( + file: UploadFile = File(...), admin: dict = Depends(verify_admin) +): + """Import schools from CSV file""" + try: + # Verify super admin status + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can import schools" + ) + + # Create a fresh service role client for database operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + + # Read and validate CSV file + content = await file.read() + csv_text = content.decode("utf-8-sig") # Handle BOM if present + csv_reader = csv.DictReader(io.StringIO(csv_text)) + + # Prepare data for batch insert + schools_data = [] + for row in csv_reader: + school_data = { + "urn": row.get("URN"), + "la_code": row.get("LA (code)"), + "la_name": row.get("LA (name)"), + "establishment_number": row.get("EstablishmentNumber"), + "establishment_name": row.get("EstablishmentName"), + "establishment_type": row.get("TypeOfEstablishment (name)"), + "establishment_type_group": row.get("EstablishmentTypeGroup (name)"), + "establishment_status": row.get("EstablishmentStatus (name)"), + "reason_establishment_opened": row.get( + "ReasonEstablishmentOpened (name)" + ), + "open_date": row.get("OpenDate"), + "reason_establishment_closed": row.get( + "ReasonEstablishmentClosed (name)" + ), + "close_date": row.get("CloseDate"), + "phase_of_education": row.get("PhaseOfEducation (name)"), + "statutory_low_age": row.get("StatutoryLowAge"), + "statutory_high_age": row.get("StatutoryHighAge"), + "boarders": row.get("Boarders (name)"), + "nursery_provision": row.get("NurseryProvision (name)"), + "official_sixth_form": row.get("OfficialSixthForm (name)"), + "gender": row.get("Gender (name)"), + "religious_character": row.get("ReligiousCharacter (name)"), + "religious_ethos": row.get("ReligiousEthos (name)"), + "diocese": row.get("Diocese (name)"), + "admissions_policy": row.get("AdmissionsPolicy (name)"), + "school_capacity": row.get("SchoolCapacity"), + "special_classes": row.get("SpecialClasses (name)"), + "census_date": row.get("CensusDate"), + "number_of_pupils": row.get("NumberOfPupils"), + "number_of_boys": row.get("NumberOfBoys"), + "number_of_girls": row.get("NumberOfGirls"), + "percentage_fsm": row.get("PercentageFSM"), + "trust_school_flag": row.get("TrustSchoolFlag (name)"), + "trusts_name": row.get("Trusts (name)"), + "school_sponsor_flag": row.get("SchoolSponsorFlag (name)"), + "school_sponsors_name": row.get("SchoolSponsors (name)"), + "federation_flag": row.get("FederationFlag (name)"), + "federations_name": row.get("Federations (name)"), + "ukprn": row.get("UKPRN"), + "fehe_identifier": row.get("FEHEIdentifier"), + "further_education_type": row.get("FurtherEducationType (name)"), + "ofsted_last_inspection": row.get("OfstedLastInsp"), + "last_changed_date": row.get("LastChangedDate"), + "street": row.get("Street"), + "locality": row.get("Locality"), + "address3": row.get("Address3"), + "town": row.get("Town"), + "county": row.get("County (name)"), + "postcode": row.get("Postcode"), + "school_website": row.get("SchoolWebsite"), + "telephone_num": row.get("TelephoneNum"), + "head_title": row.get("HeadTitle (name)"), + "head_first_name": row.get("HeadFirstName"), + "head_last_name": row.get("HeadLastName"), + "head_preferred_job_title": row.get("HeadPreferredJobTitle"), + "gssla_code": row.get("GSSLACode (name)"), + "parliamentary_constituency": row.get( + "ParliamentaryConstituency (name)" + ), + "urban_rural": row.get("UrbanRural (name)"), + "rsc_region": row.get("RSCRegion (name)"), + "country": row.get("Country (name)"), + "uprn": row.get("UPRN"), + "sen_stat": row.get("SENStat") == "true", + "sen_no_stat": row.get("SENNoStat") == "true", + "sen_unit_on_roll": row.get("SenUnitOnRoll"), + "sen_unit_capacity": row.get("SenUnitCapacity"), + "resourced_provision_on_roll": row.get("ResourcedProvisionOnRoll"), + "resourced_provision_capacity": row.get("ResourcedProvisionCapacity"), + } + + # Clean up empty strings and convert types + for key, value in school_data.items(): + if value == "": + school_data[key] = None + elif key in [ + "statutory_low_age", + "statutory_high_age", + "school_capacity", + "number_of_pupils", + "number_of_boys", + "number_of_girls", + "sen_unit_on_roll", + "sen_unit_capacity", + "resourced_provision_on_roll", + "resourced_provision_capacity", + ]: + if value: + try: + float_val = float(value) + int_val = int(float_val) + school_data[key] = int_val + except (ValueError, TypeError): + school_data[key] = None + elif key == "percentage_fsm": + if value: + try: + school_data[key] = float(value) + except (ValueError, TypeError): + school_data[key] = None + elif key in [ + "open_date", + "close_date", + "census_date", + "ofsted_last_inspection", + "last_changed_date", + ]: + if value: + try: + # Convert date from DD-MM-YYYY to YYYY-MM-DD + parts = value.split("-") + if len(parts) == 3: + school_data[key] = f"{parts[2]}-{parts[1]}-{parts[0]}" + else: + school_data[key] = None + except: + school_data[key] = None + + schools_data.append(school_data) + + # Batch insert schools using service role client + if schools_data: + result = ( + service_client.table("schools") + .upsert(schools_data, on_conflict="urn") # Update if URN already exists + .execute() + ) + + logger.info(f"Imported {len(schools_data)} schools") + return {"status": "success", "imported_count": len(schools_data)} + else: + raise HTTPException( + status_code=400, detail="No valid school data found in CSV" + ) + + except Exception as e: + logger.error(f"Error importing schools: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/schools/{school_id}") +async def view_school( + request: Request, school_id: str, admin: dict = Depends(verify_admin) +): + """View school details""" + try: + # Verify super admin status + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can view school details" + ) + + # Get school details + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Get latest statistics + stats = ( + admin_supabase.table("school_statistics") + .select("*") + .eq("school_id", school_id) + .order("census_date", desc=True) + .limit(1) + .execute() + ) + + return templates.TemplateResponse( + "admin/school_detail.html", + { + "request": request, + "admin": admin, + "school": school.data, + "statistics": stats.data[0] if stats.data else None, + }, + ) + except Exception as e: + logger.error(f"Error viewing school: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/schools/{school_id}") +async def delete_school(school_id: str, admin: dict = Depends(verify_admin)): + """Delete a school""" + try: + # Verify super admin status + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can delete schools" + ) + + # Delete school statistics first (due to foreign key constraint) + await admin_supabase.table("school_statistics").delete().eq( + "school_id", school_id + ).execute() + + # Delete school + result = ( + await admin_supabase.table("schools").delete().eq("id", school_id).execute() + ) + + return {"status": "success"} + except Exception as e: + logger.error(f"Error deleting school: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/initialize-schools-database") +async def initialize_schools_database(admin: dict = Depends(verify_admin)): + """Initialize the cc.institutes database (super admin only)""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, + detail="Only super admins can initialize the schools database", + ) + + school_manager = SchoolManager() + result = school_manager.create_schools_database() + + if result["status"] == "error": + raise HTTPException(status_code=500, detail=result["message"]) + + return result + except Exception as e: + logger.error(f"Error initializing schools database: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/schools/{school_id}/initialize-node") +async def initialize_school_node(school_id: str, admin: dict = Depends(verify_admin)): + """Initialize a school node in the cc.institutes database""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can initialize school nodes" + ) + + # Create a fresh service role client for database operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + + # Get school data from Supabase using service role client + school = ( + service_client.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Create school manager and verify database exists + school_manager = SchoolManager() + + # Verify cc.institutes database exists + try: + with school_manager.driver.session() as session: + result = session.run("SHOW DATABASES") + databases = [record["name"] for record in result] + if "cc.institutes" not in databases: + logger.error("cc.institutes database does not exist") + raise HTTPException( + status_code=500, + detail="Schools database not initialized. Please initialize database first.", + ) + except Exception as db_error: + logger.error(f"Error checking database existence: {str(db_error)}") + raise HTTPException( + status_code=500, + detail=f"Failed to verify database existence: {str(db_error)}", + ) + + # Create school node using SchoolManager + try: + result = school_manager.create_school_node(school.data) + + if result["status"] == "error": + raise Exception(result["message"]) + + return { + "status": "success", + "message": "School node created successfully", + "node_id": f"School_{school.data['urn']}", + } + + except Exception as node_error: + logger.error(f"Error creating school node: {str(node_error)}") + raise HTTPException( + status_code=500, + detail=f"Failed to create school node: {str(node_error)}", + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error initializing school node: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/schools/{school_id}/graph-status") +async def check_school_graph_status( + school_id: str, admin: dict = Depends(verify_admin) +): + """Check if a school node exists in the graph database""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, + detail="Only super admins can check school graph status", + ) + + # Get school data from Supabase + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Check if node exists in Neo4j and get its properties + school_manager = SchoolManager() + with school_manager.driver.session(database="cc.institutes") as session: + result = session.run( + """ + MATCH (s:School {unique_id: $unique_id}) + RETURN s + """, + {"unique_id": f"School_{school.data['urn']}"}, + ) + record = result.single() + exists = record is not None + + # If node exists, get its properties + node_data = None + if exists: + node = record["s"] + node_data = dict(node.items()) # Convert node properties to dict + + return {"exists": exists, "node_data": node_data} + except Exception as e: + logger.error(f"Error checking school graph status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/check-schools-database") +async def check_schools_database(admin: dict = Depends(verify_admin)): + """Check if the cc.institutes database exists (super admin only)""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check database status" + ) + + school_manager = SchoolManager() + with school_manager.driver.session() as session: + # Try to list databases + result = session.run("SHOW DATABASES") + databases = [record["name"] for record in result] + exists = "cc.institutes" in databases + + return {"exists": exists} + except Exception as e: + logger.error(f"Error checking schools database: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/check-storage") +async def check_storage(admin: dict = Depends(verify_admin)): + """Check status of storage buckets""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check storage status" + ) + + try: + logger.info( + f"Checking storage buckets using Supabase at URL: {supabase_url}" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Define the buckets we want to check + required_buckets = [ + {"name": "User Files", "id": "cc.users", "exists": False}, + {"name": "School Files", "id": "cc.institutes", "exists": False}, + ] + + # List buckets and check existence + all_buckets = service_client.storage.list_buckets() + existing_bucket_ids = [bucket.id for bucket in all_buckets] + logger.debug(f"Found buckets via list_buckets: {existing_bucket_ids}") + + # Update bucket existence status + for bucket in required_buckets: + bucket["exists"] = bucket["id"] in existing_bucket_ids + logger.debug(f"Bucket {bucket['id']} exists: {bucket['exists']}") + + logger.debug(f"Storage check result: required_buckets={required_buckets}") + + return {"buckets": required_buckets, "schema_ready": True} + + except Exception as e: + logger.error(f"Error checking storage: {str(e)}") + if hasattr(e, "response"): + logger.error( + f"Response details: {e.response.text if hasattr(e.response, 'text') else e.response}" + ) + raise HTTPException( + status_code=500, detail=f"Error checking storage: {str(e)}" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in check_storage: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/initialize-storage") +async def initialize_storage(admin: dict = Depends(verify_admin)): + """Initialize storage buckets and policies for schools""" + try: + # Verify super admin status + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can initialize storage" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + "X-Client-Info": "supabase-py/0.0.1", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # First, ensure the bucket RLS policy exists + bucket_policy = """ + -- First, enable RLS on the buckets table if not already enabled + alter table storage.buckets enable row level security; + + -- Drop existing policies to ensure clean slate + drop policy if exists "Service role has full access to buckets" on storage.buckets; + drop policy if exists "Authenticated users can create buckets" on storage.buckets; + drop policy if exists "Bucket creation requires service role" on storage.buckets; + + -- Create service role policy for full access + create policy "Service role has full access to buckets" + on storage.buckets + as permissive + for all + to authenticated + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); + """ + + try: + # Execute bucket policy using service role client + service_client.postgrest.rpc("exec_sql", {"query": bucket_policy}).execute() + logger.info("Successfully created bucket RLS policy") + except Exception as e: + logger.warning( + f"Bucket policy creation warning (may already exist): {str(e)}" + ) + + # Define buckets to create + buckets_to_create = [ + { + "id": "cc.users", + "name": "User Files", + "public": False, + "file_size_limit": 52428800, + "allowed_mime_types": [ + "image/*", + "video/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "text/csv", + "application/json", + ], + }, + { + "id": "cc.institutes", + "name": "School Files", + "public": False, + "file_size_limit": 52428800, + "allowed_mime_types": [ + "image/*", + "video/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "text/csv", + "application/json", + ], + }, + ] + + # Get list of existing buckets using service role client + try: + all_buckets = service_client.storage.list_buckets() + existing_bucket_ids = [bucket.id for bucket in all_buckets] + logger.debug(f"Found existing buckets: {existing_bucket_ids}") + except Exception as e: + logger.error(f"Error listing buckets: {str(e)}") + existing_bucket_ids = [] + + created_buckets = [] + for bucket in buckets_to_create: + try: + if bucket["id"] in existing_bucket_ids: + logger.info(f"Bucket {bucket['id']} already exists") + created_buckets.append(bucket["id"]) + else: + # Create bucket if it doesn't exist using service role client + logger.debug( + f"Creating bucket {bucket['id']} with options: {bucket}" + ) + try: + response = service_client.storage.create_bucket( + bucket["id"], + options={ + "public": bucket["public"], + "file_size_limit": bucket["file_size_limit"], + "allowed_mime_types": bucket["allowed_mime_types"], + }, + ) + logger.info(f"Created bucket {bucket['id']}") + logger.debug(f"Bucket creation response: {response}") + created_buckets.append(bucket["id"]) + except Exception as bucket_error: + logger.error( + f"Detailed bucket creation error for {bucket['id']}: {str(bucket_error)}" + ) + if hasattr(bucket_error, "response"): + logger.error( + f"Error response: {bucket_error.response.text if hasattr(bucket_error.response, 'text') else bucket_error.response}" + ) + raise bucket_error + except Exception as e: + logger.warning(f"Error with bucket {bucket['id']}: {str(e)}") + + # Create object-level RLS policies + object_policies = [ + """ + -- Enable RLS on objects table if not already enabled + alter table storage.objects enable row level security; + + -- Drop existing policies + drop policy if exists "Users can read own files" on storage.objects; + drop policy if exists "Users can upload own files" on storage.objects; + drop policy if exists "Users can update own files" on storage.objects; + drop policy if exists "Users can delete own files" on storage.objects; + drop policy if exists "Anyone can read school files" on storage.objects; + drop policy if exists "Only admins can manage school files" on storage.objects; + drop policy if exists "Service role has full access to objects" on storage.objects; + drop policy if exists "Admins can create signed URLs" on storage.objects; + + -- Create user files policies + create policy "Users can read own files" + on storage.objects for select + using ( + bucket_id = 'cc.users' + and ( + path_tokens[1] = auth.uid()::text + or exists ( + select 1 from auth.users + where auth.uid() = auth.users.id + and raw_user_meta_data->>'is_admin' = 'true' + ) + ) + ); + + create policy "Users can upload own files" + on storage.objects for insert + with check ( + bucket_id = 'cc.users' + and path_tokens[1] = auth.uid()::text + ); + + create policy "Users can update own files" + on storage.objects for update + using ( + bucket_id = 'cc.users' + and path_tokens[1] = auth.uid()::text + ); + + create policy "Users can delete own files" + on storage.objects for delete + using ( + bucket_id = 'cc.users' + and path_tokens[1] = auth.uid()::text + ); + + -- Create school files policies + create policy "Anyone can read school files" + on storage.objects for select + using (bucket_id = 'cc.institutes'); + + create policy "Only admins can manage school files" + on storage.objects for all + using ( + bucket_id = 'cc.institutes' + and ( + auth.role() = 'service_role' + or exists ( + select 1 from auth.users + where auth.uid() = auth.users.id + and raw_user_meta_data->>'is_admin' = 'true' + ) + ) + ) + with check ( + bucket_id = 'cc.institutes' + and ( + auth.role() = 'service_role' + or exists ( + select 1 from auth.users + where auth.uid() = auth.users.id + and raw_user_meta_data->>'is_admin' = 'true' + ) + ) + ); + + -- Create service role policy + create policy "Service role has full access to objects" + on storage.objects for all + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); + + -- Create signed URL policy + create policy "Admins can create signed URLs" + on storage.objects for select + using ( + exists ( + select 1 from auth.users + where auth.uid() = auth.users.id + and raw_user_meta_data->>'is_admin' = 'true' + ) + ); + """ + ] + + # Apply object-level policies using service role client + for policy in object_policies: + try: + service_client.postgrest.rpc("exec_sql", {"query": policy}).execute() + logger.info("Successfully created object RLS policies") + except Exception as e: + logger.warning( + f"Object policy creation warning (may already exist): {str(e)}" + ) + + return { + "status": "success", + "message": "Storage buckets and policies initialized", + "created_buckets": created_buckets, + } + except Exception as e: + logger.error(f"Error initializing storage: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/storage", response_class=HTMLResponse) +async def storage_management(request: Request, admin: dict = Depends(verify_admin)): + """Storage management interface""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, + detail="Only super admins can access storage management", + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Get bucket information using storage API + try: + logger.info(f"Listing buckets from Supabase storage at URL {supabase_url}") + buckets = service_client.storage.list_buckets() + # Convert bucket objects to dictionaries for template + buckets_data = [ + { + "id": bucket.id, + "name": bucket.name, + "public": bucket.public, + "created_at": bucket.created_at, + "updated_at": bucket.updated_at, + "file_size_limit": bucket.file_size_limit, + "allowed_mime_types": bucket.allowed_mime_types, + } + for bucket in buckets + if bucket.id in ["cc.users", "cc.institutes"] + ] + + logger.debug(f"Found buckets: {buckets_data}") + + except Exception as e: + logger.error(f"Error getting bucket information: {str(e)}") + if hasattr(e, "response"): + logger.error( + f"Response details: {e.response.text if hasattr(e.response, 'text') else e.response}" + ) + buckets_data = [] + + return templates.TemplateResponse( + "admin/storage_management.html", + {"request": request, "admin": admin, "buckets": buckets_data}, + ) + except Exception as e: + logger.error(f"Error in storage management: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/storage/{bucket_id}/contents") +async def list_bucket_contents( + request: Request, + bucket_id: str, + path: str = "", + admin: dict = Depends(verify_admin), +): + """List contents of a storage bucket""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can list bucket contents" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Verify bucket exists using storage API + try: + logger.info( + f"Getting bucket {bucket_id} from Supabase storage at URL {supabase_url}" + ) + bucket = service_client.storage.get_bucket(bucket_id) + bucket_data = { + "id": bucket.id, + "name": bucket.name, + "public": bucket.public, + "created_at": bucket.created_at, + "updated_at": bucket.updated_at, + "file_size_limit": bucket.file_size_limit, + "allowed_mime_types": bucket.allowed_mime_types, + } + logger.debug(f"Found bucket: {bucket_data}") + except Exception as e: + logger.error(f"Error getting bucket {bucket_id}: {str(e)}") + if hasattr(e, "response"): + logger.error( + f"Response details: {e.response.text if hasattr(e.response, 'text') else e.response}" + ) + raise HTTPException(status_code=404, detail="Bucket not found") + + # List objects in the bucket + try: + logger.info(f"Listing files in bucket {bucket_id} at path '{path}'") + # Use storage API to list files with service role client + files = service_client.storage.from_(bucket_id).list(path) + logger.debug(f"Files in bucket {bucket_id}: {files}") + + # Organize objects into folders and files + contents = {"folders": set(), "files": []} + + for file in files: + file_path = file["name"] + if path: + # Remove the prefix path if we're in a subfolder + if file_path.startswith(path): + file_path = file_path[len(path) :].lstrip("/") + + # Split path into parts + parts = file_path.split("/") + + if len(parts) > 1: + # This is in a subfolder + contents["folders"].add(parts[0]) + else: + # This is a file in the current directory + # Add full path back if we're in a subfolder + if path: + file["name"] = f"{path}/{file_path}" + contents["files"].append(file) + + contents["folders"] = sorted(list(contents["folders"])) + contents["files"] = sorted(contents["files"], key=lambda x: x["name"]) + + logger.debug(f"Processed contents: {contents}") + + except Exception as e: + logger.error(f"Error listing bucket contents: {str(e)}") + if hasattr(e, "response"): + logger.error( + f"Response details: {e.response.text if hasattr(e.response, 'text') else e.response}" + ) + contents = {"folders": [], "files": []} + + if request.headers.get("X-Requested-With") == "XMLHttpRequest": + return contents + else: + return templates.TemplateResponse( + "admin/storage_contents.html", + { + "request": request, + "admin": admin, + "bucket": bucket_data, + "contents": contents, + "current_path": path, + }, + ) + + except Exception as e: + logger.error(f"Error listing bucket contents: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("/storage/{bucket_id}/objects/{object_path:path}") +async def delete_object( + bucket_id: str, object_path: str, admin: dict = Depends(verify_admin) +): + """Delete an object from a storage bucket""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can delete objects" + ) + + # Delete the object using storage API + try: + admin_supabase.storage.from_(bucket_id).remove([object_path]) + return {"status": "success", "message": "Object deleted"} + except Exception as e: + logger.error( + f"Error deleting object {object_path} from bucket {bucket_id}: {str(e)}" + ) + raise HTTPException( + status_code=500, detail=f"Failed to delete object: {str(e)}" + ) + + except Exception as e: + logger.error(f"Error in delete object handler: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/storage/manage", response_class=HTMLResponse) +async def manage_school_storage(request: Request, admin: dict = Depends(verify_admin)): + """School files storage management interface""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can manage storage" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Get list of files from the schools bucket + try: + logger.info( + f"Listing files from Supabase storage at URL {supabase_url}, bucket: cc.institutes" + ) + + # First, list all root level items + files = service_client.storage.from_("cc.institutes").list() + logger.debug(f"Root level files from Supabase: {files}") + + # Process files to ensure we have complete path information + processed_files = [] + for file in files: + # Convert file object to dict if it's not already + if not isinstance(file, dict): + file = { + "name": file.name, + "id": getattr(file, "id", None), + "updated_at": getattr(file, "updated_at", None), + "created_at": getattr(file, "created_at", None), + "last_accessed_at": getattr(file, "last_accessed_at", None), + "metadata": getattr(file, "metadata", {}), + } + + # Get the school URN from the file path + school_urn = ( + file["name"].split("/")[0] if "/" in file["name"] else file["name"] + ) + + # If this is a school URN directory, check for tldraw.json + if not file["name"].endswith("tldraw.json"): + try: + # List contents of this folder + subfiles = service_client.storage.from_("cc.institutes").list( + school_urn + ) + logger.debug(f"Subfiles for {school_urn}: {subfiles}") + + for subfile in subfiles: + if isinstance(subfile, dict): + subfile_name = subfile["name"] + else: + subfile_name = subfile.name + + if subfile_name.endswith("tldraw.json"): + # Get school info for metadata + try: + school = ( + service_client.table("schools") + .select("*") + .eq("urn", school_urn) + .single() + .execute() + ) + metadata = ( + { + "establishment_name": school.data.get( + "establishment_name" + ), + "establishment_type": school.data.get( + "establishment_type" + ), + } + if school.data + else {} + ) + except Exception as school_error: + logger.warning( + f"Could not get school info for {school_urn}: {str(school_error)}" + ) + metadata = {} + + # Add to processed files with full path + full_path = f"{school_urn}/{subfile_name}" + processed_files.append( + { + "name": full_path, + "id": ( + getattr(subfile, "id", None) + if not isinstance(subfile, dict) + else subfile.get("id") + ), + "updated_at": ( + getattr(subfile, "updated_at", None) + if not isinstance(subfile, dict) + else subfile.get("updated_at") + ), + "created_at": ( + getattr(subfile, "created_at", None) + if not isinstance(subfile, dict) + else subfile.get("created_at") + ), + "metadata": metadata, + } + ) + except Exception as e: + logger.warning( + f"Error listing contents of {school_urn}: {str(e)}" + ) + else: + # This is already a tldraw.json file, get its school info + school_urn = file["name"].split("/")[0] + try: + school = ( + service_client.table("schools") + .select("*") + .eq("urn", school_urn) + .single() + .execute() + ) + file["metadata"] = ( + { + "establishment_name": school.data.get( + "establishment_name" + ), + "establishment_type": school.data.get( + "establishment_type" + ), + } + if school.data + else {} + ) + except Exception as school_error: + logger.warning( + f"Could not get school info for {school_urn}: {str(school_error)}" + ) + processed_files.append(file) + + logger.debug(f"Processed files: {processed_files}") + files = processed_files + + except Exception as e: + logger.error(f"Error listing school files: {str(e)}") + if hasattr(e, "response"): + logger.error( + f"Response details: {e.response.text if hasattr(e.response, 'text') else e.response}" + ) + files = [] + + return templates.TemplateResponse( + "admin/storage_manage.html", + {"request": request, "admin": admin, "files": files}, + ) + except Exception as e: + logger.error(f"Error in storage management: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/storage/{bucket_id}/view/{file_path:path}") +async def view_file( + request: Request, + bucket_id: str, + file_path: str, + admin: dict = Depends(verify_admin), +): + """View a file from storage""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can view files" + ) + + # Clean up file path and ensure it includes the school URN + file_path = file_path.strip("/") + if not "/" in file_path: + raise HTTPException( + status_code=400, detail="Invalid file path. Must include school URN." + ) + + logger.info( + f"Attempting to view file from Supabase storage at URL {supabase_url}, bucket: {bucket_id}, path: {file_path}" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Get signed URL for the file + try: + # Create signed URL that expires in 1 hour (3600 seconds) + file_url = service_client.storage.from_(bucket_id).create_signed_url( + path=file_path, expires_in=3600 + ) + + if not file_url or "signedURL" not in file_url: + logger.error( + f"Failed to generate signed URL from Supabase at {supabase_url}" + ) + raise HTTPException( + status_code=404, detail="Failed to generate signed URL" + ) + + # Replace internal Kong URL with public Supabase URL + public_url = file_url["signedURL"].replace( + "http://kong:8000", + os.getenv("VITE_SUPABASE_URL"), + ) + + logger.info( + f"Successfully generated signed URL from Supabase at {supabase_url}" + ) + logger.debug(f"Original URL: {file_url['signedURL']}") + logger.debug(f"Public URL: {public_url}") + + return {"url": public_url} + + except Exception as e: + logger.error( + f"Error getting signed URL from Supabase at {supabase_url} for {file_path} from bucket {bucket_id}: {str(e)}" + ) + raise HTTPException(status_code=404, detail=str(e)) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error viewing file: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/storage/{bucket_id}/download/{file_path:path}") +async def download_file( + request: Request, + bucket_id: str, + file_path: str, + admin: dict = Depends(verify_admin), +): + """Download a file from storage""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can download files" + ) + + # Clean up file path and ensure it includes the school URN + file_path = file_path.strip("/") + if not "/" in file_path: + raise HTTPException( + status_code=400, detail="Invalid file path. Must include school URN." + ) + + logger.info( + f"Attempting to download file from Supabase storage at URL {supabase_url}, bucket: {bucket_id}, path: {file_path}" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Get signed URL for the file + try: + # Create signed URL that expires in 1 hour (3600 seconds) + file_url = service_client.storage.from_(bucket_id).create_signed_url( + path=file_path, expires_in=3600 + ) + + if not file_url or "signedURL" not in file_url: + logger.error( + f"Failed to generate download URL from Supabase at {supabase_url}" + ) + raise HTTPException( + status_code=404, detail="Failed to generate signed URL" + ) + + # Replace internal Kong URL with public Supabase URL + public_url = file_url["signedURL"].replace( + "http://kong:8000", + os.getenv("VITE_SUPABASE_URL"), + ) + + logger.info( + f"Successfully generated download URL from Supabase at {supabase_url}" + ) + logger.debug(f"Original URL: {file_url['signedURL']}") + logger.debug(f"Public URL: {public_url}") + + return {"url": public_url} + + except Exception as e: + logger.error( + f"Error getting download URL from Supabase at {supabase_url} for {file_path} from bucket {bucket_id}: {str(e)}" + ) + raise HTTPException(status_code=404, detail=str(e)) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error downloading file: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/storage/{bucket_id}/upload/{school_urn}") +async def upload_file( + bucket_id: str, + school_urn: str, + file: UploadFile = File(...), + admin: dict = Depends(verify_admin), +): + """Upload a file to a storage bucket""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can upload files" + ) + + # Create a fresh service role client for storage operations + service_client = create_client(supabase_url, service_role_key) + service_client.headers = { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + service_client.storage._client.headers.update( + { + "apiKey": service_role_key, + "Authorization": f"Bearer {service_role_key}", + "Content-Type": "application/json", + } + ) + + # Verify bucket exists + try: + bucket = service_client.storage.get_bucket(bucket_id) + except Exception as e: + logger.error(f"Error getting bucket {bucket_id}: {str(e)}") + raise HTTPException(status_code=404, detail="Bucket not found") + + # Get school data to verify URN and get metadata + try: + school = ( + service_client.table("schools") + .select("*") + .eq("urn", school_urn) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + except Exception as e: + logger.error(f"Error getting school data: {str(e)}") + raise HTTPException(status_code=404, detail="School not found") + + # Construct file path + file_path = f"{school_urn}/tldraw.json" + + # Read file content + content = await file.read() + + try: + # Upload file with metadata + result = service_client.storage.from_(bucket_id).upload( + path=file_path, + file=content, + file_options={ + "content-type": "application/json", + "x-upsert": "true", # Update if exists + }, + ) + + # Update file metadata + metadata = { + "establishment_name": school.data.get("establishment_name"), + "establishment_type": school.data.get("establishment_type"), + "size": len(content), + "mimetype": "application/json", + } + + # Try to update metadata (this might not be supported by all storage providers) + try: + service_client.storage.from_(bucket_id).update_file_metadata( + path=file_path, metadata=metadata + ) + except Exception as metadata_error: + logger.warning(f"Could not update file metadata: {str(metadata_error)}") + + return { + "status": "success", + "message": "File uploaded successfully", + "path": file_path, + } + + except Exception as upload_error: + logger.error(f"Error uploading file: {str(upload_error)}") + raise HTTPException( + status_code=500, detail=f"Failed to upload file: {str(upload_error)}" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error in upload handler: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/schools/{school_id}/create-private-database") +async def create_private_database(school_id: str, admin: dict = Depends(verify_admin)): + """Create private database for school""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can create private databases" + ) + + # Get school data + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Create school manager and create private database + school_manager = SchoolManager() + result = school_manager.create_private_database(school.data) + + if result["status"] == "error": + raise HTTPException(status_code=500, detail=result["message"]) + + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error creating private database: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/schools/{school_id}/create-basic-structure") +async def create_basic_structure(school_id: str, admin: dict = Depends(verify_admin)): + """Create basic school structure in both public and private databases""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can create school structure" + ) + + # Get school data + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Create school node + school_node = entities.SchoolNode( + unique_id=f"School_{school.data['urn']}", + path=f"/schools/cc.institutes/{school.data['urn']}", + urn=school.data["urn"], + establishment_number=school.data["establishment_number"], + establishment_name=school.data["establishment_name"], + establishment_type=school.data["establishment_type"], + establishment_status=school.data["establishment_status"], + phase_of_education=( + school.data["phase_of_education"] + if school.data["phase_of_education"] not in [None, ""] + else None + ), + statutory_low_age=( + int(school.data["statutory_low_age"]) + if school.data.get("statutory_low_age") is not None + else 0 + ), + statutory_high_age=( + int(school.data["statutory_high_age"]) + if school.data.get("statutory_high_age") is not None + else 0 + ), + religious_character=( + school.data.get("religious_character") + if school.data.get("religious_character") not in [None, ""] + else None + ), + school_capacity=( + int(school.data["school_capacity"]) + if school.data.get("school_capacity") is not None + else 0 + ), + school_website=school.data.get("school_website", ""), + ofsted_rating=( + school.data.get("ofsted_rating") + if school.data.get("ofsted_rating") not in [None, ""] + else None + ), + ) + + # Create school manager + school_manager = SchoolManager() + + # Ensure school node exists in the private database + with school_manager.neontology as neo: + # Create/merge in private database + private_db_name = f"cc.institutes.{school.data['urn']}" + neo.create_or_merge_node( + school_node, database=private_db_name, operation="merge" + ) + + # Create structure in public database + public_result = school_manager.create_basic_structure( + school_node, "cc.institutes" + ) + if public_result["status"] == "error": + raise HTTPException( + status_code=500, + detail=f"Error creating public structure: {public_result['message']}", + ) + + # Create structure in private database + private_result = school_manager.create_basic_structure( + school_node, private_db_name + ) + if private_result["status"] == "error": + raise HTTPException( + status_code=500, + detail=f"Error creating private structure: {private_result['message']}", + ) + + return { + "status": "success", + "message": "School structure created in both databases", + "public_result": public_result, + "private_result": private_result, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error creating school structure: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/schools/{school_id}/create-detailed-structure") +async def create_detailed_structure( + school_id: str, file: UploadFile = File(...), admin: dict = Depends(verify_admin) +): + """Create detailed school structure from Excel file""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, + detail="Only super admins can create detailed structure", + ) + + # Get school data + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Create school node + school_node = entities.SchoolNode( + unique_id=f"School_{school.data['urn']}", + urn=school.data["urn"], + establishment_number=school.data["establishment_number"], + establishment_name=school.data["establishment_name"], + establishment_type=school.data["establishment_type"], + establishment_status=school.data["establishment_status"], + phase_of_education=school.data["phase_of_education"], + statutory_low_age=( + int(school.data["statutory_low_age"]) + if school.data.get("statutory_low_age") is not None + else 0 + ), + statutory_high_age=( + int(school.data["statutory_high_age"]) + if school.data.get("statutory_high_age") is not None + else 0 + ), + religious_character=( + school.data.get("religious_character") + if school.data.get("religious_character") not in [None, ""] + else None + ), + school_capacity=( + int(school.data["school_capacity"]) + if school.data.get("school_capacity") is not None + else 0 + ), + school_website=school.data.get("school_website", ""), + ofsted_rating=( + school.data.get("ofsted_rating") + if school.data.get("ofsted_rating") not in [None, ""] + else None + ), + path=f"/schools/cc.institutes/{school.data['urn']}", + ) + + # Read file content + content = await file.read() + + # Create school manager + school_manager = SchoolManager() + + # Create detailed structure in public database + public_result = school_manager.create_detailed_structure( + school_node, "cc.institutes", content + ) + if public_result["status"] == "error": + raise HTTPException( + status_code=500, + detail=f"Error creating public detailed structure: {public_result['message']}", + ) + + # Create detailed structure in private database + private_db_name = f"cc.institutes.{school.data['urn']}" + private_result = school_manager.create_detailed_structure( + school_node, private_db_name, content + ) + if private_result["status"] == "error": + raise HTTPException( + status_code=500, + detail=f"Error creating private detailed structure: {private_result['message']}", + ) + + return { + "status": "success", + "message": "Detailed structure created in both databases", + "public_result": public_result, + "private_result": private_result, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error creating detailed structure: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +def check_private_school_database(school_urn): + """Checks if private database exists for school""" + try: + private_db_name = f"cc.institutes.{school_urn}" + with graph_provider.neontology as neo: + result = neo.run_query("SHOW DATABASES", {}) + databases = [record["name"] for record in result] + exists = private_db_name in databases + return {"exists": exists} + except Exception as e: + logger.error(f"Error checking private database: {str(e)}") + raise + + +@router.get("/schools/{school_id}/check-private-school-database") +async def check_private_school_database_endpoint( + school_id: str, admin: dict = Depends(verify_admin) +): + """Check if private database exists for school""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check private database" + ) + + # Get school data + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + return check_private_school_database(school.data["urn"]) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error checking private database: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/schools/{school_id}/check-structure-status") +async def check_structure_status(school_id: str, admin: dict = Depends(verify_admin)): + """Check the structure status for a school in both public and private databases""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check structure status" + ) + + # Get basic structure status + basic_status = await check_basic_structure(school_id, admin) + + # Get detailed structure status + detailed_status = await check_detailed_structure(school_id, admin) + + # Combine results + return { + "public_database": { + "basic": basic_status["public_database"], + "detailed": detailed_status["public_database"], + }, + "private_database": { + "exists": basic_status["private_database"]["exists"], + "basic": basic_status["private_database"]["status"], + "detailed": detailed_status["private_database"]["status"], + }, + } + + except Exception as e: + logger.error(f"Error checking structure status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/schema") +async def schema_page(request: Request, admin: dict = Depends(verify_admin)): + """Schema management interface""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can manage schema" + ) + + return templates.TemplateResponse( + "admin/schema_details.html", {"request": request, "admin": admin} + ) + except Exception as e: + logger.error(f"Error in schema management: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/check-schema") +async def check_schema(admin: dict = Depends(verify_admin)): + """Check schema status""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check schema" + ) + + # Get schema status from both databases + public_schema = graph_provider.check_schema_status("cc.institutes") + + # Combine results + combined_schema = { + "constraints": list(set(public_schema["constraints"])), + "constraints_count": len(set(public_schema["constraints"])), + "constraints_valid": public_schema["constraints_valid"], + "indexes": list(set(public_schema["indexes"])), + "indexes_count": len(set(public_schema["indexes"])), + "indexes_valid": public_schema["indexes_valid"], + "labels": list(set(public_schema["labels"])), + "labels_count": len(set(public_schema["labels"])), + "labels_valid": public_schema["labels_valid"], + } + + return JSONResponse(content=combined_schema) + except Exception as e: + logger.error(f"Error checking schema: {str(e)}") + return JSONResponse(content={"error": str(e)}, status_code=500) + + +@router.get("/schema-definition") +async def get_schema_definition(admin: dict = Depends(verify_admin)): + """Get schema definition""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can view schema definition" + ) + + schema_info = graph_provider.get_schema_info() + return JSONResponse(content=schema_info) + except Exception as e: + logger.error(f"Error getting schema definition: {str(e)}") + return JSONResponse(content={"error": str(e)}, status_code=500) + + +@router.post("/initialize-schema") +async def initialize_schema(admin: dict = Depends(verify_admin)): + """Initialize schema for both databases""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can initialize schema" + ) + + # Initialize schema for both databases + graph_provider.initialize_schema("cc.institutes") + return JSONResponse(content={"message": "Schema initialized successfully"}) + except Exception as e: + logger.error(f"Error initializing schema: {str(e)}") + return JSONResponse(content={"error": str(e)}, status_code=500) + + +@router.get("/schools/{school_id}/check-basic-structure") +async def check_basic_structure(school_id: str, admin: dict = Depends(verify_admin)): + """Check if basic structure exists for a school""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check structure status" + ) + + # Get school data + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Create graph provider for Neo4j operations + provider = GraphProvider() + + # Generate the unique ID for the school + school_unique_id = f"School_{school.data['urn']}" + + # Check basic structure in public database + with provider.neontology as neo: + basic_query = """ + MATCH (s:School {unique_id: $school_id}) + + // Get all nodes connected to school for debugging + CALL { + WITH s + MATCH (n) + WHERE n.unique_id CONTAINS s.unique_id + RETURN COLLECT({label: labels(n)[0], id: n.unique_id}) as debug_nodes + } + + // Check Department Structure + OPTIONAL MATCH (dept_struct:DepartmentStructure) + WHERE dept_struct.unique_id = $dept_struct_id + WITH s, debug_nodes, { + exists: dept_struct IS NOT NULL, + node_id: dept_struct.unique_id + } as dept_structure + + // Check Curriculum Structure + OPTIONAL MATCH (curr_struct:CurriculumStructure) + WHERE curr_struct.unique_id = $curr_struct_id + WITH s, debug_nodes, dept_structure, { + exists: curr_struct IS NOT NULL, + node_id: curr_struct.unique_id + } as curr_structure + + // Check Pastoral Structure + OPTIONAL MATCH (past_struct:PastoralStructure) + WHERE past_struct.unique_id = $past_struct_id + WITH debug_nodes, dept_structure, curr_structure, { + exists: past_struct IS NOT NULL, + node_id: past_struct.unique_id + } as past_structure + + // Return structure information + RETURN { + has_basic: dept_structure.exists AND curr_structure.exists AND past_structure.exists, + department_structure: dept_structure, + curriculum_structure: curr_structure, + pastoral_structure: past_structure, + debug_nodes: debug_nodes + } as status + """ + + # Use GraphNamingProvider to generate correct IDs + params = { + "school_id": school_unique_id, + "dept_struct_id": f"DepartmentStructure_{school_unique_id}", + "curr_struct_id": f"CurriculumStructure_{school_unique_id}", + "past_struct_id": f"PastoralStructure_{school_unique_id}", + } + + # Run query in public database + public_result = neo.run_query(basic_query, params, "cc.institutes") + public_status = ( + public_result[0]["status"] if public_result else {"has_basic": False} + ) + + # Check private database if it exists + private_db_name = f"cc.institutes.{school.data['urn']}" + private_exists = False + private_status = None + + # Check if private database exists using Neontology + db_result = neo.run_query("SHOW DATABASES", {}) + databases = [record["name"] for record in db_result] + private_exists = private_db_name in databases + + if private_exists: + private_result = neo.run_query(basic_query, params, private_db_name) + private_status = ( + private_result[0]["status"] + if private_result + else {"has_basic": False} + ) + + return { + "public_database": public_status, + "private_database": { + "exists": private_exists, + "status": private_status if private_exists else None, + }, + } + + except Exception as e: + logger.error(f"Error checking basic structure: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/schools/{school_id}/check-detailed-structure") +async def check_detailed_structure(school_id: str, admin: dict = Depends(verify_admin)): + """Check if detailed structure exists for a school""" + try: + if not admin.get("is_super_admin"): + raise HTTPException( + status_code=403, detail="Only super admins can check structure status" + ) + + # Get school data + school = ( + admin_supabase.table("schools") + .select("*") + .eq("id", school_id) + .single() + .execute() + ) + if not school.data: + raise HTTPException(status_code=404, detail="School not found") + + # Create graph provider for Neo4j operations + provider = GraphProvider() + + # Generate the unique ID for the school + school_unique_id = f"School_{school.data['urn']}" + + # Check detailed structure in public database + with provider.neontology as neo: + detailed_query = """ + MATCH (s:School {unique_id: $school_id}) + + // Get all nodes connected to school for debugging + CALL { + WITH s + MATCH (n) + WHERE n.unique_id CONTAINS s.unique_id + RETURN COLLECT({label: labels(n)[0], id: n.unique_id}) as debug_nodes + } + + // Check Department Structure and Departments + OPTIONAL MATCH (dept_struct:DepartmentStructure) + WHERE dept_struct.unique_id = $dept_struct_id + OPTIONAL MATCH (dept:Department) + WHERE dept.unique_id STARTS WITH 'Department_' + s.unique_id + WITH s, debug_nodes, { + exists: dept_struct IS NOT NULL, + has_departments: COUNT(dept) > 0, + department_count: COUNT(dept) + } as dept_structure + + // Check Curriculum Structure and Key Stages + OPTIONAL MATCH (curr_struct:CurriculumStructure) + WHERE curr_struct.unique_id = $curr_struct_id + OPTIONAL MATCH (ks:KeyStage) + WHERE ks.unique_id STARTS WITH 'KeyStage_' + s.unique_id + WITH s, debug_nodes, dept_structure, { + exists: curr_struct IS NOT NULL, + has_key_stages: COUNT(ks) > 0, + key_stage_count: COUNT(ks) + } as curr_structure + + // Check Pastoral Structure and Year Groups + OPTIONAL MATCH (past_struct:PastoralStructure) + WHERE past_struct.unique_id = $past_struct_id + OPTIONAL MATCH (yg:YearGroup) + WHERE yg.unique_id STARTS WITH 'YearGroup_' + s.unique_id + WITH debug_nodes, dept_structure, curr_structure, { + exists: past_struct IS NOT NULL, + has_year_groups: COUNT(yg) > 0, + year_group_count: COUNT(yg) + } as past_structure + + // Return structure information + RETURN { + has_detailed: + dept_structure.exists AND dept_structure.has_departments AND + curr_structure.exists AND curr_structure.has_key_stages AND + past_structure.exists AND past_structure.has_year_groups, + department_structure: dept_structure, + curriculum_structure: curr_structure, + pastoral_structure: past_structure, + debug_nodes: debug_nodes + } as status + """ + + # Use GraphNamingProvider to generate correct IDs + params = { + "school_id": school_unique_id, + "dept_struct_id": f"DepartmentStructure_{school_unique_id}", + "curr_struct_id": f"CurriculumStructure_{school_unique_id}", + "past_struct_id": f"PastoralStructure_{school_unique_id}", + } + + # Run query in public database + public_result = neo.run_query(detailed_query, params, "cc.institutes") + public_status = ( + public_result[0]["status"] if public_result else {"has_detailed": False} + ) + + # Check private database if it exists + private_db_name = f"cc.institutes.{school.data['urn']}" + private_exists = False + private_status = None + + try: + with provider.neontology.driver.session() as session: + result = session.run("SHOW DATABASES") + databases = [record["name"] for record in result] + private_exists = private_db_name in databases + + if private_exists: + private_result = neo.run_query( + detailed_query, params, private_db_name + ) + private_status = ( + private_result[0]["status"] + if private_result + else {"has_detailed": False} + ) + except Exception as e: + logger.warning(f"Error checking private database: {str(e)}") + + return { + "public_database": public_status, + "private_database": { + "exists": private_exists, + "status": private_status if private_exists else None, + }, + } + + except Exception as e: + logger.error(f"Error checking detailed structure: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# Export the router +__all__ = ["router"] diff --git a/routers/admin_panel_routes.py b/routers/admin_panel_routes.py new file mode 100644 index 0000000..c3e997d --- /dev/null +++ b/routers/admin_panel_routes.py @@ -0,0 +1,113 @@ +from fastapi import APIRouter, Request, Depends, HTTPException +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +import os +from modules.logger_tool import initialise_logger +from modules.database.services.school_admin_service import SchoolAdminService +from modules.database.supabase.utils.storage import StorageManager +from .auth import verify_admin +from typing import Dict + +router = APIRouter() +templates = Jinja2Templates(directory="templates") +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +# Initialize services +school_service = SchoolAdminService() +storage_manager = StorageManager() + +@router.get("/schools/manage", response_class=HTMLResponse) +async def manage_schools(request: Request, admin: Dict = Depends(verify_admin)): + """Manage schools page""" + return templates.TemplateResponse( + "admin/schools/manage.html", + {"request": request, "admin": admin} + ) + +@router.get("/storage/manage", response_class=HTMLResponse) +async def manage_storage(request: Request, admin: Dict = Depends(verify_admin)): + """Storage management page""" + try: + # Get list of storage buckets with correct IDs + buckets = [ + { + "id": "cc.institutes", + "name": "School Files", + "public": False, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + "image/*", + "video/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "text/csv", + "application/json" + ] + }, + { + "id": "cc.users", + "name": "User Files", + "public": False, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + "image/*", + "video/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "text/csv", + "application/json" + ] + } + ] + + return templates.TemplateResponse( + "admin/storage/manage.html", + {"request": request, "admin": admin, "buckets": buckets} + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/schema", response_class=HTMLResponse) +async def manage_schema(request: Request, admin: Dict = Depends(verify_admin)): + """Schema management page""" + return templates.TemplateResponse( + "admin/schema/manage.html", + {"request": request, "admin": admin} + ) + +@router.get("/storage/{bucket_id}/contents") +async def list_bucket_contents( + request: Request, + bucket_id: str, + path: str = "", + admin: Dict = Depends(verify_admin) +): + """List contents of a storage bucket""" + try: + contents = storage_manager.list_bucket_contents(bucket_id, path) + bucket = {"id": bucket_id, "name": bucket_id.replace("_", " ").title()} + + return templates.TemplateResponse( + "admin/storage/contents.html", + { + "request": request, + "admin": admin, + "bucket": bucket, + "contents": contents, + "current_path": path + } + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/admin_routes.py b/routers/admin_routes.py new file mode 100644 index 0000000..dfa4ceb --- /dev/null +++ b/routers/admin_routes.py @@ -0,0 +1,498 @@ +from fastapi import APIRouter, Request, Depends, HTTPException, File, UploadFile, Form +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.templating import Jinja2Templates +from typing import Dict +import os +from modules.logger_tool import initialise_logger +from modules.database.services.admin_service import AdminService, AdminProfileBase +from modules.database.services.school_admin_service import SchoolAdminService +from modules.database.supabase.utils.client import SupabaseAnonClient +from modules.database.supabase.utils.storage import StorageManager +from .auth import verify_admin +import csv +import io + +router = APIRouter() +templates = Jinja2Templates(directory="templates") +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +# Initialize services +admin_service = AdminService() +school_service = SchoolAdminService() +storage_manager = StorageManager(SupabaseAnonClient) + +@router.get("/", response_class=HTMLResponse) +async def admin_dashboard(request: Request, admin: Dict = Depends(verify_admin)): + """Render admin dashboard""" + return templates.TemplateResponse( + "admin/dashboard/index.html", + { + "request": request, + "admin": admin, + "app_version": os.getenv("APP_VERSION", "Unknown") + } + ) + +@router.get("/users") +async def list_users(request: Request, admin: Dict = Depends(verify_admin)): + """List all users""" + return templates.TemplateResponse( + "admin/users/list.html", + {"request": request, "admin": admin} + ) + +@router.get("/users/{user_id}") +async def get_user(request: Request, user_id: str, admin: Dict = Depends(verify_admin)): + """Get user details""" + return templates.TemplateResponse( + "admin/users/detail.html", + {"request": request, "admin": admin, "user_id": user_id} + ) + +@router.get("/admins") +async def list_admins(request: Request, admin: Dict = Depends(verify_admin)): + """List all admins""" + if not admin.get("is_super_admin"): + raise HTTPException(status_code=403, detail="Only super admins can view admin list") + + admins = admin_service.list_admins() + return templates.TemplateResponse( + "admin/users/admins.html", + {"request": request, "admin": admin, "admins": admins} + ) + +@router.post("/admins") +async def create_admin(admin_data: AdminProfileBase, current_admin: Dict = Depends(verify_admin)): + """Create a new admin""" + try: + result = admin_service.create_admin(admin_data, current_admin) + return JSONResponse(content={"status": "success", "admin": result}) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/schools/manage", response_class=HTMLResponse) +async def manage_schools(request: Request, admin: Dict = Depends(verify_admin)): + """Manage schools page""" + try: + # Fetch schools from Supabase + result = admin_service.supabase.table("schools").select("*").execute() + schools = result.data if result else [] + + # Sort schools by establishment_name + schools.sort(key=lambda x: x.get("establishment_name", "")) + + return templates.TemplateResponse( + "admin/schools/manage.html", + { + "request": request, + "admin": admin, + "schools": schools, + "schools_count": len(schools) + } + ) + except Exception as e: + logger.error(f"Error fetching schools: {str(e)}") + return templates.TemplateResponse( + "admin/schools/manage.html", + { + "request": request, + "admin": admin, + "schools": [], + "schools_count": 0, + "error": str(e) + } + ) + +@router.post("/schools/import") +async def import_schools( + file: UploadFile = File(...), + admin: Dict = Depends(verify_admin) +): + """Import schools from CSV file""" + if not file.filename.endswith('.csv'): + raise HTTPException(status_code=400, detail="Please upload a CSV file") + + try: + # Process the CSV file + content = await file.read() + csv_text = content.decode('utf-8-sig') # Handle BOM if present + csv_reader = csv.DictReader(io.StringIO(csv_text)) + + # Prepare data for batch insert + schools_data = [] + for row in csv_reader: + school_data = { + "urn": row.get("URN"), + "la_code": row.get("LA (code)"), + "la_name": row.get("LA (name)"), + "establishment_number": row.get("EstablishmentNumber"), + "establishment_name": row.get("EstablishmentName"), + "establishment_type": row.get("TypeOfEstablishment (name)"), + "establishment_type_group": row.get("EstablishmentTypeGroup (name)"), + "establishment_status": row.get("EstablishmentStatus (name)"), + "reason_establishment_opened": row.get("ReasonEstablishmentOpened (name)"), + "open_date": row.get("OpenDate"), + "reason_establishment_closed": row.get("ReasonEstablishmentClosed (name)"), + "close_date": row.get("CloseDate"), + "phase_of_education": row.get("PhaseOfEducation (name)"), + "statutory_low_age": row.get("StatutoryLowAge"), + "statutory_high_age": row.get("StatutoryHighAge"), + "boarders": row.get("Boarders (name)"), + "nursery_provision": row.get("NurseryProvision (name)"), + "official_sixth_form": row.get("OfficialSixthForm (name)"), + "gender": row.get("Gender (name)"), + "religious_character": row.get("ReligiousCharacter (name)"), + "religious_ethos": row.get("ReligiousEthos (name)"), + "diocese": row.get("Diocese (name)"), + "admissions_policy": row.get("AdmissionsPolicy (name)"), + "school_capacity": row.get("SchoolCapacity"), + "special_classes": row.get("SpecialClasses (name)"), + "census_date": row.get("CensusDate"), + "number_of_pupils": row.get("NumberOfPupils"), + "number_of_boys": row.get("NumberOfBoys"), + "number_of_girls": row.get("NumberOfGirls"), + "percentage_fsm": row.get("PercentageFSM"), + "trust_school_flag": row.get("TrustSchoolFlag (name)"), + "trusts_name": row.get("Trusts (name)"), + "school_sponsor_flag": row.get("SchoolSponsorFlag (name)"), + "school_sponsors_name": row.get("SchoolSponsors (name)"), + "federation_flag": row.get("FederationFlag (name)"), + "federations_name": row.get("Federations (name)"), + "ukprn": row.get("UKPRN"), + "fehe_identifier": row.get("FEHEIdentifier"), + "further_education_type": row.get("FurtherEducationType (name)"), + "ofsted_last_inspection": row.get("OfstedLastInsp"), + "last_changed_date": row.get("LastChangedDate"), + "street": row.get("Street"), + "locality": row.get("Locality"), + "address3": row.get("Address3"), + "town": row.get("Town"), + "county": row.get("County (name)"), + "postcode": row.get("Postcode"), + "school_website": row.get("SchoolWebsite"), + "telephone_num": row.get("TelephoneNum"), + "head_title": row.get("HeadTitle (name)"), + "head_first_name": row.get("HeadFirstName"), + "head_last_name": row.get("HeadLastName"), + "head_preferred_job_title": row.get("HeadPreferredJobTitle"), + "gssla_code": row.get("GSSLACode (name)"), + "parliamentary_constituency": row.get("ParliamentaryConstituency (name)"), + "urban_rural": row.get("UrbanRural (name)"), + "rsc_region": row.get("RSCRegion (name)"), + "country": row.get("Country (name)"), + "uprn": row.get("UPRN"), + "sen_stat": row.get("SENStat") == "true", + "sen_no_stat": row.get("SENNoStat") == "true", + "sen_unit_on_roll": row.get("SenUnitOnRoll"), + "sen_unit_capacity": row.get("SenUnitCapacity"), + "resourced_provision_on_roll": row.get("ResourcedProvisionOnRoll"), + "resourced_provision_capacity": row.get("ResourcedProvisionCapacity"), + } + + # Clean up empty strings and convert types + for key, value in school_data.items(): + if value == "": + school_data[key] = None + elif key in ["statutory_low_age", "statutory_high_age", "school_capacity", + "number_of_pupils", "number_of_boys", "number_of_girls", + "sen_unit_on_roll", "sen_unit_capacity", + "resourced_provision_on_roll", "resourced_provision_capacity"]: + if value: + try: + float_val = float(value) + int_val = int(float_val) + school_data[key] = int_val + except (ValueError, TypeError): + school_data[key] = None + elif key == "percentage_fsm": + if value: + try: + school_data[key] = float(value) + except (ValueError, TypeError): + school_data[key] = None + elif key in ["open_date", "close_date", "census_date", + "ofsted_last_inspection", "last_changed_date"]: + if value: + try: + # Convert date from DD-MM-YYYY to YYYY-MM-DD + parts = value.split("-") + if len(parts) == 3: + school_data[key] = f"{parts[2]}-{parts[1]}-{parts[0]}" + else: + school_data[key] = None + except: + school_data[key] = None + + schools_data.append(school_data) + + # Batch insert schools using admin service's Supabase client + if schools_data: + result = admin_service.supabase.table("schools").upsert( + schools_data, + on_conflict="urn" # Update if URN already exists + ).execute() + + logger.info(f"Imported {len(schools_data)} schools") + return {"status": "success", "imported_count": len(schools_data)} + else: + raise HTTPException(status_code=400, detail="No valid school data found in CSV") + + except Exception as e: + logger.error(f"Error importing schools: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/initialize-schools-database") +async def initialize_schools_database(admin: Dict = Depends(verify_admin)): + """Initialize schools database""" + if not admin.get("is_super_admin"): + raise HTTPException(status_code=403, detail="Only super admins can initialize database") + + result = school_service.create_schools_database() + if result["status"] == "error": + raise HTTPException(status_code=500, detail=result["message"]) + return result + +@router.get("/check-schools-database") +async def check_schools_database(admin: Dict = Depends(verify_admin)): + """Check schools database status""" + try: + # Use SchoolService to check if database exists and has required nodes/relationships + result = school_service.check_schools_database() + return {"exists": result["status"] == "success"} + except Exception as e: + logger.error(f"Error checking schools database: {str(e)}") + return {"exists": False, "error": str(e)} + +@router.get("/storage", response_class=HTMLResponse) +async def storage_management(request: Request, admin: Dict = Depends(verify_admin)): + """Storage management page""" + try: + # Get list of storage buckets with correct IDs + buckets = [ + { + "id": "cc.institutes", + "name": "School Files", + "public": False, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + "image/*", + "video/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "text/csv", + "application/json" + ] + }, + { + "id": "cc.users", + "name": "User Files", + "public": False, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + "image/*", + "video/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain", + "text/csv", + "application/json" + ] + } + ] + + return templates.TemplateResponse( + "admin/storage/manage.html", + {"request": request, "admin": admin, "buckets": buckets} + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/storage/{bucket_id}/contents") +async def list_bucket_contents( + request: Request, + bucket_id: str, + path: str = "", + admin: Dict = Depends(verify_admin) +): + """List contents of a storage bucket""" + try: + contents = storage_manager.list_bucket_contents(bucket_id, path) + bucket = {"id": bucket_id, "name": bucket_id.replace("_", " ").title()} + + return templates.TemplateResponse( + "admin/storage/contents.html", + { + "request": request, + "admin": admin, + "bucket": bucket, + "contents": contents, + "current_path": path + } + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/storage/{bucket_id}/download/{file_path:path}") +async def download_file( + bucket_id: str, + file_path: str, + admin: Dict = Depends(verify_admin) +): + """Get download URL for a file""" + try: + url = storage_manager.create_signed_url(bucket_id, file_path) + return {"url": url} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/storage/{bucket_id}/objects/{object_path:path}") +async def delete_object( + bucket_id: str, + object_path: str, + admin: Dict = Depends(verify_admin) +): + """Delete an object from storage""" + try: + storage_manager.delete_file(bucket_id, object_path) + return {"status": "success"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/check-storage") +async def check_storage(admin: Dict = Depends(verify_admin)): + """Check storage buckets status""" + try: + # Use the same bucket IDs as defined in initialize_storage + buckets = [ + {"id": "cc.users", "name": "User Files"}, + {"id": "cc.institutes", "name": "School Files"} + ] + + results = [] + for bucket in buckets: + exists = storage_manager.check_bucket_exists(bucket["id"]) + results.append({ + "id": bucket["id"], + "name": bucket["name"], + "exists": exists + }) + + return {"buckets": results} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/initialize-storage") +async def initialize_storage(admin: Dict = Depends(verify_admin)): + """Initialize storage buckets and policies for schools""" + try: + # Verify super admin status + if not admin.get('is_super_admin'): + raise HTTPException(status_code=403, detail="Only super admins can initialize storage") + + # Use the storage manager to initialize storage + storage_manager = StorageManager(SupabaseAnonClient) + return storage_manager.initialize_storage() + except Exception as e: + logger.error(f"Error initializing storage: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/check-schema") +async def check_schema(admin: Dict = Depends(verify_admin)): + """Check Neo4j schema status""" + try: + from modules.database.services.graph_service import GraphService + graph_service = GraphService() + + # Get actual schema status + schema_status = graph_service.check_schema_status() + + # Return status with proper validation + return { + "constraints_valid": schema_status["constraints_count"] > 0, + "constraints_count": schema_status["constraints_count"], + "indexes_valid": schema_status["indexes_count"] > 0, + "indexes_count": schema_status["indexes_count"], + "labels_valid": schema_status["labels_count"] > 0, + "labels_count": schema_status["labels_count"] + } + except Exception as e: + logger.error(f"Error checking schema: {str(e)}") + return { + "constraints_valid": False, + "constraints_count": 0, + "indexes_valid": False, + "indexes_count": 0, + "labels_valid": False, + "labels_count": 0, + "error": str(e) + } + +@router.post("/initialize-schema") +async def initialize_schema(admin: Dict = Depends(verify_admin)): + """Initialize Neo4j schema (constraints and indexes)""" + if not admin.get("is_super_admin"): + raise HTTPException(status_code=403, detail="Only super admins can initialize schema") + + try: + from modules.database.services.graph_service import GraphService + graph_service = GraphService() + + # Initialize schema + result = graph_service.initialize_schema() + + if result["status"] == "error": + raise HTTPException(status_code=500, detail=result["message"]) + + return result + except Exception as e: + logger.error(f"Error initializing schema: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/schools/{school_id}") +async def view_school(request: Request, school_id: str, admin: Dict = Depends(verify_admin)): + """View school details""" + try: + # Fetch school details from Supabase + result = admin_service.supabase.table("schools").select("*").eq("id", school_id).single().execute() + school = result.data if result else None + + if not school: + raise HTTPException(status_code=404, detail="School not found") + + return templates.TemplateResponse( + "admin/schools/detail.html", + {"request": request, "admin": admin, "school": school} + ) + except Exception as e: + logger.error(f"Error fetching school details: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.delete("/schools/{school_id}") +async def delete_school(school_id: str, admin: Dict = Depends(verify_admin)): + """Delete a school""" + try: + # Verify super admin status + if not admin.get("is_super_admin"): + raise HTTPException(status_code=403, detail="Only super admins can delete schools") + + # Delete the school from Supabase + result = admin_service.supabase.table("schools").delete().eq("id", school_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="School not found") + + return {"status": "success", "message": "School deleted successfully"} + except Exception as e: + logger.error(f"Error deleting school: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/assets/__init___.py b/routers/assets/__init___.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/assets/__pycache__/pdf.cpython-311.pyc b/routers/assets/__pycache__/pdf.cpython-311.pyc new file mode 100644 index 0000000..507643a Binary files /dev/null and b/routers/assets/__pycache__/pdf.cpython-311.pyc differ diff --git a/routers/assets/__pycache__/powerpoint.cpython-311.pyc b/routers/assets/__pycache__/powerpoint.cpython-311.pyc new file mode 100644 index 0000000..6148173 Binary files /dev/null and b/routers/assets/__pycache__/powerpoint.cpython-311.pyc differ diff --git a/routers/assets/__pycache__/word.cpython-311.pyc b/routers/assets/__pycache__/word.cpython-311.pyc new file mode 100644 index 0000000..ec0a3e6 Binary files /dev/null and b/routers/assets/__pycache__/word.cpython-311.pyc differ diff --git a/routers/assets/pdf.py b/routers/assets/pdf.py new file mode 100644 index 0000000..c3d9cec --- /dev/null +++ b/routers/assets/pdf.py @@ -0,0 +1,423 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(log_name="pdf", log_level=os.getenv("LOG_LEVEL"), log_dir=os.getenv("LOG_PATH"), log_format="default", runtime=True) + +from fastapi import APIRouter, UploadFile, File, HTTPException +from fastapi.responses import JSONResponse +from pathlib import Path +import tempfile +from PIL import Image +import io +import base64 +import traceback +import sys +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError +import asyncio +import psutil +import math +import time +from pdfminer.high_level import extract_pages +from pdfminer.layout import LTTextContainer, LTChar, LTLine, LTRect, LTFigure, LTTextBox, LTTextBoxHorizontal, LTTextLine +import re + +router = APIRouter() + +# Global semaphore to control total concurrent PDF processing +MAX_CONCURRENT_PROCESSING = 4 # Adjust based on server capacity +processing_semaphore = asyncio.Semaphore(MAX_CONCURRENT_PROCESSING) + +def calculate_optimal_workers(): + """Calculate optimal number of worker threads based on system resources.""" + cpu_count = os.cpu_count() or 4 + available_memory = psutil.virtual_memory().available + memory_per_worker = 500 * 1024 * 1024 # 500MB per worker estimate + + # Calculate workers based on CPU and memory constraints + cpu_based_workers = max(1, cpu_count - 1) # Leave one core free + memory_based_workers = max(1, int(available_memory / memory_per_worker)) + + # Take the minimum of CPU and memory-based calculations + optimal_workers = min(cpu_based_workers, memory_based_workers) + + # Cap at a reasonable maximum + final_workers = min(optimal_workers, 8) # Maximum 8 workers per process + + logger.info("Resource utilization:", { + "total_cpus": cpu_count, + "available_memory_gb": available_memory / (1024**3), + "cpu_based_workers": cpu_based_workers, + "memory_based_workers": memory_based_workers, + "final_workers": final_workers + }) + + return final_workers + +def is_heading(textbox, page_height): + """Determine if a textbox is likely a heading based on font size and position.""" + if not isinstance(textbox, LTTextContainer): + return False, 0 + + # Get the most common font size in the textbox + font_sizes = [] + for text_line in textbox._objs: + if isinstance(text_line, LTTextLine): + font_sizes.extend( + char.size + for char in text_line._objs + if isinstance(char, LTChar) + ) + if not font_sizes: + return False, 0 + + most_common_size = max(set(font_sizes), key=font_sizes.count) + + # Position near top of page suggests a heading + is_near_top = textbox.y1 > (page_height - 100) + + # Determine heading level based on font size and position + if most_common_size > 20 or is_near_top: + return True, 1 + elif most_common_size > 16: + return True, 2 + elif most_common_size > 14: + return True, 3 + + return False, 0 + +def clean_text(text): + """Clean and normalize text content.""" + # Remove multiple spaces and newlines + text = re.sub(r'\s+', ' ', text) + # Remove special characters often found in PDFs + text = re.sub(r'[^\x00-\x7F]+', '', text) + return text.strip() + +def extract_page_text(page): + """Extract text from a PDF page and format as markdown.""" + page_height = page.height + text_elements = [] + current_list_items = [] + + # First pass: collect all text elements and identify their roles + for element in page: + if isinstance(element, LTTextContainer): + text = clean_text(element.get_text()) + if not text: + continue + + is_head, level = is_heading(element, page_height) + + # Check if this looks like a list item + is_list_item = bool(re.match(r'^[\u2022\u2023\u25E6\u2043\u2219•\-*]\s', text)) + + if is_head: + # If we have pending list items, add them first + if current_list_items: + text_elements.extend(current_list_items) + current_list_items = [] + text_elements.append((f"{'#' * level} {text.lstrip('1234567890.-* ')}", element.y1)) + elif is_list_item: + current_list_items.append((f"* {text.lstrip('1234567890.-* ')}", element.y1)) + else: + # If this is regular text and we have pending list items + if current_list_items: + # Check if this text is part of the same list (similar y-position) + if any(abs(item[1] - element.y1) < 20 for item in current_list_items): + current_list_items.append((f"* {text}", element.y1)) + continue + else: + # Add pending list items before adding this text + text_elements.extend(current_list_items) + current_list_items = [] + text_elements.append((text, element.y1)) + + # Add any remaining list items + if current_list_items: + text_elements.extend(current_list_items) + + # Sort elements by vertical position (top to bottom) + text_elements.sort(key=lambda x: -x[1]) + + # Return just the text parts, properly formatted + return '\n\n'.join(element[0] for element in text_elements) + +def process_page(temp_dir: str, pdf_path: str, page_info: tuple, timeout: int = 30) -> dict: + """ + Worker function to process a single page and maintain A4 proportions. + Args: + temp_dir: Path to temporary directory + pdf_path: Path to PDF file + page_info: Tuple of (index, page_number) + timeout: Maximum time in seconds to process a single page + Returns: + dict: Processed page information + """ + i, page_idx = page_info + page_num = page_idx + 1 # PDF pages are 1-indexed + output_prefix = str(Path(temp_dir) / f"page_{page_num}") + + try: + # Extract text from PDF page + pages = list(extract_pages(pdf_path, page_numbers=[page_idx])) + page_text = extract_page_text(pages[0]) if pages else "" + # Convert PDF page to PNG with timeout + process = subprocess.Popen( + [ + 'pdftoppm', + '-png', + '-singlefile', + '-f', + str(page_num), + '-l', + str(page_num), + '-r', + '600', # High resolution for better quality + pdf_path, + output_prefix, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + try: + stdout, stderr = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + raise TimeoutError(f"Page {page_num} processing timed out after {timeout} seconds") + + if process.returncode != 0: + raise Exception(f"pdftoppm failed for page {page_num}: {stderr.decode()}") + + output_file = f"{output_prefix}.png" + if not Path(output_file).exists(): + raise Exception(f"Could not find output file for page {page_num}") + + # Open and process the image + with Image.open(output_file) as img: + result = _process_image(img, i) + if result['success']: + result['meta'] = { + 'text': page_text, + 'format': 'markdown' + } + return result + except Exception as e: + logger.error(f"Error processing page {page_num}: {str(e)}") + return { + "index": i, + "error": str(e), + "success": False, + } + +def _process_image(img: Image.Image, index: int) -> dict: + """Process a single image, maintaining A4 proportions.""" + try: + # Determine orientation and target dimensions + is_portrait = img.height > img.width + target_height = 720 # Fixed height to match frontend slide height + + if is_portrait: + # A4 portrait ratio is 210:297 + target_width = int(target_height * (210/297)) + else: + # A4 landscape ratio is 297:210 + target_width = int(target_height * (297/210)) + + # Resize image maintaining aspect ratio + img = img.resize((target_width, target_height), Image.Resampling.LANCZOS) + + # Convert to base64 + buffered = io.BytesIO() + img.save(buffered, format="PNG", optimize=True) + img_str = base64.b64encode(buffered.getvalue()).decode() + + return { + "index": index, + "data": f"data:image/png;base64,{img_str}", + "success": True, + "dimensions": { + "width": target_width, + "height": target_height, + "orientation": "portrait" if is_portrait else "landscape" + } + } + except Exception as e: + logger.error(f"Error processing image for page {index}: {str(e)}") + return { + "index": index, + "error": str(e), + "success": False, + } + +async def process_pages_in_chunks(temp_dir: str, pdf_path: str, visible_pages: list, chunk_size: int = 5): + """Process pages in chunks to manage memory better.""" + all_processed_pages = [] + num_workers = calculate_optimal_workers() + total_chunks = math.ceil(len(visible_pages) / chunk_size) + + logger.info("Starting page processing:", { + "total_pages": len(visible_pages), + "chunk_size": chunk_size, + "total_chunks": total_chunks, + "workers_per_chunk": num_workers + }) + + # Process pages in chunks + for chunk_index in range(0, len(visible_pages), chunk_size): + chunk = visible_pages[chunk_index:chunk_index + chunk_size] + processed_chunk = [] + current_chunk_num = (chunk_index // chunk_size) + 1 + + logger.info(f"Processing chunk {current_chunk_num}/{total_chunks}", { + "chunk_size": len(chunk), + "chunk_start_index": chunk_index, + "memory_usage_gb": psutil.Process().memory_info().rss / (1024**3) + }) + + start_time = time.time() + with ThreadPoolExecutor(max_workers=num_workers) as executor: + # Submit chunk of tasks + future_to_page = { + executor.submit( + process_page, temp_dir, pdf_path, page_info + ): page_info + for page_info in chunk + } + + # Process completed tasks as they finish + for future in as_completed(future_to_page): + try: + result = future.result(timeout=60) # Increased timeout to 60 seconds per page + if result.get('success', False): + processed_chunk.append(result) + page_info = future_to_page[future] + logger.debug(f"Processed page {page_info[1] + 1}", { + "success": result.get('success', False), + "processing_time": time.time() - start_time + }) + except TimeoutError: + page_info = future_to_page[future] + logger.error(f"Timeout processing page {page_info[1] + 1}") + except Exception as e: + page_info = future_to_page[future] + logger.error(f"Error processing page {page_info[1] + 1}: {str(e)}") + + chunk_time = time.time() - start_time + logger.info(f"Completed chunk {current_chunk_num}/{total_chunks}", { + "processed_pages": len(processed_chunk), + "chunk_processing_time": chunk_time, + "avg_time_per_page": chunk_time / len(chunk) if chunk else 0 + }) + + all_processed_pages.extend(processed_chunk) + + # Small delay between chunks to allow other tasks to process + await asyncio.sleep(0.1) + + return all_processed_pages + +@router.post("/convert") +async def convert_pdf_to_images(file: UploadFile = File(...)): + try: + async with processing_semaphore: # Control concurrent processing + start_time = time.time() + # Log request details + logger.info( + "Received file upload request", + { + "filename": file.filename, + "content_type": file.content_type, + "current_memory_usage_gb": psutil.Process() + .memory_info() + .rss + / (1024**3), + "cpu_percent": psutil.cpu_percent(interval=1), + }, + ) + + # Validate file + if not file.filename.endswith('.pdf'): + logger.error("Invalid file type") + return JSONResponse({ + "status": "error", + "message": "Invalid file type. Please upload a .pdf file" + }, status_code=400) + + # Create a temporary directory to store the PDF file + with tempfile.TemporaryDirectory() as temp_dir: + pdf_path = Path(temp_dir) / "document.pdf" + logger.debug(f"Saving file to temporary path: {pdf_path}") + + try: + # Save uploaded file + content = await file.read() + logger.debug(f"Read file content, size: {len(content)} bytes") + + with open(pdf_path, "wb") as buffer: + buffer.write(content) + logger.debug("File saved successfully") + + if not pdf_path.exists() or pdf_path.stat().st_size == 0: + raise Exception("Failed to save file or file is empty") + + # Get number of pages using pdfinfo + result = subprocess.run(['pdfinfo', str(pdf_path)], capture_output=True, text=True) + pages_line = [line for line in result.stdout.split('\n') if line.startswith('Pages:')][0] + num_pages = int(pages_line.split(':')[1].strip()) + + visible_pages = [(i, i) for i in range(num_pages)] + + if num_pages == 0: + logger.warning("No pages found in document") + return JSONResponse({ + "status": "error", + "message": "No pages found in document" + }, status_code=400) + + logger.info(f"Processing {num_pages} pages") + + # Calculate chunk size based on number of pages + chunk_size = min(5, max(2, math.ceil(num_pages / 4))) + processed_pages = await process_pages_in_chunks(str(temp_dir), str(pdf_path), visible_pages, chunk_size) + + if not processed_pages: + raise Exception("Failed to process any pages successfully") + + # Sort pages by index + processed_pages.sort(key=lambda x: x['index']) + + logger.info(f"Successfully processed {len(processed_pages)} pages") + + # After processing all pages + total_time = time.time() - start_time + logger.info("PDF processing completed", { + "total_processing_time": total_time, + "pages_processed": len(processed_pages), + "avg_time_per_page": total_time / len(processed_pages) if processed_pages else 0, + "final_memory_usage_gb": psutil.Process().memory_info().rss / (1024**3) + }) + + return JSONResponse({ + "status": "success", + "slides": processed_pages, # Using same format as PowerPoint for consistency + "processing_stats": { + "total_time": total_time, + "pages_processed": len(processed_pages), + "avg_time_per_page": total_time / len(processed_pages) if processed_pages else 0 + } + }) + + except Exception as inner_error: + logger.error(f"Inner error: {str(inner_error)}") + logger.error(traceback.format_exc()) + raise + + except Exception as e: + logger.error(f"Error processing PDF: {str(e)}") + logger.error(f"Python version: {sys.version}") + logger.error(f"Traceback: {traceback.format_exc()}") + return JSONResponse({ + "status": "error", + "message": f"Failed to process PDF: {str(e)}" + }, status_code=500) diff --git a/routers/assets/powerpoint.py b/routers/assets/powerpoint.py new file mode 100644 index 0000000..4bd8cf0 --- /dev/null +++ b/routers/assets/powerpoint.py @@ -0,0 +1,398 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(log_name="powerpoint", log_level=os.getenv("LOG_LEVEL"), log_dir=os.getenv("LOG_PATH"), log_format="default", runtime=True) + +from fastapi import APIRouter, UploadFile, File, HTTPException +from fastapi.responses import JSONResponse +from pathlib import Path +import tempfile +from pptx import Presentation +from PIL import Image +import io +import base64 +import traceback +import sys +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError +import asyncio +import psutil +import math +import time + +router = APIRouter() + +# Global semaphore to control total concurrent PowerPoint processing +MAX_CONCURRENT_PROCESSING = 4 # Adjust based on server capacity +processing_semaphore = asyncio.Semaphore(MAX_CONCURRENT_PROCESSING) + +def calculate_optimal_workers(): + """Calculate optimal number of worker threads based on system resources.""" + cpu_count = os.cpu_count() or 4 + available_memory = psutil.virtual_memory().available + memory_per_worker = 500 * 1024 * 1024 # 500MB per worker estimate + + # Calculate workers based on CPU and memory constraints + cpu_based_workers = max(1, cpu_count - 1) # Leave one core free + memory_based_workers = max(1, int(available_memory / memory_per_worker)) + + # Take the minimum of CPU and memory-based calculations + optimal_workers = min(cpu_based_workers, memory_based_workers) + + # Cap at a reasonable maximum + final_workers = min(optimal_workers, 8) # Maximum 8 workers per process + + # Log resource information + logger.info("Resource utilization:", { + "total_cpus": cpu_count, + "available_memory_gb": available_memory / (1024**3), + "cpu_based_workers": cpu_based_workers, + "memory_based_workers": memory_based_workers, + "final_workers": final_workers + }) + + return final_workers + +def extract_text_from_shape(shape): + """Extract text from a PowerPoint shape.""" + if hasattr(shape, 'text') and shape.text.strip(): + return shape.text.strip() + + # Handle tables + if shape.has_table: + table_text = [] + for row in shape.table.rows: + row_text = [] + row_text.extend(cell.text.strip() for cell in row.cells if cell.text.strip()) + if row_text: + table_text.append('| ' + ' | '.join(row_text) + ' |') + if table_text: + # Add markdown table header separator + table_text.insert(1, '|' + '---|' * (len(table_text[0].split('|')) - 2)) + return '\n'.join(table_text) + + # Handle grouped shapes + if hasattr(shape, 'shapes'): + group_text = [] + for subshape in shape.shapes: + if text := extract_text_from_shape(subshape): + group_text.append(text) + return '\n'.join(group_text) if group_text else '' + + return '' + +def extract_slide_text(slide): + """Extract text from a PowerPoint slide and format as markdown.""" + slide_text = [] + + # Extract title if present + if slide.shapes.title and slide.shapes.title.text.strip(): + slide_text.append(f"# {slide.shapes.title.text.strip()}") + + # Process all shapes + for shape in slide.shapes: + if shape != slide.shapes.title: # Skip title as we've already processed it + if text := extract_text_from_shape(shape): + slide_text.append(text) + + return '\n\n'.join(slide_text) + +def process_slide(temp_dir: str, pdf_path: str, pptx_path: str, slide_info: tuple, timeout: int = 30) -> dict: + """ + Worker function to process a single slide and enforce 16:9 aspect ratio. + Args: + temp_dir: Path to temporary directory + pdf_path: Path to PDF file + pptx_path: Path to PowerPoint file + slide_info: Tuple of (index, slide_number) + timeout: Maximum time in seconds to process a single slide + Returns: + dict: Processed slide information + """ + i, slide_idx = slide_info + slide_num = slide_idx + 1 # PDF pages are 1-indexed + output_prefix = str(Path(temp_dir) / f"slide_{slide_num}") + + try: + # Extract text from PowerPoint slide + prs = Presentation(pptx_path) + slide_text = extract_slide_text(prs.slides[slide_idx]) + + # Convert PDF page to PNG with timeout + process = subprocess.Popen( + [ + 'pdftoppm', + '-png', + '-singlefile', + '-f', + str(slide_num), + '-l', + str(slide_num), + '-r', + '600', # High resolution for better quality + pdf_path, + output_prefix, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + try: + stdout, stderr = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + raise TimeoutError(f"Slide {slide_num} processing timed out after {timeout} seconds") + + if process.returncode != 0: + raise Exception(f"pdftoppm failed for slide {slide_num}: {stderr.decode()}") + + output_file = f"{output_prefix}.png" + if not Path(output_file).exists(): + raise Exception(f"Could not find output file for slide {slide_num}") + + # Open and process the image + with Image.open(output_file) as img: + result = _process_image(img, i) + if result['success']: + result['meta'] = { + 'text': slide_text, + 'format': 'markdown' + } + return result + except Exception as e: + logger.error(f"Error processing slide {slide_num}: {str(e)}") + return { + "index": i, + "error": str(e), + "success": False, + } + +def _process_image(img: Image.Image, index: int) -> dict: + """Process a single image, enforcing aspect ratio and size constraints.""" + try: + # Enforce 16:9 aspect ratio + target_aspect_ratio = 16 / 9 + img_aspect_ratio = img.width / img.height + + if img_aspect_ratio > target_aspect_ratio: # Wider than 16:9 + new_width = int(img.height * target_aspect_ratio) + offset = (img.width - new_width) // 2 + img = img.crop((offset, 0, offset + new_width, img.height)) + elif img_aspect_ratio < target_aspect_ratio: # Taller than 16:9 + new_height = int(img.width / target_aspect_ratio) + offset = (img.height - new_height) // 2 + img = img.crop((0, offset, img.width, offset + new_height)) + + # Resize to target resolution (2560x1440) + img = img.resize((2560, 1440), Image.Resampling.LANCZOS) + + # Convert to base64 + buffered = io.BytesIO() + img.save(buffered, format="PNG", optimize=True) + img_str = base64.b64encode(buffered.getvalue()).decode() + + return { + "index": index, + "data": f"data:image/png;base64,{img_str}", + "success": True, + } + except Exception as e: + logger.error(f"Error processing image for slide {index}: {str(e)}") + return { + "index": index, + "error": str(e), + "success": False, + } + +async def process_slides_in_chunks(temp_dir: str, pdf_path: str, pptx_path: str, visible_slides: list, chunk_size: int = 5): + """Process slides in chunks to manage memory better.""" + all_processed_slides = [] + num_workers = calculate_optimal_workers() + total_chunks = math.ceil(len(visible_slides) / chunk_size) + + logger.info("Starting slide processing:", { + "total_slides": len(visible_slides), + "chunk_size": chunk_size, + "total_chunks": total_chunks, + "workers_per_chunk": num_workers + }) + + # Process slides in chunks + for chunk_index in range(0, len(visible_slides), chunk_size): + chunk = visible_slides[chunk_index:chunk_index + chunk_size] + processed_chunk = [] + current_chunk_num = (chunk_index // chunk_size) + 1 + + logger.info(f"Processing chunk {current_chunk_num}/{total_chunks}", { + "chunk_size": len(chunk), + "chunk_start_index": chunk_index, + "memory_usage_gb": psutil.Process().memory_info().rss / (1024**3) + }) + + start_time = time.time() + with ThreadPoolExecutor(max_workers=num_workers) as executor: + # Submit chunk of tasks + future_to_slide = { + executor.submit( + process_slide, temp_dir, pdf_path, pptx_path, slide_info + ): slide_info + for slide_info in chunk + } + + # Process completed tasks as they finish + for future in as_completed(future_to_slide): + try: + result = future.result(timeout=60) # Increased timeout to 60 seconds per slide + if result.get('success', False): + processed_chunk.append(result) + slide_info = future_to_slide[future] + logger.debug(f"Processed slide {slide_info[1] + 1}", { + "success": result.get('success', False), + "processing_time": time.time() - start_time + }) + except TimeoutError: + slide_info = future_to_slide[future] + logger.error(f"Timeout processing slide {slide_info[1] + 1}") + except Exception as e: + slide_info = future_to_slide[future] + logger.error(f"Error processing slide {slide_info[1] + 1}: {str(e)}") + + chunk_time = time.time() - start_time + logger.info(f"Completed chunk {current_chunk_num}/{total_chunks}", { + "processed_slides": len(processed_chunk), + "chunk_processing_time": chunk_time, + "avg_time_per_slide": chunk_time / len(chunk) if chunk else 0 + }) + + all_processed_slides.extend(processed_chunk) + + # Small delay between chunks to allow other tasks to process + await asyncio.sleep(0.1) + + return all_processed_slides + +@router.post("/convert") +async def convert_pptx_to_images(file: UploadFile = File(...)): + try: + async with processing_semaphore: # Control concurrent processing + start_time = time.time() + # Log request details + logger.info( + "Received file upload request", + { + "filename": file.filename, + "content_type": file.content_type, + "current_memory_usage_gb": psutil.Process() + .memory_info() + .rss + / (1024**3), + "cpu_percent": psutil.cpu_percent(interval=1), + }, + ) + + # Validate file + if not file.filename.endswith('.pptx'): + logger.error("Invalid file type") + return JSONResponse({ + "status": "error", + "message": "Invalid file type. Please upload a .pptx file" + }, status_code=400) + + # Create a temporary directory to store the PowerPoint file + with tempfile.TemporaryDirectory() as temp_dir: + pptx_path = Path(temp_dir) / "presentation.pptx" + logger.debug(f"Saving file to temporary path: {pptx_path}") + + try: + # Save uploaded file + content = await file.read() + logger.debug(f"Read file content, size: {len(content)} bytes") + + with open(pptx_path, "wb") as buffer: + buffer.write(content) + logger.debug("File saved successfully") + + if not pptx_path.exists() or pptx_path.stat().st_size == 0: + raise Exception("Failed to save file or file is empty") + + # Open the presentation and get visible slides + prs = Presentation(str(pptx_path)) + visible_slides = [ + (i, slide_idx) + for i, (slide_idx, _) in enumerate( + (i, slide) + for i, slide in enumerate(prs.slides) + if not hasattr(slide, 'show') or slide.show + ) + ] + num_slides = len(visible_slides) + + if num_slides == 0: + logger.warning("No visible slides found in presentation") + return JSONResponse({ + "status": "error", + "message": "No visible slides found in presentation" + }, status_code=400) + + logger.info(f"Processing {num_slides} visible slides") + + # Convert PowerPoint to PDF + pdf_path = Path(temp_dir) / "presentation.pdf" + logger.debug("Converting PowerPoint to PDF") + + result = subprocess.run([ + 'soffice', + '--headless', + '--convert-to', 'pdf', + '--outdir', str(temp_dir), + str(pptx_path) + ], check=True, capture_output=True, text=True) + + if not pdf_path.exists(): + raise Exception("PDF file was not created") + + logger.debug(f"PDF created successfully at {pdf_path}, size: {pdf_path.stat().st_size} bytes") + + # Calculate chunk size based on number of slides + chunk_size = min(5, max(2, math.ceil(num_slides / 4))) + processed_slides = await process_slides_in_chunks(str(temp_dir), str(pdf_path), str(pptx_path), visible_slides, chunk_size) + + if not processed_slides: + raise Exception("Failed to process any slides successfully") + + # Sort slides by index + processed_slides.sort(key=lambda x: x['index']) + + logger.info(f"Successfully processed {len(processed_slides)} slides") + + # After processing all slides + total_time = time.time() - start_time + logger.info("PowerPoint processing completed", { + "total_processing_time": total_time, + "slides_processed": len(processed_slides), + "avg_time_per_slide": total_time / len(processed_slides) if processed_slides else 0, + "final_memory_usage_gb": psutil.Process().memory_info().rss / (1024**3) + }) + + return JSONResponse({ + "status": "success", + "slides": processed_slides, + "processing_stats": { + "total_time": total_time, + "slides_processed": len(processed_slides), + "avg_time_per_slide": total_time / len(processed_slides) if processed_slides else 0 + } + }) + + except Exception as inner_error: + logger.error(f"Inner error: {str(inner_error)}") + logger.error(traceback.format_exc()) + raise + + except Exception as e: + logger.error(f"Error processing PowerPoint: {str(e)}") + logger.error(f"Python version: {sys.version}") + logger.error(f"Traceback: {traceback.format_exc()}") + return JSONResponse({ + "status": "error", + "message": f"Failed to process PowerPoint: {str(e)}" + }, status_code=500) \ No newline at end of file diff --git a/routers/assets/shared.py b/routers/assets/shared.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/assets/word.py b/routers/assets/word.py new file mode 100644 index 0000000..7bc386d --- /dev/null +++ b/routers/assets/word.py @@ -0,0 +1,418 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(log_name="word", log_level=os.getenv("LOG_LEVEL"), log_dir=os.getenv("LOG_PATH"), log_format="default", runtime=True) + +from fastapi import APIRouter, UploadFile, File, HTTPException +from fastapi.responses import JSONResponse +from pathlib import Path +import tempfile +from PIL import Image +import io +import base64 +import traceback +import sys +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError +import asyncio +import psutil +import math +import time +from docx import Document + +router = APIRouter() + +# Global semaphore to control total concurrent Word processing +MAX_CONCURRENT_PROCESSING = 4 # Adjust based on server capacity +processing_semaphore = asyncio.Semaphore(MAX_CONCURRENT_PROCESSING) + +def calculate_optimal_workers(): + """Calculate optimal number of worker threads based on system resources.""" + cpu_count = os.cpu_count() or 4 + available_memory = psutil.virtual_memory().available + memory_per_worker = 500 * 1024 * 1024 # 500MB per worker estimate + + # Calculate workers based on CPU and memory constraints + cpu_based_workers = max(1, cpu_count - 1) # Leave one core free + memory_based_workers = max(1, int(available_memory / memory_per_worker)) + + # Take the minimum of CPU and memory-based calculations + optimal_workers = min(cpu_based_workers, memory_based_workers) + + # Cap at a reasonable maximum + final_workers = min(optimal_workers, 8) # Maximum 8 workers per process + + logger.info("Resource utilization:", { + "total_cpus": cpu_count, + "available_memory_gb": available_memory / (1024**3), + "cpu_based_workers": cpu_based_workers, + "memory_based_workers": memory_based_workers, + "final_workers": final_workers + }) + + return final_workers + +def extract_text_from_paragraph(paragraph): + """Extract text from a Word paragraph and format as markdown.""" + text = paragraph.text.strip() + if not text: + return '' + + # Handle different heading levels + if paragraph.style.name.startswith('Heading'): + level = int(paragraph.style.name[-1]) + return f"{'#' * level} {text}" + + # Handle lists + if paragraph._element.pPr is not None and paragraph._element.pPr.numPr is not None: + return f"* {text}" + + return text + +def extract_text_from_table(table): + """Extract text from a Word table and format as markdown.""" + # Process header row + header_row = [] + header_row.extend((cell.text.strip() or ' ') for cell in table.rows[0].cells) + table_text = [ + '| ' + ' | '.join(header_row) + ' |', + '|' + '---|' * (len(header_row) - 1) + '---|', + ] + # Process remaining rows + for row in table.rows[1:]: + row_text = [] + row_text.extend((cell.text.strip() or ' ') for cell in row.cells) + table_text.append('| ' + ' | '.join(row_text) + ' |') + + return '\n'.join(table_text) + +def extract_page_text(doc, page_index): + """Extract text from a Word document page and format as markdown.""" + # Note: python-docx doesn't provide direct page access, so we'll use a heuristic + # to group paragraphs into pages based on content length + CHARS_PER_PAGE = 3000 # Approximate characters per page + + all_blocks = [] + current_chars = 0 + current_page = 0 + + for element in doc.element.body: + if current_page > page_index: + break + + if element.tag.endswith('p'): + paragraph = doc.paragraphs[len(all_blocks)] + if text := extract_text_from_paragraph(paragraph): + current_chars += len(text) + if current_page == page_index: + all_blocks.append(text) + elif element.tag.endswith('tbl'): + table = doc.tables[sum(isinstance(b, str) for b in all_blocks)] + if text := extract_text_from_table(table): + current_chars += len(text) + if current_page == page_index: + all_blocks.append(text) + + if current_chars >= CHARS_PER_PAGE: + current_page += 1 + current_chars = 0 + + return '\n\n'.join(all_blocks) + +def process_page(temp_dir: str, pdf_path: str, docx_path: str, page_info: tuple, timeout: int = 30) -> dict: + """ + Worker function to process a single page and maintain A4 proportions. + Args: + temp_dir: Path to temporary directory + pdf_path: Path to PDF file + docx_path: Path to Word file + page_info: Tuple of (index, page_number) + timeout: Maximum time in seconds to process a single page + Returns: + dict: Processed page information + """ + i, page_idx = page_info + page_num = page_idx + 1 # PDF pages are 1-indexed + output_prefix = str(Path(temp_dir) / f"page_{page_num}") + + try: + # Extract text from Word document + doc = Document(docx_path) + page_text = extract_page_text(doc, page_idx) + + # Convert PDF page to PNG with timeout + process = subprocess.Popen( + [ + 'pdftoppm', + '-png', + '-singlefile', + '-f', + str(page_num), + '-l', + str(page_num), + '-r', + '600', # High resolution for better quality + pdf_path, + output_prefix, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + try: + stdout, stderr = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + raise TimeoutError(f"Page {page_num} processing timed out after {timeout} seconds") + + if process.returncode != 0: + raise Exception(f"pdftoppm failed for page {page_num}: {stderr.decode()}") + + output_file = f"{output_prefix}.png" + if not Path(output_file).exists(): + raise Exception(f"Could not find output file for page {page_num}") + + # Open and process the image + with Image.open(output_file) as img: + result = _process_image(img, i) + if result['success']: + result['meta'] = { + 'text': page_text, + 'format': 'markdown' + } + return result + except Exception as e: + logger.error(f"Error processing page {page_num}: {str(e)}") + return { + "index": i, + "error": str(e), + "success": False, + } + +def _process_image(img: Image.Image, index: int) -> dict: + """Process a single image, maintaining A4 proportions.""" + try: + # Determine orientation and target dimensions + is_portrait = img.height > img.width + target_height = 720 # Fixed height to match frontend slide height + + if is_portrait: + # A4 portrait ratio is 210:297 + target_width = int(target_height * (210/297)) + else: + # A4 landscape ratio is 297:210 + target_width = int(target_height * (297/210)) + + # Resize image maintaining aspect ratio + img = img.resize((target_width, target_height), Image.Resampling.LANCZOS) + + # Convert to base64 + buffered = io.BytesIO() + img.save(buffered, format="PNG", optimize=True) + img_str = base64.b64encode(buffered.getvalue()).decode() + + return { + "index": index, + "data": f"data:image/png;base64,{img_str}", + "success": True, + "dimensions": { + "width": target_width, + "height": target_height, + "orientation": "portrait" if is_portrait else "landscape" + } + } + except Exception as e: + logger.error(f"Error processing image for page {index}: {str(e)}") + return { + "index": index, + "error": str(e), + "success": False, + } + +async def process_pages_in_chunks(temp_dir: str, pdf_path: str, docx_path: str, visible_pages: list, chunk_size: int = 5): + """Process pages in chunks to manage memory better.""" + all_processed_pages = [] + num_workers = calculate_optimal_workers() + total_chunks = math.ceil(len(visible_pages) / chunk_size) + + logger.info("Starting page processing:", { + "total_pages": len(visible_pages), + "chunk_size": chunk_size, + "total_chunks": total_chunks, + "workers_per_chunk": num_workers + }) + + # Process pages in chunks + for chunk_index in range(0, len(visible_pages), chunk_size): + chunk = visible_pages[chunk_index:chunk_index + chunk_size] + processed_chunk = [] + current_chunk_num = (chunk_index // chunk_size) + 1 + + logger.info(f"Processing chunk {current_chunk_num}/{total_chunks}", { + "chunk_size": len(chunk), + "chunk_start_index": chunk_index, + "memory_usage_gb": psutil.Process().memory_info().rss / (1024**3) + }) + + start_time = time.time() + with ThreadPoolExecutor(max_workers=num_workers) as executor: + # Submit chunk of tasks + future_to_page = { + executor.submit( + process_page, temp_dir, pdf_path, docx_path, page_info + ): page_info + for page_info in chunk + } + + # Process completed tasks as they finish + for future in as_completed(future_to_page): + try: + result = future.result(timeout=60) # Increased timeout to 60 seconds per page + if result.get('success', False): + processed_chunk.append(result) + page_info = future_to_page[future] + logger.debug(f"Processed page {page_info[1] + 1}", { + "success": result.get('success', False), + "processing_time": time.time() - start_time + }) + except TimeoutError: + page_info = future_to_page[future] + logger.error(f"Timeout processing page {page_info[1] + 1}") + except Exception as e: + page_info = future_to_page[future] + logger.error(f"Error processing page {page_info[1] + 1}: {str(e)}") + + chunk_time = time.time() - start_time + logger.info(f"Completed chunk {current_chunk_num}/{total_chunks}", { + "processed_pages": len(processed_chunk), + "chunk_processing_time": chunk_time, + "avg_time_per_page": chunk_time / len(chunk) if chunk else 0 + }) + + all_processed_pages.extend(processed_chunk) + + # Small delay between chunks to allow other tasks to process + await asyncio.sleep(0.1) + + return all_processed_pages + +@router.post("/convert") +async def convert_docx_to_images(file: UploadFile = File(...)): + try: + async with processing_semaphore: # Control concurrent processing + start_time = time.time() + # Log request details + logger.info( + "Received file upload request", + { + "filename": file.filename, + "content_type": file.content_type, + "current_memory_usage_gb": psutil.Process() + .memory_info() + .rss + / (1024**3), + "cpu_percent": psutil.cpu_percent(interval=1), + }, + ) + + # Validate file + if not file.filename.endswith('.docx'): + logger.error("Invalid file type") + return JSONResponse({ + "status": "error", + "message": "Invalid file type. Please upload a .docx file" + }, status_code=400) + + # Create a temporary directory to store the Word file + with tempfile.TemporaryDirectory() as temp_dir: + docx_path = Path(temp_dir) / "document.docx" + pdf_path = Path(temp_dir) / "document.pdf" + + logger.debug(f"Saving file to temporary path: {docx_path}") + + try: + # Save uploaded file + content = await file.read() + logger.debug(f"Read file content, size: {len(content)} bytes") + + with open(docx_path, "wb") as buffer: + buffer.write(content) + logger.debug("File saved successfully") + + if not docx_path.exists() or docx_path.stat().st_size == 0: + raise Exception("Failed to save file or file is empty") + + # Convert Word to PDF using LibreOffice + logger.debug("Converting Word to PDF") + result = subprocess.run([ + 'soffice', + '--headless', + '--convert-to', 'pdf', + '--outdir', str(temp_dir), + str(docx_path) + ], check=True, capture_output=True, text=True) + + if not pdf_path.exists(): + raise Exception("PDF file was not created") + + logger.debug(f"PDF created successfully at {pdf_path}, size: {pdf_path.stat().st_size} bytes") + + # Get number of pages using pdfinfo + result = subprocess.run(['pdfinfo', str(pdf_path)], capture_output=True, text=True) + pages_line = [line for line in result.stdout.split('\n') if line.startswith('Pages:')][0] + num_pages = int(pages_line.split(':')[1].strip()) + + visible_pages = [(i, i) for i in range(num_pages)] + + if num_pages == 0: + logger.warning("No pages found in document") + return JSONResponse({ + "status": "error", + "message": "No pages found in document" + }, status_code=400) + + logger.info(f"Processing {num_pages} pages") + + # Calculate chunk size based on number of pages + chunk_size = min(5, max(2, math.ceil(num_pages / 4))) + processed_pages = await process_pages_in_chunks(str(temp_dir), str(pdf_path), str(docx_path), visible_pages, chunk_size) + + if not processed_pages: + raise Exception("Failed to process any pages successfully") + + # Sort pages by index + processed_pages.sort(key=lambda x: x['index']) + + logger.info(f"Successfully processed {len(processed_pages)} pages") + + # After processing all pages + total_time = time.time() - start_time + logger.info("Word document processing completed", { + "total_processing_time": total_time, + "pages_processed": len(processed_pages), + "avg_time_per_page": total_time / len(processed_pages) if processed_pages else 0, + "final_memory_usage_gb": psutil.Process().memory_info().rss / (1024**3) + }) + + return JSONResponse({ + "status": "success", + "slides": processed_pages, # Using same format as PowerPoint for consistency + "processing_stats": { + "total_time": total_time, + "pages_processed": len(processed_pages), + "avg_time_per_page": total_time / len(processed_pages) if processed_pages else 0 + } + }) + + except Exception as inner_error: + logger.error(f"Inner error: {str(inner_error)}") + logger.error(traceback.format_exc()) + raise + + except Exception as e: + logger.error(f"Error processing Word document: {str(e)}") + logger.error(f"Python version: {sys.version}") + logger.error(f"Traceback: {traceback.format_exc()}") + return JSONResponse({ + "status": "error", + "message": f"Failed to process Word document: {str(e)}" + }, status_code=500) diff --git a/routers/auth.py b/routers/auth.py new file mode 100644 index 0000000..3c4290a --- /dev/null +++ b/routers/auth.py @@ -0,0 +1,139 @@ +from fastapi import APIRouter, Request, Response, HTTPException, Form, Body +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from typing import Dict +import os +from modules.logger_tool import initialise_logger +from modules.database.services.admin_service import AdminService +from modules.database.services.auth_service import auth_service + +router = APIRouter() +templates = Jinja2Templates(directory="templates") +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +# Initialize services +admin_service = AdminService() + +async def verify_admin(request: Request) -> Dict: + """Verify that the user is an admin and has necessary permissions""" + session = request.cookies.get("sb-access-token") + return await auth_service.verify_admin(session) + +@router.get("/admin", response_class=HTMLResponse) +async def admin_root(request: Request): + """Root admin route - redirects to login or dashboard""" + try: + admin = await verify_admin(request) + return RedirectResponse(url="/api/admin/", status_code=303) + except HTTPException: + # Check if super admin exists + has_super_admin = await auth_service.check_super_admin_exists() + if not has_super_admin: + return RedirectResponse(url="/api/admin/login?init=true", status_code=303) + return RedirectResponse(url="/api/admin/login", status_code=303) + +@router.get("/admin/login", response_class=HTMLResponse) +async def login_page( + request: Request, + error: str = None, + success: str = None, + init: bool = False +): + """Render admin login page""" + # Check if super admin exists + has_super_admin = await auth_service.check_super_admin_exists() + + # If no super admin and init flag is true, show initialization form + if not has_super_admin: + expected_email = os.getenv("VITE_SUPER_ADMIN_EMAIL") + return templates.TemplateResponse( + "admin/login.html", + { + "request": request, + "error": error, + "success": success, + "init_super_admin": True, + "expected_super_admin_email": expected_email + } + ) + + return templates.TemplateResponse( + "admin/login.html", + { + "request": request, + "error": error, + "success": success, + "init_super_admin": False + } + ) + +@router.post("/admin/login") +async def login( + request: Request, + response: Response, + email: str = Form(...), + password: str = Form(...) +): + """Handle admin login""" + try: + # Login with auth service + auth_result = await auth_service.login_admin(email, password) + + # Set session cookie and redirect + response = RedirectResponse(url="/api/admin/", status_code=303) + response.set_cookie( + "sb-access-token", + auth_result["access_token"], + httponly=True, + secure=True + ) + return response + + except HTTPException as e: + return RedirectResponse( + url=f"/api/admin/login?error={str(e.detail)}", + status_code=303 + ) + except Exception as e: + logger.error(f"Login error: {str(e)}") + return RedirectResponse( + url=f"/api/admin/login?error={str(e)}", + status_code=303 + ) + +@router.post("/admin/logout") +async def logout(response: Response): + """Handle admin logout""" + try: + response = RedirectResponse(url="/api/admin/login", status_code=303) + response.delete_cookie("sb-access-token") + return response + except Exception as e: + logger.error(f"Logout error: {str(e)}") + raise HTTPException(status_code=500, detail="Logout failed") + +@router.post("/admin/initialize-super-admin") +async def initialize_super_admin( + admin_data: Dict = Body(...), + request: Request = None +): + """Initialize the super admin account""" + try: + # Validate required fields + required_fields = ["email", "password", "display_name"] + for field in required_fields: + if field not in admin_data: + raise HTTPException(status_code=400, detail=f"Missing required field: {field}") + + # Set up super admin + admin_service = AdminService() + result = admin_service.setup_super_admin(admin_data) + + return { + "status": "success", + "message": "Super admin account created successfully! Please log in with your credentials.", + "admin": result + } + except Exception as e: + logger.error(f"Error initializing super admin: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/connections/__init__.py b/routers/connections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/connections/__pycache__/__init__.cpython-311.pyc b/routers/connections/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..97fe55b Binary files /dev/null and b/routers/connections/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/connections/__pycache__/arbor_router.cpython-311.pyc b/routers/connections/__pycache__/arbor_router.cpython-311.pyc new file mode 100644 index 0000000..f5ecf9c Binary files /dev/null and b/routers/connections/__pycache__/arbor_router.cpython-311.pyc differ diff --git a/routers/connections/arbor_router.py b/routers/connections/arbor_router.py new file mode 100644 index 0000000..8db38ab --- /dev/null +++ b/routers/connections/arbor_router.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, HTTPException +import os +import requests +from base64 import b64decode + +router = APIRouter() + +def get_basic_auth_header(token: str) -> dict: + """Decode the base64 token and return the appropriate header.""" + decoded_token = b64decode(token).decode('utf-8') + return {"Authorization": f"Basic {token}"} + +@router.get("/data/{id}") +async def fetch_arbor_data(id: int, token: str): + url_mapping = { + 1: os.environ["KS3_COURSE_CLASS_MEMBERSHIP_URL"], + 2: os.environ["TEACHING_GROUP_MEMBERSHIPS_2023_2024_URL"], + 3: os.environ["SCHEDULED_TIMETABLE_SLOTS_URL"], + 4: os.environ["BEHAVIOURAL_INCIDENTS_REPORTING_URL"], + 5: os.environ["Y7_LESSON_TIMETABLE_URL"] + } + if id not in url_mapping: + raise HTTPException(status_code=404, detail="Data ID not supported") + + headers = get_basic_auth_header(token) + response = requests.get(url_mapping[id], headers=headers) + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Failed to fetch data from Arbor") + return response.json() diff --git a/routers/connections/handleID_3.py b/routers/connections/handleID_3.py new file mode 100644 index 0000000..d372987 --- /dev/null +++ b/routers/connections/handleID_3.py @@ -0,0 +1,19 @@ +import sys +import json + +def filter_by_staff(data, staff_name="Kevin Carter"): + return [entry for entry in data if entry.get("Staff") == staff_name] + +if __name__ == "__main__": + if len(sys.argv) > 1: + staff_name = sys.argv[1] + else: + staff_name = "Kevin Carter" + + input_data = sys.stdin.read() + try: + data = json.loads(input_data) + filtered_data = filter_by_staff(data, staff_name) + print(json.dumps(filtered_data, indent=4)) + except json.JSONDecodeError: + print("Invalid JSON input", file=sys.stderr) \ No newline at end of file diff --git a/routers/connections/ollama_format_timetable.py b/routers/connections/ollama_format_timetable.py new file mode 100644 index 0000000..64a0e32 --- /dev/null +++ b/routers/connections/ollama_format_timetable.py @@ -0,0 +1,34 @@ +import os +import sys +import json +import requests + +def format_timetable_with_ollama(timetable_data): + url = f"{os.environ.get('APP_API_URL')}/llm/private/ollama/ollama_generate" + headers = {"Content-Type": "application/json"} + prompt = ( + "Create a markdown formatted table of the following timetable data. " + "The table should have columns for 'Day', 'Time Slot', 'Effective Dates', 'Event', 'Room', and 'Staff':\n\n" + f"{json.dumps(timetable_data, indent=4)}" + ) + payload = { + "model": "llama3", # Adjust the model name if necessary + "prompt": prompt + } + + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 200: + return response.json().get("response") + else: + raise Exception(f"Failed to get response from Ollama: {response.status_code} {response.text}") + +if __name__ == "__main__": + input_data = sys.stdin.read() + try: + timetable_data = json.loads(input_data) + markdown_table = format_timetable_with_ollama(timetable_data) + print(markdown_table) + except json.JSONDecodeError: + print("Invalid JSON input", file=sys.stderr) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) \ No newline at end of file diff --git a/routers/connections/openai_format_timetable.py b/routers/connections/openai_format_timetable.py new file mode 100644 index 0000000..c7283dc --- /dev/null +++ b/routers/connections/openai_format_timetable.py @@ -0,0 +1,45 @@ +import sys +import os +import json +import requests + +def format_timetable_with_openai(timetable_data): + url = f"{os.environ.get('APP_API_URL')}/llm/public/openai/openai_general_prompt" + headers = {"Content-Type": "application/json"} + prompt = ( + "Create a markdown formatted table of the following timetable data. " + "The table should have columns for 'Day', 'Time Slot', 'Effective Dates', 'Event', 'Room', and 'Staff':\n\n" + f"{json.dumps(timetable_data, indent=4)}" + ) + payload = { + "model": "gpt-4-turbo", # Adjust the model name if necessary + "prompt": prompt, + "max_tokens": 1500, + "temperature": 0.7, + "top_p": 1.0, + "n": 1, + "stop": None + } + + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 200: + return response.json().get("response") + else: + raise Exception(f"Failed to get response from OpenAI: {response.status_code} {response.text}") + +if __name__ == "__main__": + input_data = sys.stdin.read() + try: + timetable_data = json.loads(input_data) + markdown_table = format_timetable_with_openai(timetable_data) + + # Save the markdown table to a .md file + output_file = "timetable.md" + with open(output_file, "w") as file: + file.write(markdown_table) + + print(f"Markdown table saved to {output_file}") + except json.JSONDecodeError: + print("Invalid JSON input", file=sys.stderr) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) \ No newline at end of file diff --git a/routers/database/__init__.py b/routers/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/database/__pycache__/__init__.cpython-311.pyc b/routers/database/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..91e8fda Binary files /dev/null and b/routers/database/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/database/department.py b/routers/database/department.py new file mode 100644 index 0000000..2c5f3a7 --- /dev/null +++ b/routers/database/department.py @@ -0,0 +1,4 @@ +from fastapi import APIRouter, Depends, File, UploadFile +from backend.app.run.dependencies import admin_dependency + +router = APIRouter() diff --git a/routers/database/handle_connection.py b/routers/database/handle_connection.py new file mode 100644 index 0000000..fde870a --- /dev/null +++ b/routers/database/handle_connection.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, Depends +from backend.app.run.dependencies import admin_dependency + +import modules.database.tools.neo4j_driver_tools as driver +import modules.database.tools.neo4j_session_tools as session +import modules.database.tools.neo4j_http_tools as http +import modules.database.tools.queries as query + +router = APIRouter() + +# Handle neo4j driver +@router.post("/create-driver") +async def create_driver(driver: driver.Neo4jDriver = Depends(driver.get_neo4j_driver)): + return driver \ No newline at end of file diff --git a/routers/database/init/__init__.py b/routers/database/init/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/database/init/__pycache__/__init__.cpython-311.pyc b/routers/database/init/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f0daf5a Binary files /dev/null and b/routers/database/init/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/database/init/__pycache__/calendar.cpython-311.pyc b/routers/database/init/__pycache__/calendar.cpython-311.pyc new file mode 100644 index 0000000..31fa158 Binary files /dev/null and b/routers/database/init/__pycache__/calendar.cpython-311.pyc differ diff --git a/routers/database/init/__pycache__/curriculum.cpython-311.pyc b/routers/database/init/__pycache__/curriculum.cpython-311.pyc new file mode 100644 index 0000000..a619b2c Binary files /dev/null and b/routers/database/init/__pycache__/curriculum.cpython-311.pyc differ diff --git a/routers/database/init/__pycache__/entity_init.cpython-311.pyc b/routers/database/init/__pycache__/entity_init.cpython-311.pyc new file mode 100644 index 0000000..ed0d949 Binary files /dev/null and b/routers/database/init/__pycache__/entity_init.cpython-311.pyc differ diff --git a/routers/database/init/__pycache__/get_data.cpython-311.pyc b/routers/database/init/__pycache__/get_data.cpython-311.pyc new file mode 100644 index 0000000..20d6685 Binary files /dev/null and b/routers/database/init/__pycache__/get_data.cpython-311.pyc differ diff --git a/routers/database/init/__pycache__/schools.cpython-311.pyc b/routers/database/init/__pycache__/schools.cpython-311.pyc new file mode 100644 index 0000000..d63cadb Binary files /dev/null and b/routers/database/init/__pycache__/schools.cpython-311.pyc differ diff --git a/routers/database/init/__pycache__/timetables.cpython-311.pyc b/routers/database/init/__pycache__/timetables.cpython-311.pyc new file mode 100644 index 0000000..1598fbd Binary files /dev/null and b/routers/database/init/__pycache__/timetables.cpython-311.pyc differ diff --git a/routers/database/init/calendar.py b/routers/database/init/calendar.py new file mode 100644 index 0000000..6788c3f --- /dev/null +++ b/routers/database/init/calendar.py @@ -0,0 +1,32 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_init_calendar' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from modules.database.tools.neontology.basenode import BaseNode +import modules.database.init.init_calendar as init_calendar +from fastapi import APIRouter +from datetime import date +from fastapi import HTTPException + +router = APIRouter() + +@router.post("/create-calendar") +async def create_calendar(db_name: str, start_date: date, end_date: date, attach_to_calendar_node: bool = False, entity_node: BaseNode = None): + try: + logging.info(f"Creating calendar for {db_name} from {start_date} to {end_date}") + if entity_node is None: + logging.info("No user entity node provided, proceeding without attaching to user entity.") + return init_calendar.create_calendar(db_name, start_date, end_date, attach_to_calendar_node, entity_node) + except Exception as e: + logging.error(f"Error processing request: {e}") + raise HTTPException(status_code=422, detail=str(e)) \ No newline at end of file diff --git a/routers/database/init/classes.py b/routers/database/init/classes.py new file mode 100644 index 0000000..b0a1379 --- /dev/null +++ b/routers/database/init/classes.py @@ -0,0 +1,16 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +from fastapi import APIRouter, File, UploadFile, Form, BackgroundTasks + +router = APIRouter() + +@router.post("/upload-class-list") +async def upload_class_list( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + user_node: str = Form(...), + worker_node: str = Form(...) +): + pass diff --git a/routers/database/init/curriculum.py b/routers/database/init/curriculum.py new file mode 100644 index 0000000..2f6de65 --- /dev/null +++ b/routers/database/init/curriculum.py @@ -0,0 +1,50 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_init_curriculum' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.init.xl_tools as xl +import modules.database.init.init_school_curriculum as init_school_curriculum +from modules.database.schemas.nodes.schools.schools import SchoolNode +from fastapi import APIRouter, File, UploadFile, Form + +router = APIRouter() + +@router.post("/upload-curriculum") +async def upload_curriculum(file: UploadFile = File(...), db_name: str = Form(...)): + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return {"status": "Error", "message": "Invalid file format"} + logging.info(f"Uploading curriculum for {db_name}") + dataframes = xl.create_dataframes_from_fastapiuploadfile(file) + return init_school_curriculum.create_curriculum(db_name, dataframes) + +@router.post("/upload-school-curriculum") +async def upload_school_curriculum( + file: UploadFile = File(...), + db_name: str = Form(...), + school_uuid: str = Form(...), + school_name: str = Form(...), + school_website: str = Form(...), + school_path: str = Form(...) +): + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return {"status": "Error", "message": "Invalid file format"} + logging.info(f"Uploading curriculum for school {school_name} in {db_name}") + dataframes = xl.create_dataframes_from_fastapiuploadfile(file) + school_node = SchoolNode( + unique_id=f'School_{school_uuid}', + school_uuid=school_uuid, + school_name=school_name, + school_website=school_website, + path=school_path + ) + return init_school_curriculum.create_curriculum(db_name, dataframes, school_node) \ No newline at end of file diff --git a/routers/database/init/entity_init.py b/routers/database/init/entity_init.py new file mode 100644 index 0000000..e3fbb98 --- /dev/null +++ b/routers/database/init/entity_init.py @@ -0,0 +1,280 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neo4j_session_tools as session_tools +import modules.database.init.init_user as init_user +from modules.database.tools.neo4j_db_formatter import format_user_email_for_neo_db +import modules.database.init.init_school as init_school +import modules.database.init.init_school_timetable as init_school_timetable +import modules.database.init.init_school_curriculum as init_school_curriculum +import modules.database.init.xl_tools as xl +from modules.database.schemas.nodes.schools.schools import SchoolNode, SubjectClassNode, RoomNode, DepartmentNode +from fastapi import APIRouter, Form, HTTPException +from fastapi.responses import JSONResponse +import json + +VALID_USER_TYPES = ['admin', 'cc_admin', 'cc_email_school_admin', 'cc_ms_school_admin', 'email_school_admin', 'ms_school_admin', 'cc_email_teacher', 'cc_ms_teacher', 'cc_email_student', 'cc_ms_student', 'email_teacher', 'ms_teacher', 'email_student', 'ms_student', 'ms_federated_teacher', 'ms_federated_student', 'standard', 'developer'] # TODO: Implement dev_ user types for pytests, consider use of cc_ user types + +router = APIRouter() + +# Helpers +def initialise_schools_from_config(): + """Initialize a school with the configuration provided from env variables + """ + default_config = { + "school_uuid": "kevlarai", + "school_name": "KevlarAI School", + "school_website": "https://kevlarai.com", + "timetable_file": "kevlarai_data/kevlarai_timetable.xlsx", + "curriculum_file": "kevlarai_data/kevlarai_curriculum.xlsx" + } + + # school_config_str = os.getenv("SCHOOL_CONFIG") # TODO: Implement this + school_config = default_config + + db_name = f"cc.institutes.{school_config['school_uuid']}" + curriculum_db_name = f"{db_name}.curriculum" + + logger.info(f"Creating database for {school_config['school_name']} using db_name: {db_name}") + driver = driver_tools.get_driver() + if driver is None: + logger.error("Failed to connect to Neo4j") + return + + with driver.session() as session: + # Create main school database + session_tools.create_database(session, db_name) + logger.debug(f"Database {db_name} created") + + # Create curriculum database + session_tools.create_database(session, curriculum_db_name) + logger.debug(f"Curriculum database {curriculum_db_name} created") + + # Add filesystem path debugging + base_path = os.getenv("NODE_FILESYSTEM_PATH") + schools_path = os.path.join(base_path, "schools") + school_path = os.path.join(schools_path, f"cc.institutes.{school_config['school_uuid']}") + + logger.debug("Filesystem paths:", { + "base_path": base_path, + "schools_path": schools_path, + "school_path": school_path + }) + + # Check if directories exist + logger.debug("Directory existence check:", { + "base_exists": os.path.exists(base_path), + "schools_exists": os.path.exists(schools_path), + "school_exists": os.path.exists(school_path) + }) + + # Create database entry for school without timetable or curriculum + logger.info(f"Creating school entry for {school_config['school_name']} in database {db_name} without timetable or curriculum") + result = init_school.create_school( + db_name=db_name, + school_uuid=school_config["school_uuid"], + school_name=school_config["school_name"], + school_website=school_config["school_website"] + ) + logger.success(f"{school_config['school_name']} school entry created successfully") + + # Create school node from result + school_node = result['school_node'] + refreshed_school_node = SchoolNode( + unique_id=school_node.unique_id, + school_uuid=school_node.school_uuid, + school_name=school_node.school_name, + school_website=school_node.school_website, + path=school_node.path + ) + + # Create timetable entries for school from Excel file + timetable_file = os.path.join(os.getenv("BACKEND_INIT_PATH"), school_config["timetable_file"]) + + logger.info(f"Creating timetable entries for {school_config['school_name']} using timetable file: {timetable_file}.") + school_timetable_dataframes = xl.create_dataframes(timetable_file) + + + init_school_timetable.create_school_timetable( + dataframes=school_timetable_dataframes, + db_name=db_name, + school_node=refreshed_school_node + ) + logger.success("Timetable entries created successfully") + + # Create curriculum entries for school from Excel file in both databases + curriculum_file = os.path.join(os.getenv("BACKEND_INIT_PATH"), school_config["curriculum_file"]) + school_curriculum_dataframes = xl.create_dataframes(curriculum_file) + + logger.info(f"Creating curriculum entries for {school_config['school_name']} using curriculum file: {curriculum_file}.") + init_school_curriculum.create_curriculum( + dataframes=school_curriculum_dataframes, + db_name=db_name, + curriculum_db_name=curriculum_db_name, + school_node=refreshed_school_node + ) + logger.success("Curriculum entries created successfully") + + +@router.post("/create-user") +async def create_user( + user_id: str = Form(...), + user_type: str = Form(...), + user_name: str = Form(...), + user_email: str = Form(...), + school_uuid: str = Form(None), + school_name: str = Form(None), + school_website: str = Form(None), + school_path: str = Form(None), + worker_data: str = Form(None) +): + logger.info(f"Creating user with user_id: {user_id}, user_type: {user_type}, user_name: {user_name}, user_email: {user_email}") + + if school_uuid: + logger.info(f"School UUID provided: {school_uuid}") + else: + logger.info(f"No school UUID provided") + + if school_name: + logger.info(f"School name provided: {school_name}") + else: + logger.info(f"No school name provided") + + if school_website: + logger.info(f"School website provided: {school_website}") + else: + logger.info(f"No school website provided") + + if school_path: + logger.info(f"School path provided: {school_path}") + else: + logger.info(f"No school path provided") + + if worker_data: + logger.info(f"Worker data provided: {worker_data}") + else: + logger.info(f"No worker data provided") + + # Validate inputs + if any(param is None for param in (user_type, user_name, user_email, user_id)): + raise HTTPException(status_code=400, detail=f"Invalid user data") + + if user_type not in VALID_USER_TYPES: + raise HTTPException(status_code=400, detail=f"Invalid user type: {user_type}") + + try: + # Parse worker data + worker_data_dict = json.loads(worker_data) if worker_data else None + + # Create school node if school data provided + school_node = None + if all([school_uuid, school_name, school_website, school_path]): + school_node = SchoolNode( + unique_id=f'School_{school_uuid}', + school_uuid=school_uuid, + school_name=school_name, + school_website=school_website, + path=school_path + ) + + # Create user with single database reference + formatted_email = format_user_email_for_neo_db(user_email) + user_db_name = f"cc.users.{formatted_email}" + + result = init_user.create_user( + db_name=user_db_name, + user_id=user_id, + user_type=user_type, + username=user_name, + email=user_email, + school_node=school_node, + worker_data=worker_data_dict + ) + + # Ensure the result is JSON serializable + response_data = { + "status": "success", + "data": { + "user_node": result['user_node'], + "worker_node": result['worker_node'], + "calendar_nodes": result.get('calendar_nodes') + } + } + + return JSONResponse(content=response_data) + + except Exception as e: + logger.error(f"Error creating user in Neo4j: {str(e)}", exc_info=True) + return JSONResponse( + content={"status": "error", "message": str(e)}, + status_code=500 + ) + +@router.post("/create-schools") +async def create_schools(): + initialise_schools_from_config() + return JSONResponse(content={"status": "success", "message": "Schools created successfully"}) + +@router.post("/create-department") +async def create_department( + db_name: str = Form(...), + unique_id: str = Form(...), + department_name: str = Form(...), + department_code: str = Form(...), + path: str = Form(...) +): + if db_name is None or unique_id is None or department_name is None or department_code is None or path is None: + logging.error(f"Invalid department data: {db_name}, {unique_id}, {department_name}, {department_code}, {path}") + raise HTTPException(status_code=400, detail="Invalid department data") + + department = DepartmentNode( + unique_id=unique_id, + department_name=department_name, + department_code=department_code, + path=path + ) + + logger.info(f"Creating department {department_name} with unique_id {unique_id}") + try: + result = init_school.create_department(db_name, department) + return JSONResponse(content={"status": "success", "data": result}) + except Exception as e: + logger.error(f"Error creating department: {str(e)}") + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) + +@router.post("/create-class") +async def create_class( + db_name: str = Form(...), + unique_id: str = Form(...), + subject_class_code: str = Form(...), + year_group: str = Form(...), + subject: str = Form(...), + subject_code: str = Form(...), + path: str = Form(...) +): + subject_class_node = SubjectClassNode( + unique_id=unique_id, + subject_class_code=subject_class_code, + year_group=year_group, + subject=subject, + subject_code=subject_code, + path=path + ) + # Implementation for creating a class + pass + +@router.post("/create-room") +async def create_room( + db_name: str = Form(...), + room_unique_id: str = Form(...), + room_code: str = Form(...), + path: str = Form(...) +): + room = RoomNode( + room_unique_id=room_unique_id, + room_code=room_code, + path=path + ) + # Implementation for creating a room + pass \ No newline at end of file diff --git a/routers/database/init/get_data.py b/routers/database/init/get_data.py new file mode 100644 index 0000000..49f2416 --- /dev/null +++ b/routers/database/init/get_data.py @@ -0,0 +1,28 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_init_get_data' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.init.xl_tools as xl +from fastapi import APIRouter, File, UploadFile + +router = APIRouter() + +@router.post("/get-dataframes-from-xl") +async def get_dataframes_from_xl(file: UploadFile = File(...)): + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return {"status": "Error", "message": "Invalid file format"} + try: + logging.info(f"Getting dataframes from {file.filename}") + return xl.create_dataframes(await file.read()) + except Exception as e: + return {"status": "Error", "message": str(e)} \ No newline at end of file diff --git a/routers/database/init/schools.py b/routers/database/init/schools.py new file mode 100644 index 0000000..32d4151 --- /dev/null +++ b/routers/database/init/schools.py @@ -0,0 +1,115 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_init_schools' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks +import pandas as pd +import modules.database.tools.neo4j_driver_tools as driver +from modules.database.tools.neo4j_session_tools import get_node_by_unique_id +import modules.database.init.init_school_timetable as init_school_timetable +import modules.database.init.init_worker_timetable as init_worker_timetable +from modules.database.schemas.nodes.schools.schools import SchoolNode +import modules.database.init.xl_tools as xl +import json + +router = APIRouter() + +@router.post("/upload-school-timetable") +async def upload_school_timetable( + file: UploadFile = File(...), + db_name: str = Form(...), + unique_id: str = Form(...), + school_uuid: str = Form(...), + school_name: str = Form(...), + school_website: str = Form(...), + path: str = Form(...) +): + school_node = SchoolNode( + unique_id=unique_id, + school_uuid=school_uuid, + school_name=school_name, + school_website=school_website, + path=path + ) + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return {"status": "Error", "message": "Invalid file format"} + logging.info(f"Uploading timetable for {db_name} from {file.filename}") + dataframes = xl.create_dataframes_from_fastapiuploadfile(file) + return init_school_timetable.create_school_timetable(dataframes, db_name, school_node) + +@router.post("/upload-worker-timetable") +async def upload_worker_timetable( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + worker_node: str = Form(...) +): + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + raise HTTPException(status_code=422, detail="Invalid file format") + + try: + worker_node_data = json.loads(worker_node) + logging.info(f"Uploading worker timetable for {worker_node_data['teacher_code']} from {file.filename} for {worker_node_data['worker_db_name']}") + logging.debug(f"Worker node data: {worker_node_data}") + + # Read file content into memory + file_content = await file.read() + + # Schedule the processing of the timetable in the background + background_tasks.add_task( + process_worker_timetable, + file_content, + worker_node_data + ) + + return { + "status": "Accepted", + "message": "Processing of teacher timetable started" + } + except Exception as e: + logging.error(f"Error handling timetable upload: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +async def process_worker_timetable(file_content, worker_node_data): + neo_driver = driver.get_driver(db_name=worker_node_data['worker_db_name']) + if neo_driver is None: + logging.error(f"Failed to connect to the database {worker_node_data['worker_db_name']}") + return + + try: + # Create a DataFrame from the file content + from io import BytesIO + timetable_df = pd.read_excel(BytesIO(file_content)) + + # Get the school version of the worker node + logging.info(f"Getting school worker node for {worker_node_data['unique_id']} from {worker_node_data['worker_db_name']}") + + with neo_driver.session(database=worker_node_data['worker_db_name']) as neo_session: + school_worker_node = get_node_by_unique_id(session=neo_session, unique_id=worker_node_data['unique_id']) + + if school_worker_node is None: + error_msg = f"School worker node not found for unique_id: {worker_node_data['unique_id']}" + logging.error(error_msg) + raise Exception(error_msg) + + logging.debug(f"School worker node found: {school_worker_node}") + + logging.info(f"Initializing worker timetable for school worker: {school_worker_node['teacher_code']}") + init_worker_timetable.init_worker_timetable(timetable_df, school_worker_node) + logging.info(f"Worker timetable initialized for school worker: {school_worker_node['teacher_code']}") + + except Exception as e: + logging.error(f"Error processing worker timetable: {str(e)}") + raise + finally: + logging.info(f"Closing driver for {worker_node_data['worker_db_name']}") + driver.close_driver(neo_driver) \ No newline at end of file diff --git a/routers/database/init/timetables.py b/routers/database/init/timetables.py new file mode 100644 index 0000000..1919c77 --- /dev/null +++ b/routers/database/init/timetables.py @@ -0,0 +1,166 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_init_timetables' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from fastapi import APIRouter, File, UploadFile, Form, HTTPException, BackgroundTasks +import pandas as pd +import modules.database.tools.neo4j_driver_tools as driver +from modules.database.tools.neo4j_session_tools import get_node_by_unique_id +import modules.database.init.init_school_timetable as init_school_timetable +import modules.database.init.init_worker_timetable as init_worker_timetable +from modules.database.schemas.nodes.users import UserNode +from modules.database.schemas.nodes.schools.schools import SchoolNode +from modules.database.schemas.nodes.workers.workers import TeacherNode +import modules.database.init.xl_tools as xl +import json +import modules.database.tools.neontology_tools as neon + +router = APIRouter() + +@router.post("/upload-school-timetable") +async def upload_school_timetable( + file: UploadFile = File(...), + db_name: str = Form(...), + unique_id: str = Form(...), + school_uuid: str = Form(...), + school_name: str = Form(...), + school_website: str = Form(...), + path: str = Form(...) +): + school_node = SchoolNode( + unique_id=unique_id, + school_uuid=school_uuid, + school_name=school_name, + school_website=school_website, + path=path + ) + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + return {"status": "Error", "message": "Invalid file format"} + logging.info(f"Uploading timetable for {db_name} from {file.filename}") + dataframes = xl.create_dataframes_from_fastapiuploadfile(file) + return init_school_timetable.create_school_timetable(dataframes, db_name, school_node) + +@router.post("/upload-worker-timetable") +async def upload_worker_timetable( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + user_node: str = Form(...), + worker_node: str = Form(...) +): + if file.content_type != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + raise HTTPException(status_code=422, detail="Invalid file format") + + try: + worker_node_data = json.loads(worker_node) + user_node_data = json.loads(user_node) + logging.info(f"Uploading worker timetable for {worker_node_data['teacher_code']} from {file.filename} for {worker_node_data['worker_db_name']}") + logging.debug(f"Worker node data: {worker_node_data}") + logging.debug(f"User node data: {user_node_data}") + + # Read file content into memory + file_content = await file.read() + + # Schedule the processing of the timetable in the background + background_tasks.add_task( + process_worker_timetable, + file_content, + user_node_data, + worker_node_data + ) + + return { + "status": "Accepted", + "message": "Processing of teacher timetable started" + } + except Exception as e: + logging.error(f"Error handling timetable upload: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +async def process_worker_timetable(file_content, user_node_data, worker_node_data): + # Initialize neontology connection first + neon.init_neontology_connection() + + neo_driver = driver.get_driver(db_name=worker_node_data['worker_db_name']) + if neo_driver is None: + logging.error(f"Failed to connect to the database {worker_node_data['worker_db_name']}") + return + + try: + # Create a DataFrame from the file content + from io import BytesIO + timetable_df = pd.read_excel(BytesIO(file_content)) + + # Get the school version of the worker node + logging.info(f"Getting school worker node for {worker_node_data['unique_id']} from {worker_node_data['worker_db_name']}") + + with neo_driver.session(database=worker_node_data['worker_db_name']) as neo_session: + school_worker_node = get_node_by_unique_id(session=neo_session, unique_id=worker_node_data['unique_id']) + + if school_worker_node is None: + error_msg = f"School worker node not found for unique_id: {worker_node_data['unique_id']}" + logging.error(error_msg) + raise Exception(error_msg) + + logging.debug(f"School worker node found: {school_worker_node}") + + # Create timetable in school database + logging.info(f"Initializing worker timetable for school worker: {school_worker_node['teacher_code']}") + init_worker_timetable.init_worker_timetable(timetable_df, school_worker_node) + logging.info(f"Worker timetable initialized for school worker: {school_worker_node['teacher_code']}") + + # Create timetable in user database + if 'user_db_name' in worker_node_data: + from modules.database.init.init_user_timetable import create_user_worker_timetable + from modules.database.schemas.nodes.workers.workers import TeacherNode + + logging.info(f"Creating user timetable structure in {worker_node_data['user_db_name']}") + + # Create TeacherNode from worker_node_data + user_worker_node = TeacherNode( + unique_id=worker_node_data['unique_id'], + teacher_code=worker_node_data['teacher_code'], + teacher_name_formal=worker_node_data['teacher_name_formal'], + teacher_email=worker_node_data['teacher_email'], + path=worker_node_data['path'], + worker_db_name=worker_node_data['worker_db_name'], + user_db_name=worker_node_data['user_db_name'] + ) + + # Create user node + user_node = UserNode( + unique_id=user_node_data['unique_id'], + user_id=user_node_data['user_id'], + user_type=user_node_data['user_type'], + user_name=user_node_data['user_name'], + user_email=user_node_data['user_email'], + path=user_node_data['path'], + worker_node_data=user_node_data['worker_node_data'] + ) + + # Create user timetable structure + create_user_worker_timetable( + user_node=user_node, + user_worker_node=user_worker_node, + school_db_name=worker_node_data['worker_db_name'] + ) + + logging.info(f"User timetable structure created in {worker_node_data['user_db_name']}") + else: + logging.warning("No user_db_name provided, skipping user timetable creation") + + except Exception as e: + logging.error(f"Error processing worker timetable: {str(e)}") + raise + finally: + logging.info(f"Closing driver for {worker_node_data['worker_db_name']}") + driver.close_driver(neo_driver) \ No newline at end of file diff --git a/routers/database/schools.py b/routers/database/schools.py new file mode 100644 index 0000000..bdb7897 --- /dev/null +++ b/routers/database/schools.py @@ -0,0 +1,99 @@ +from fastapi import APIRouter, Depends, File, UploadFile +from backend.app.run.dependencies import admin_dependency +from pydantic import BaseModel + +router = APIRouter() + +class NodeBase(BaseModel): + Name: str + +class LocalAuthority(NodeBase): + pass + +class SchoolNode(NodeBase): + Type: str + Status: str + +class ParliamentaryConstituency(NodeBase): + pass + +class AdministrativeWard(NodeBase): + pass + +class RelationshipBase(BaseModel): + start_node: NodeBase + end_node: NodeBase + relationship_type: str + +class HasParliamentaryConstituency(RelationshipBase): + pass + +class HasAdministrativeWard(RelationshipBase): + pass + +class HasSchool(RelationshipBase): + pass + +@router.post("/batch-create-schools") +async def add_school_to_global(file: UploadFile = File(...)): + if file is None: + return {"status": "Error", "message": "No file received"} + + try: + import pandas as pd + from io import BytesIO + from app.modules.driver_tools import create_node_http, create_relationship_http + data = pd.read_csv(BytesIO(await file.read()), usecols=["LA (name)", "ParliamentaryConstituency (name)", "AdministrativeWard (name)", "EstablishmentName", "TypeOfEstablishment (name)", "EstablishmentStatus (name)"]) + unique_las = data["LA (name)"].unique() + for la_name in unique_las: + la_node = {"Name": la_name} + la_id = create_node_http("LocalAuthority", la_node, db="GlobalSchools") + constituencies = data[data["LA (name)"] == la_name]["ParliamentaryConstituency (name)"].unique() + for constituency in constituencies: + constituency_node = {"Name": constituency} + constituency_id = create_node_http("ParliamentaryConstituency", constituency_node, db="GlobalSchools") + create_relationship_http({"start_node": {"id": la_id}, "end_node": {"id": constituency_id}, "relationship_type": "HAS_PARLIAMENTARY_CONSTITUENCY"}, db="GlobalSchools") + wards = data[(data["LA (name)"] == la_name) & (data["ParliamentaryConstituency (name)"] == constituency)]["AdministrativeWard (name)"].unique() + for ward in wards: + ward_node = {"Name": ward} + ward_id = create_node_http("AdministrativeWard", ward_node, db="GlobalSchools") + create_relationship_http({"start_node": {"id": constituency_id}, "end_node": {"id": ward_id}, "relationship_type": "HAS_ADMINISTRATIVE_WARD"}, db="GlobalSchools") + schools = data[(data["LA (name)"] == la_name) & (data["ParliamentaryConstituency (name)"] == constituency) & (data["AdministrativeWard (name)"] == ward)] + for index, school in schools.iterrows(): + school_node = { + "Name": school["EstablishmentName"], + "Type": school["TypeOfEstablishment (name)"], + "Status": school["EstablishmentStatus (name)"] + } + school_id = create_node_http("School", school_node, db="GlobalSchools") + create_relationship_http({"start_node": {"id": ward_id}, "end_node": {"id": school_id}, "relationship_type": "HAS_SCHOOL"}, db="GlobalSchools") + return {"status": "Success", "message": "Graph structure updated successfully"} + except Exception as e: + print("Failed to process file:", e) + return {"status": "Error", "message": "Failed to process file"} + +@router.post("/create-school") +async def add_school_to_global(file: UploadFile = File(...)): + if file is None: + return {"status": "Error", "message": "No file received"} + + try: + import pandas as pd + from io import BytesIO + data = pd.read_excel(BytesIO(await file.read()), usecols=[0], nrows=5).squeeze() + print("Data read from file:", data) + if len(data) < 5: + return {"status": "Error", "message": "Insufficient data in file"} + school_data = { + "name": data[0], + "address": data[1], + "ofsted_number": data[2], + "website": data[3], + "geo_location": data[4] + } + from app.modules.driver_tools import create_node_http + response = create_node_http("globalschools", "School", school_data) + return {"status": "School added to global school db via HTTP", "school_data": school_data, "response": response} + except Exception as e: + print("Failed to process file:", e) + return {"status": "Error", "message": "Failed to process file"} diff --git a/routers/database/student.py b/routers/database/student.py new file mode 100644 index 0000000..bb08ecb --- /dev/null +++ b/routers/database/student.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter, Depends, File, UploadFile +from backend.app.run.dependencies import admin_dependency + +router = APIRouter() + diff --git a/routers/database/teacher.py b/routers/database/teacher.py new file mode 100644 index 0000000..bb08ecb --- /dev/null +++ b/routers/database/teacher.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter, Depends, File, UploadFile +from backend.app.run.dependencies import admin_dependency + +router = APIRouter() + diff --git a/routers/database/tools/__init__.py b/routers/database/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/database/tools/__pycache__/__init__.cpython-311.pyc b/routers/database/tools/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..535f516 Binary files /dev/null and b/routers/database/tools/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/calendar_structure_router.cpython-311.pyc b/routers/database/tools/__pycache__/calendar_structure_router.cpython-311.pyc new file mode 100644 index 0000000..be7bf5a Binary files /dev/null and b/routers/database/tools/__pycache__/calendar_structure_router.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/default_nodes_router.cpython-311.pyc b/routers/database/tools/__pycache__/default_nodes_router.cpython-311.pyc new file mode 100644 index 0000000..57b0515 Binary files /dev/null and b/routers/database/tools/__pycache__/default_nodes_router.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/get_events.cpython-311.pyc b/routers/database/tools/__pycache__/get_events.cpython-311.pyc new file mode 100644 index 0000000..bbb31c9 Binary files /dev/null and b/routers/database/tools/__pycache__/get_events.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/get_nodes.cpython-311.pyc b/routers/database/tools/__pycache__/get_nodes.cpython-311.pyc new file mode 100644 index 0000000..66ac8d9 Binary files /dev/null and b/routers/database/tools/__pycache__/get_nodes.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/get_nodes_and_edges.cpython-311.pyc b/routers/database/tools/__pycache__/get_nodes_and_edges.cpython-311.pyc new file mode 100644 index 0000000..2b30e79 Binary files /dev/null and b/routers/database/tools/__pycache__/get_nodes_and_edges.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/tldraw_filesystem.cpython-311.pyc b/routers/database/tools/__pycache__/tldraw_filesystem.cpython-311.pyc new file mode 100644 index 0000000..e7cc29f Binary files /dev/null and b/routers/database/tools/__pycache__/tldraw_filesystem.cpython-311.pyc differ diff --git a/routers/database/tools/__pycache__/worker_structure_router.cpython-311.pyc b/routers/database/tools/__pycache__/worker_structure_router.cpython-311.pyc new file mode 100644 index 0000000..de5d4f1 Binary files /dev/null and b/routers/database/tools/__pycache__/worker_structure_router.cpython-311.pyc differ diff --git a/routers/database/tools/calendar_structure_router.py b/routers/database/tools/calendar_structure_router.py new file mode 100644 index 0000000..d5e6d73 --- /dev/null +++ b/routers/database/tools/calendar_structure_router.py @@ -0,0 +1,220 @@ +import os +from fastapi import APIRouter, HTTPException +from typing import List, Dict, Any, Optional +from datetime import datetime, timedelta +from modules.logger_tool import initialise_logger +from modules.database.tools import neo4j_driver_tools as driver_tools + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +router = APIRouter() + +@router.get("/get-calendar-structure") +async def get_calendar_structure(db_name: str) -> Dict[str, Any]: + """ + Get the complete calendar structure including years, months, weeks, and days. + """ + try: + # Get all calendar nodes in a single query + query = """ + // Match all calendar-related nodes + MATCH (y:CalendarYear) + OPTIONAL MATCH (y)-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth) + OPTIONAL MATCH (m)-[:MONTH_INCLUDES_DAY]->(d:CalendarDay) + OPTIONAL MATCH (w:CalendarWeek)-[:WEEK_INCLUDES_DAY]->(d) + WITH y, m, w, d + ORDER BY y.date, m.date, w.date, d.date + + // Collect all nodes with dates converted to strings + RETURN { + years: collect(DISTINCT { + id: y.unique_id, + path: y.path, + date: toString(y.date), + __primarylabel__: 'CalendarYear' + }), + months: collect(DISTINCT { + id: m.unique_id, + path: m.path, + date: toString(m.date), + __primarylabel__: 'CalendarMonth' + }), + weeks: collect(DISTINCT { + id: w.unique_id, + path: w.path, + date: toString(w.date), + __primarylabel__: 'CalendarWeek' + }), + days: collect(DISTINCT { + id: d.unique_id, + path: d.path, + date: toString(d.date), + week_id: w.unique_id, + month_id: m.unique_id, + __primarylabel__: 'CalendarDay' + }) + } as structure + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + record = result.single() + if not record: + raise HTTPException(status_code=404, detail="Calendar structure not found") + + structure = record["structure"] + + # Find current day using string comparison + today = datetime.now().strftime("%Y-%m-%d") + current_day = next( + (day["id"] for day in structure["days"] + if day["date"] == today), + structure["days"][0]["id"] if structure["days"] else None + ) + + return { + "status": "success", + "structure": { + "years": structure["years"], + "months": structure["months"], + "weeks": structure["weeks"], + "days": structure["days"], + "currentDay": current_day + } + } + + except Exception as e: + logger.error(f"Error getting calendar structure: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-calendar-days") +async def get_calendar_days(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]: + """ + Get all calendar days in a date range. + """ + try: + query = """ + MATCH (d:CalendarDay) + WHERE date(d.date) >= date($start_date) AND date(d.date) <= date($end_date) + OPTIONAL MATCH (w:CalendarWeek)-[:WEEK_INCLUDES_DAY]->(d) + OPTIONAL MATCH (m:CalendarMonth)-[:MONTH_INCLUDES_DAY]->(d) + RETURN { + id: d.unique_id, + path: d.path, + date: d.date, + week_id: w.unique_id, + month_id: m.unique_id, + __primarylabel__: 'CalendarDay' + } as day + ORDER BY d.date + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, start_date=start_date, end_date=end_date) + days = [record["day"] for record in result] + + return { + "status": "success", + "days": days + } + + except Exception as e: + logger.error(f"Error getting calendar days: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-calendar-weeks") +async def get_calendar_weeks(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]: + """ + Get all calendar weeks in a date range. + """ + try: + query = """ + MATCH (w:CalendarWeek)-[:WEEK_INCLUDES_DAY]->(d:CalendarDay) + WHERE date(w.date) >= date($start_date) AND date(w.date) <= date($end_date) + WITH w, collect(d) as days + RETURN { + id: w.unique_id, + path: w.path, + date: w.date, + day_ids: [day in days | day.unique_id], + __primarylabel__: 'CalendarWeek' + } as week + ORDER BY w.date + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, start_date=start_date, end_date=end_date) + weeks = [record["week"] for record in result] + + return { + "status": "success", + "weeks": weeks + } + + except Exception as e: + logger.error(f"Error getting calendar weeks: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-calendar-months") +async def get_calendar_months(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]: + """ + Get all calendar months in a date range. + """ + try: + query = """ + MATCH (m:CalendarMonth)-[:MONTH_INCLUDES_DAY]->(d:CalendarDay) + WHERE date(m.date) >= date($start_date) AND date(m.date) <= date($end_date) + WITH m, collect(d) as days + RETURN { + id: m.unique_id, + path: m.path, + date: m.date, + day_ids: [day in days | day.unique_id], + __primarylabel__: 'CalendarMonth' + } as month + ORDER BY m.date + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, start_date=start_date, end_date=end_date) + months = [record["month"] for record in result] + + return { + "status": "success", + "months": months + } + + except Exception as e: + logger.error(f"Error getting calendar months: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-calendar-years") +async def get_calendar_years(db_name: str) -> Dict[str, Any]: + """ + Get all calendar years. + """ + try: + query = """ + MATCH (y:CalendarYear)-[:YEAR_INCLUDES_MONTH]->(m:CalendarMonth) + WITH y, collect(m) as months + RETURN { + id: y.unique_id, + path: y.path, + date: y.date, + month_ids: [month in months | month.unique_id], + __primarylabel__: 'CalendarYear' + } as year + ORDER BY y.date + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + years = [record["year"] for record in result] + + return { + "status": "success", + "years": years + } + + except Exception as e: + logger.error(f"Error getting calendar years: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/routers/database/tools/default_nodes_router.py b/routers/database/tools/default_nodes_router.py new file mode 100644 index 0000000..f835e46 --- /dev/null +++ b/routers/database/tools/default_nodes_router.py @@ -0,0 +1,257 @@ +from fastapi import APIRouter, HTTPException +from typing import Dict, Any +from modules.database.tools import neo4j_driver_tools as driver_tools +from modules.logger_tool import initialise_logger +from neo4j.time import DateTime, Date +import os +from datetime import datetime + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +router = APIRouter() + +def convert_neo4j_values(value: Any) -> Any: + """Convert Neo4j types to JSON-serializable types.""" + if isinstance(value, DateTime): + return value.isoformat() # Convert to ISO format string + elif isinstance(value, Date): + return value.isoformat() # Convert Date to ISO format string + elif isinstance(value, dict): + return {k: convert_neo4j_values(v) for k, v in value.items()} + elif isinstance(value, list): + return [convert_neo4j_values(v) for v in value] + return value + +def get_default_node_week(db_name: str) -> Dict[str, Any]: + """Get the current week node.""" + # Get today's date + today = datetime.now() + + # Find the calendar week node that contains today's date + query = """ + MATCH (w:CalendarWeek) + WHERE date(w.start_date) <= date($today) AND date($today) <= date(w.start_date) + duration('P7D') + RETURN w + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, today=today.strftime('%Y-%m-%d')) + week_node = result.single() + + if not week_node: + raise HTTPException(status_code=404, detail="No default node found for context: week") + + node = week_node["w"] + node_data = dict(node) + converted_data = convert_neo4j_values(node_data) + + return { + "status": "success", + "node": { + "id": node["unique_id"], + "path": node["path"], + "type": "CalendarWeek", + "label": node.get("title", "Calendar Week"), + "data": converted_data + } + } + +def get_default_node_month(db_name: str) -> Dict[str, Any]: + """Get the current month node.""" + # Get today's date + today = datetime.now() + + # Find the calendar month node for the current month + query = """ + MATCH (m:CalendarMonth) + WHERE m.year = $year AND m.month = $month + RETURN m + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, year=str(today.year), month=str(today.month)) + month_node = result.single() + + if not month_node: + raise HTTPException(status_code=404, detail="No default node found for context: month") + + node = month_node["m"] + node_data = dict(node) + converted_data = convert_neo4j_values(node_data) + + return { + "status": "success", + "node": { + "id": node["unique_id"], + "path": node["path"], + "type": "CalendarMonth", + "label": node.get("title", "Calendar Month"), + "data": converted_data + } + } + +@router.get("/get-default-node/{context}") +async def get_default_node(context: str, db_name: str, base_context: str | None = None) -> Dict[str, Any]: + """Get the default node for a given context.""" + try: + # Handle special cases for week and month + if context == 'week': + return get_default_node_week(db_name) + elif context == 'month': + return get_default_node_month(db_name) + + # Map contexts to their default node queries + context_queries = { + # Base Contexts + 'profile': """ + MATCH (n:User) + RETURN n LIMIT 1 + """, + 'worker': """ + MATCH (n) + WHERE n:SchoolAdmin OR n:Teacher OR n:Student OR n:Developer OR n:SuperAdmin + RETURN n LIMIT 1 + """, + 'calendar': """ + MATCH (n:Calendar) + RETURN n LIMIT 1 + """, + 'teaching': """ + MATCH (n:Teacher) + RETURN n LIMIT 1 + """, + 'school': """ + MATCH (n:School) + RETURN n LIMIT 1 + """, + 'department': """ + MATCH (n:Department) + RETURN n LIMIT 1 + """, + 'class': """ + MATCH (n:Class) + RETURN n LIMIT 1 + """, + + # Extended Contexts - Overview queries for each base context + 'overview': """ + MATCH (n) + WHERE CASE $base_context + WHEN 'profile' THEN n:User + WHEN 'calendar' THEN n:Calendar + WHEN 'teaching' THEN n:Teacher + WHEN 'school' THEN n:School + WHEN 'department' THEN n:Department + WHEN 'class' THEN n:Class + ELSE false + END + RETURN n LIMIT 1 + """, + + # Extended Contexts - User + 'settings': """ + MATCH (n:User) + RETURN n LIMIT 1 + """, + 'history': """ + MATCH (n:User) + RETURN n LIMIT 1 + """, + 'journal': """ + MATCH (n:Journal) + RETURN n LIMIT 1 + """, + 'planner': """ + MATCH (n:Planner) + RETURN n LIMIT 1 + """, + + # Extended Contexts - Calendar + 'day': """ + MATCH (n:CalendarDay) + WHERE date(n.date) = date() + RETURN n LIMIT 1 + """, + 'year': """ + MATCH (n:CalendarYear) + WHERE n.year = toString(date().year) + RETURN n LIMIT 1 + """, + + # Extended Contexts - Teaching + 'timetable': """ + MATCH (n:UserTeacherTimetable) + RETURN n LIMIT 1 + """, + 'classes': """ + MATCH (n:Class) + RETURN n LIMIT 1 + """, + 'lessons': """ + MATCH (n:TimetableLesson) + RETURN n LIMIT 1 + """, + + # Extended Contexts - School + 'departments': """ + MATCH (n:Department) + RETURN n LIMIT 1 + """, + 'staff': """ + MATCH (n:Teacher) + RETURN n LIMIT 1 + """, + + # Extended Contexts - Department + 'teachers': """ + MATCH (n:Teacher) + RETURN n LIMIT 1 + """, + 'subjects': """ + MATCH (n:Subject) + RETURN n LIMIT 1 + """, + + # Extended Contexts - Class + 'students': """ + MATCH (n:Student) + RETURN n LIMIT 1 + """ + } + + if context not in context_queries: + raise HTTPException(status_code=400, detail=f"Invalid context: {context}") + + query = context_queries[context] + + with driver_tools.get_session(database=db_name) as session: + # For overview context, we need to pass the database name as a parameter + params = {'db_name': db_name, 'base_context': base_context} if context == 'overview' else {} + result = session.run(query, params) + record = result.single() + + if not record: + raise HTTPException( + status_code=404, + detail=f"No default node found for context: {context}" + ) + + node = record["n"] + node_data = dict(node) + + # Convert Neo4j types to JSON-serializable types + converted_data = convert_neo4j_values(node_data) + + return { + "status": "success", + "node": { + "id": node["unique_id"], + "path": node["path"], + "type": list(node.labels)[0], + "label": node.get("title", ""), + "data": converted_data + } + } + + except Exception as e: + logger.error(f"Error getting default node: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/routers/database/tools/default_school_contexts_router.py b/routers/database/tools/default_school_contexts_router.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/database/tools/get_events.py b/routers/database/tools/get_events.py new file mode 100644 index 0000000..647defd --- /dev/null +++ b/routers/database/tools/get_events.py @@ -0,0 +1,101 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_calendar_get_events' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver +from fastapi import APIRouter, HTTPException +import colorsys +import random + +# Predefined vibrant color palette +BASE_COLORS = [ + "#FF4136", "#FF851B", "#FFDC00", "#2ECC40", "#0074D9", "#B10DC9", + "#F012BE", "#FF6F61", "#7FDBFF", "#01FF70", "#001f3f", "#85144b", + "#39CCCC", "#3D9970", "#e74c3c", "#e67e22", "#f1c40f", "#2ecc71", + "#1abc9c", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", + "#2980b9", "#8e44ad", "#2c3e50", "#d35400", "#c0392b", "#bdc3c7", + "#7f8c8d", "#00a86b", "#8B4513", "#4B0082", "#800000", "#1E90FF" +] + +def generate_vibrant_color(): + h = random.random() + s = 0.5 + random.random() * 0.5 # 0.5 to 1.0 + v = 0.5 + random.random() * 0.5 # 0.5 to 1.0 + r, g, b = [int(x * 255) for x in colorsys.hsv_to_rgb(h, s, v)] + return f"#{r:02x}{g:02x}{b:02x}" + +# Extend the color palette +EXTENDED_COLOR_PALETTE = BASE_COLORS + [generate_vibrant_color() for _ in range(100)] + +def get_subject_class_color(subject_class): + # Use a hash function to generate a unique number for each subject class + hash_value = hash(subject_class) + + # Use the hash to select a color from the extended palette + color_index = hash_value % len(EXTENDED_COLOR_PALETTE) + color = EXTENDED_COLOR_PALETTE[color_index] + + return color + +router = APIRouter() + +@router.get("/get_teacher_timetable_events") +async def get_teacher_timetable_events( + unique_id: str, + worker_db_name: str +): + logging.info(f"Getting timetable events for teacher {unique_id} from database {worker_db_name}") + neo_driver = driver.get_driver(db_name=worker_db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=worker_db_name) as neo_session: + query = """ + MATCH (t:Teacher {unique_id: $unique_id})-[:TEACHER_HAS_TIMETABLE]->(tt:TeacherTimetable) + -[:TIMETABLE_HAS_CLASS]->(sc:SubjectClass)-[:CLASS_HAS_LESSON]->(tl:TimetableLesson) + RETURN tl.unique_id as id, + tl.period_code as period_code, + COALESCE(sc.subject_class_code, 'Untitled Class') as subject_class, + tl.date as date, + tl.start_time as start_time, + tl.end_time as end_time, + tl.path as path + """ + result = neo_session.run(query, unique_id=unique_id) + + events = [] + for record in result: + start = f"{record['date']}T{record['start_time']}" + end = f"{record['date']}T{record['end_time']}" + title = f"{record['subject_class']}" + events.append({ + "id": record["id"], + "title": title, + "start": start, + "end": end, + "groupId": f"subject-class-{record['subject_class']}", + "extendedProps": { + "subjectClass": record['subject_class'], + "color": get_subject_class_color(record['subject_class']), + "periodCode": record['period_code'], + "path": record['path'] + } + }) + logging.info(f"Found {len(events)} events for teacher {unique_id}") + return {"status": "success", "events": events} + except Exception as e: + logging.error(f"Error fetching events: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) diff --git a/routers/database/tools/get_nodes.py b/routers/database/tools/get_nodes.py new file mode 100644 index 0000000..706ed5a --- /dev/null +++ b/routers/database/tools/get_nodes.py @@ -0,0 +1,563 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_tools_get_nodes' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver +import modules.database.tools.neo4j_session_tools as session +from modules.database.schemas.nodes.calendars import CalendarNode +from modules.database.schemas.nodes.schools.timetable import SchoolTimetableNode, AcademicYearNode, AcademicTermNode, AcademicWeekNode, AcademicDayNode, AcademicPeriodNode, RegistrationPeriodNode +from modules.database.schemas.nodes.users import UserNode +from modules.database.schemas.nodes.workers.workers import TeacherNode, StudentNode, DeveloperNode, SchoolAdminNode +from modules.database.schemas.nodes.schools.schools import SchoolNode, DepartmentNode, SubjectClassNode, RoomNode +from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode, UserTeacherTimetableNode +from fastapi import APIRouter, HTTPException, Query + +router = APIRouter() + +@router.get("/get-node") +async def get_node(unique_id: str = Query(...), db_name: str = Query(...)): + logging.info(f"Getting node for {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n {unique_id: $unique_id}) + RETURN n + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + + if record: + node = record['n'] + node_labels = list(node.labels) + node_data = dict(node) + + try: + # Convert node based on its type + node_type = node_labels[0] if node_labels else "Unknown" + if node_type in globals(): + node_class = globals()[f"{node_type}Node"] + node_object = node_class(**node_data) + node_dict = node_object.to_dict() + else: + node_dict = node_data + + return { + "status": "success", + "node": { + "node_type": node_type, + "node_data": node_dict + } + } + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + return { + "status": "error", + "message": "Error processing node data", + "details": str(e) + } + else: + return {"status": "not_found", "message": "Node not found"} + except Exception as e: + logging.error(f"Error retrieving node: {str(e)}") + return {"status": "error", "message": "Internal server error"} + finally: + driver.close_driver(neo_driver) + +@router.get("/get-user-node") +async def get_user_node(user_id: str = Query(...)): + db_name = f"cc.users.{user_id}" + logging.info(f"Getting user node for user {user_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + nodes = session.find_nodes_by_label_and_properties(neo_session, "User", {"user_id": user_id}) + if nodes: + user_node = nodes[0] + data = UserNode(**user_node) + user_node_data = data.to_dict() + return {"status": "success", "user_node": user_node_data, "user_node_raw": nodes} + else: + return {"status": "not_found", "message": "User node not found"} + except Exception as e: + logging.error(f"Error retrieving user node: {str(e)}") + return {"status": "error", "message": "Internal server error"} + finally: + driver.close_driver(neo_driver) + +@router.get("/get-connected-nodes") +async def get_connected_nodes(unique_id: str = Query(...), db_name: str = Query(...)): + logging.info(f"Getting connected nodes for {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n {unique_id: $unique_id}) + OPTIONAL MATCH (n)-[]-(connected) + RETURN n, collect(connected) as connected_nodes + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + if record: + main_node = record['n'] + connected_nodes = record['connected_nodes'] + + main_node_labels = list(main_node.labels) + main_node_type = main_node_labels[0] if main_node_labels else "Unknown" + main_node_data = dict(main_node) + + try: + main_node_class = globals()[f"{main_node_type}Node"] + main_node_object = main_node_class(**main_node_data) + main_node_dict = main_node_object.to_dict() + except Exception as e: + logging.error(f"Error converting main node to dict: {str(e)}") + main_node_dict = main_node_data + + connected_nodes_list = [] + + for node in connected_nodes: + node_labels = list(node.labels) + node_type = node_labels[0] if node_labels else "Unknown" + node_data = dict(node) + try: + node_class = globals()[f"{node_type}Node"] + node_object = node_class(**node_data) + connected_node_dict = node_object.to_dict() + except Exception as e: + logging.error(f"Error converting connected node to dict: {str(e)}") + connected_node_dict = node_data + + connected_node_info = { + "node_type": node_type, + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + + logging.debug(f"connected_nodes_list: {connected_nodes_list}") + + return { + "status": "success", + "main_node": { + "node_type": main_node_type, + "node_data": main_node_dict + }, + "connected_nodes": connected_nodes_list + } + else: + return {"status": "not_found", "message": "Node not found"} + except Exception as e: + logging.error(f"Error retrieving connected nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-user-connected-nodes") +async def get_user_connected_nodes(unique_id: str = Query(...)): + logging.info(f"Getting user adjacent nodes for node {unique_id}") + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") # TODO: This function needs to be able to take a db_name as a parameter + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + raise HTTPException(status_code=500, detail="Failed to connect to the database") + try: + with neo_driver.session(database=db_name) as neo_session: + user_node_and_connected_nodes = session.get_node_by_unique_id_and_adjacent_nodes(neo_session, unique_id) + user_node = user_node_and_connected_nodes['node'] + connected_nodes = user_node_and_connected_nodes['connected_nodes'] + try: + data = UserNode(**user_node) + user_node_dict = data.to_dict() + except Exception as e: + logging.error(f"Error converting user node to dict: {str(e)}") + connected_nodes_list = [] + for connected_node in connected_nodes: + node_data = connected_node['node'] + node_labels = list(node_data.labels) + logging.debug(f"node_labels: {node_labels}") + for label in node_labels: + logging.debug(f"label: {label}") + try: + if 'Developer' == label: + logging.debug(f"Developer node found") + node_object = DeveloperNode(**node_data) + elif 'SchoolAdmin' == label: + logging.debug(f"SchoolAdmin node found") + node_object = SchoolAdminNode(**node_data) + elif 'Teacher' == label: + logging.debug(f"Teacher node found") + node_object = TeacherNode(**node_data) + elif 'Student' == label: + logging.debug(f"Student node found") + node_object = StudentNode(**node_data) + elif 'Calendar' == label: + logging.debug(f"Calendar node found") + node_object = CalendarNode(**node_data) + elif 'TeacherTimetable' == label: + logging.debug(f"TeacherTimetable node found") + node_object = TeacherTimetableNode(**node_data) + elif 'UserTeacherTimetable' == label: + logging.debug(f"UserTeacherTimetable node found") + node_object = UserTeacherTimetableNode(**node_data) + elif 'School' == label: + logging.debug(f"School node found") + node_object = SchoolNode(**node_data) + elif 'Department' == label: + logging.debug(f"Department node found") + node_object = DepartmentNode(**node_data) + elif 'Student' == label: + logging.debug(f"Student node found") + node_object = StudentNode(**node_data) + elif 'Class' == label: + logging.debug(f"Class node found") + node_object = SubjectClassNode(**node_data) + elif 'Room' == label: + logging.debug(f"Room node found") + node_object = RoomNode(**node_data) + else: + logging.error(f"Unknown node label: {node_labels}") + continue + connected_node_dict = node_object.to_dict() + logging.debug(f"connected_node_dict: {connected_node_dict}") + connected_node_info = { + "node_type": label, + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + return {"status": "success", "user_node": user_node_dict, "user_connected_nodes": connected_nodes_list} + except Exception as e: + logging.error(f"Error retrieving adjacent nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-worker-connected-nodes") +async def get_worker_connected_nodes(unique_id: str = Query(...)): + logging.info(f"Getting worker adjacent nodes for node {unique_id}") + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") # TODO: This function needs to be able to take a db_name as a parameter + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + raise HTTPException(status_code=500, detail="Failed to connect to the database") + try: + with neo_driver.session(database=db_name) as neo_session: + node_and_connected_nodes = session.get_node_by_unique_id_and_adjacent_nodes(neo_session, unique_id) + worker_node = node_and_connected_nodes['node'] + connected_nodes = node_and_connected_nodes['connected_nodes'] + try: + data = TeacherNode(**worker_node) + worker_node_dict = data.to_dict() + except Exception as e: + logging.error(f"Error converting user node to dict: {str(e)}") + connected_nodes_list = [] + for connected_node in connected_nodes: + node_data = connected_node['node'] + node_labels = list(node_data.labels) + logging.debug(f"node_labels: {node_labels}") + for label in node_labels: + logging.debug(f"label: {label}") + try: + if 'Calendar' == label: + logging.debug(f"Calendar node found") + node_object = CalendarNode(**node_data) + elif 'TeacherTimetable' == label: + logging.debug(f"TeacherTimetable node found") + node_object = TeacherTimetableNode(**node_data) + elif 'UserTeacherTimetable' == label: + logging.debug(f"UserTeacherTimetable node found") + node_object = UserTeacherTimetableNode(**node_data) + elif 'School' == label: + logging.debug(f"School node found") + node_object = SchoolNode(**node_data) + elif 'Department' == label: + logging.debug(f"Department node found") + node_object = DepartmentNode(**node_data) + elif 'Student' == label: + logging.debug(f"Student node found") + node_object = StudentNode(**node_data) + elif 'Class' == label: + logging.debug(f"Class node found") + node_object = SubjectClassNode(**node_data) + elif 'Room' == label: + logging.debug(f"Room node found") + node_object = RoomNode(**node_data) + else: + logging.error(f"Unknown node label: {node_labels}") + continue + connected_node_dict = node_object.to_dict() + logging.debug(f"connected_node_dict: {connected_node_dict}") + connected_node_info = { + "node_type": label, + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + return {"status": "success", "user_node": worker_node_dict, "worker_connected_nodes": connected_nodes_list} + except Exception as e: + logging.error(f"Error retrieving worker adjacent nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-calendar-connected-nodes") +async def get_calendar_connected_nodes(unique_id: str = Query(...)): + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") + logging.info(f"Getting connected nodes for calendar {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n) + WHERE n.unique_id = $unique_id AND (n:Calendar OR n:CalendarYear OR n:CalendarMonth OR n:CalendarWeek OR n:CalendarDay OR n:CalendarTimeChunk) + OPTIONAL MATCH (n)-[]-(connected) + RETURN n, collect(connected) as connected_nodes + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + if record: + calendar_node = record['n'] + connected_nodes = record['connected_nodes'] + + node_type = list(calendar_node.labels)[0] + calendar_dict = globals()[f"{node_type}Node"](**calendar_node).to_dict() + connected_nodes_list = [] + + for node in connected_nodes: + node_labels = list(node.labels) + node_data = dict(node) + try: + node_class = globals()[f"{node_labels[0]}Node"] + node_object = node_class(**node_data) + connected_node_dict = node_object.to_dict() + connected_node_info = { + "node_type": node_labels[0], + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + + return {"status": "success", "calendar_node": calendar_dict, "connected_nodes": connected_nodes_list} + else: + return {"status": "not_found", "message": "Calendar node not found"} + except Exception as e: + logging.error(f"Error retrieving connected nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-teacher-timetable-connected-nodes") +async def get_teacher_timetable_connected_nodes(unique_id: str = Query(...)): + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") + logging.info(f"Getting connected nodes for teacher timetable {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n:TeacherTimetable {unique_id: $unique_id}) + OPTIONAL MATCH (n)-[]-(connected) + RETURN n, collect(connected) as connected_nodes + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + if record: + teacher_timetable_node = record['n'] + connected_nodes = record['connected_nodes'] + + teacher_timetable_dict = TeacherTimetableNode(**teacher_timetable_node).to_dict() + connected_nodes_list = [] + + for node in connected_nodes: + node_labels = list(node.labels) + node_data = dict(node) + try: + if 'TimetableLesson' in node_labels: + node_object = TimetableLessonNode(**node_data) + elif 'PlannedLesson' in node_labels: + node_object = PlannedLessonNode(**node_data) + else: + logging.error(f"Unknown node label: {node_labels}") + continue + connected_node_dict = node_object.to_dict() + connected_node_info = { + "node_type": node_labels[0], + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + + return {"status": "success", "teacher_timetable_node": teacher_timetable_dict, "connected_nodes": connected_nodes_list} + else: + return {"status": "not_found", "message": "Teacher timetable node not found"} + except Exception as e: + logging.error(f"Error retrieving connected nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-school-timetable-connected-nodes") +async def get_school_timetable_connected_nodes(unique_id: str = Query(...)): + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") + logging.info(f"Getting connected nodes for school timetable {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n:SchoolTimetable {unique_id: $unique_id}) + OPTIONAL MATCH (n)-[]-(connected) + RETURN n, collect(connected) as connected_nodes + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + if record: + school_timetable_node = record['n'] + connected_nodes = record['connected_nodes'] + + school_timetable_dict = SchoolTimetableNode(**school_timetable_node).to_dict() + connected_nodes_list = [] + + for node in connected_nodes: + node_labels = list(node.labels) + node_data = dict(node) + try: + if 'AcademicYear' in node_labels: + node_object = AcademicYearNode(**node_data) + elif 'AcademicTerm' in node_labels: + node_object = AcademicTermNode(**node_data) + elif 'AcademicWeek' in node_labels: + node_object = AcademicWeekNode(**node_data) + elif 'AcademicDay' in node_labels: + node_object = AcademicDayNode(**node_data) + elif 'AcademicPeriod' in node_labels: + node_object = AcademicPeriodNode(**node_data) + elif 'RegistrationPeriod' in node_labels: + node_object = RegistrationPeriodNode(**node_data) + else: + logging.error(f"Unknown node label: {node_labels}") + continue + connected_node_dict = node_object.to_dict() + connected_node_info = { + "node_type": node_labels[0], + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + + return {"status": "success", "school_timetable_node": school_timetable_dict, "connected_nodes": connected_nodes_list} + else: + return {"status": "not_found", "message": "School timetable node not found"} + except Exception as e: + logging.error(f"Error retrieving connected nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-curriculum-connected-nodes") +async def get_curriculum_connected_nodes(unique_id: str = Query(...)): + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") + logging.info(f"Getting connected nodes for curriculum {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n) + WHERE n.unique_id = $unique_id AND (n:PastoralStructure OR n:YearGroup OR n:CurriculumStructure OR n:KeyStage OR n:KeyStageSyllabus OR n:YearGroupSyllabus OR n:Subject OR n:Topic OR n:TopicLesson OR n:LearningStatement OR n:ScienceLab) + OPTIONAL MATCH (n)-[]-(connected) + RETURN n, collect(connected) as connected_nodes + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + if record: + curriculum_node = record['n'] + connected_nodes = record['connected_nodes'] + + node_type = list(curriculum_node.labels)[0] + curriculum_dict = globals()[f"{node_type}Node"](**curriculum_node).to_dict() + connected_nodes_list = [] + + for node in connected_nodes: + node_labels = list(node.labels) + node_data = dict(node) + try: + node_class = globals()[f"{node_labels[0]}Node"] + node_object = node_class(**node_data) + connected_node_dict = node_object.to_dict() + connected_node_info = { + "node_type": node_labels[0], + "node_data": connected_node_dict + } + connected_nodes_list.append(connected_node_info) + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + + return {"status": "success", "curriculum_node": curriculum_dict, "connected_nodes": connected_nodes_list} + else: + return {"status": "not_found", "message": "Curriculum node not found"} + except Exception as e: + logging.error(f"Error retrieving connected nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) + +@router.get("/get-school-node") +async def get_school_node(school_uuid: str = Query(...)): + logging.info(f"Getting school node for school {school_uuid}...") + db_name = f"cc.institutes.{school_uuid}" + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + nodes = session.find_nodes_by_label_and_properties(neo_session, "School", {"school_uuid": school_uuid}) + if nodes: + school_node = nodes[0] + data = SchoolNode( + unique_id=school_node["unique_id"], + school_uuid=school_node["school_uuid"], + school_name=school_node["school_name"], + school_website=school_node["school_website"], + path=school_node["path"] + ) + school_node_data = data.to_dict() + return {"status": "success", "school_node": school_node_data, "school_node_raw": nodes} + else: + return {"status": "not_found", "message": "School node not found"} + except Exception as e: + logging.error(f"Error retrieving school node: {str(e)}") + return {"status": "error", "message": "Internal server error"} + finally: + driver.close_driver(neo_driver) \ No newline at end of file diff --git a/routers/database/tools/get_nodes_and_edges.py b/routers/database/tools/get_nodes_and_edges.py new file mode 100644 index 0000000..e157ff2 --- /dev/null +++ b/routers/database/tools/get_nodes_and_edges.py @@ -0,0 +1,174 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_tools_get_nodes' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver +import modules.database.tools.neo4j_session_tools as session +from modules.database.schemas.nodes.calendars import CalendarNode, CalendarYearNode, CalendarMonthNode, CalendarWeekNode, CalendarDayNode, CalendarTimeChunkNode +from modules.database.schemas.nodes.users import UserNode +from modules.database.schemas.nodes.workers.workers import TeacherNode, StudentNode, DeveloperNode, SchoolAdminNode +from modules.database.schemas.nodes.structures.schools import PastoralStructureNode, CurriculumStructureNode +from modules.database.schemas.nodes.schools.pastoral import YearGroupNode, YearGroupSyllabusNode +from modules.database.schemas.nodes.schools.curriculum import SubjectNode, TopicNode, TopicLessonNode, LearningStatementNode, ScienceLabNode +from modules.database.schemas.nodes.schools.timetable import SchoolTimetableNode, AcademicYearNode, AcademicTermNode, AcademicWeekNode, AcademicDayNode, OffTimetableDayNode, StaffDayNode, AcademicPeriodNode, RegistrationPeriodNode, OffTimetablePeriodNode, AcademicTermBreakNode, BreakPeriodNode, HolidayDayNode, HolidayWeekNode +from modules.database.schemas.nodes.workers.timetable import TeacherTimetableNode, TimetableLessonNode, PlannedLessonNode, UserTeacherTimetableNode, StudentTimetableNode, SchoolAdminTimetableNode, DeveloperTimetableNode, SuperAdminTimetableNode +from modules.database.schemas.nodes.schools.schools import SchoolNode, DepartmentNode, SubjectClassNode, RoomNode +from fastapi import APIRouter, HTTPException, Query + +router = APIRouter() + +@router.get("/get-all-nodes-and-edges") +async def get_all_nodes_and_edges(): + db_name = os.getenv("NEO4J_DB_NAME", "cc.institutes.kevlarai") + logging.info(f"Getting all nodes and edges from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n)-[r]->(m) + RETURN n, r, m + """ + result = neo_session.run(query) + nodes = {} + relationships = [] + + for record in result: + source = record['n'] + target = record['m'] + relationship = record['r'] + + for node in [source, target]: + if node.id not in nodes: + node_labels = list(node.labels) + node_type = node_labels[0] if node_labels else "Unknown" + node_data = dict(node) + try: + node_class = globals()[f"{node_type}Node"] + node_object = node_class(**node_data) + node_dict = node_object.to_dict() + except Exception as e: + logging.error(f"Error converting node to dict: {str(e)}") + node_dict = node_data + + nodes[node.id] = { + "node_type": node_type, + "node_data": node_dict + } + + relationship_info = { + "start_node": source.id, + "end_node": target.id, + "relationship_type": relationship.type, + "relationship_properties": dict(relationship) + } + relationships.append(relationship_info) + + return { + "status": "success", + "nodes": list(nodes.values()), + "relationships": relationships + } + except Exception as e: + logging.error(f"Error retrieving all nodes and edges: {str(e)}") + return {"status": "error", "message": "Internal server error"} + finally: + driver.close_driver(neo_driver) + + +@router.get("/get-connected-nodes-and-edges") +async def get_connected_nodes_and_edges(unique_id: str = Query(...), db_name: str = Query(...)): + logging.info(f"Getting connected nodes and edges for {unique_id} from database {db_name}") + neo_driver = driver.get_driver(db_name=db_name) + if neo_driver is None: + return {"status": "error", "message": "Failed to connect to the database"} + + try: + with neo_driver.session(database=db_name) as neo_session: + query = """ + MATCH (n {unique_id: $unique_id}) + OPTIONAL MATCH (n)-[r]-(connected) + RETURN n, collect(connected) as connected_nodes, collect(r) as relationships + """ + result = neo_session.run(query, unique_id=unique_id) + record = result.single() + if record: + main_node = record['n'] + connected_nodes = record['connected_nodes'] + relationships = record['relationships'] + + main_node_labels = list(main_node.labels) + main_node_type = main_node_labels[0] if main_node_labels else "Unknown" + main_node_data = dict(main_node) + + try: + main_node_class = globals()[f"{main_node_type}Node"] + main_node_object = main_node_class(**main_node_data) + main_node_dict = main_node_object.to_dict() + except Exception as e: + logging.error(f"Error converting main node to dict: {str(e)}") + main_node_dict = main_node_data + + connected_nodes_list = [] + relationship_list = [] + + for node, relationship in zip(connected_nodes, relationships): + node_labels = list(node.labels) + node_type = node_labels[0] if node_labels else "Unknown" + node_data = dict(node) + try: + node_class = globals()[f"{node_type}Node"] + node_object = node_class(**node_data) + connected_node_dict = node_object.to_dict() + except Exception as e: + logging.error(f"Error converting connected node to dict: {str(e)}") + connected_node_dict = node_data + + connected_node_info = { + "node_type": node_type, + "node_data": connected_node_dict, + "relationship_type": relationship.type, # Get relationship type + "relationship_properties": dict(relationship) # Relationship properties, if any + } + connected_nodes_list.append(connected_node_info) + + relationship_info = { + "start_node": dict(relationship.start_node), + "end_node": dict(relationship.end_node), + "relationship_type": relationship.type, + "relationship_properties": dict(relationship) + } + relationship_list.append(relationship_info) + + logging.info(f"Main node: {main_node_dict}") + logging.info(f"Connected nodes: {connected_nodes_list}") + logging.info(f"Relationships: {relationship_list}") + + return { + "status": "success", + "main_node": { + "node_type": main_node_type, + "node_data": main_node_dict + }, + "connected_nodes": connected_nodes_list, + "relationships": relationship_list + } + else: + return {"status": "not_found", "message": "Node not found"} + except Exception as e: + logging.error(f"Error retrieving connected nodes: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + finally: + driver.close_driver(neo_driver) \ No newline at end of file diff --git a/routers/database/tools/get_school_curriculum_context.py b/routers/database/tools/get_school_curriculum_context.py new file mode 100644 index 0000000..24f11d4 --- /dev/null +++ b/routers/database/tools/get_school_curriculum_context.py @@ -0,0 +1,3 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) diff --git a/routers/database/tools/navigation/__init__.py b/routers/database/tools/navigation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/database/tools/tldraw_filesystem.py b/routers/database/tools/tldraw_filesystem.py new file mode 100644 index 0000000..20cac21 --- /dev/null +++ b/routers/database/tools/tldraw_filesystem.py @@ -0,0 +1,196 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_database_tools_tldraw_filesystem' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from fastapi import APIRouter, HTTPException, Query +from typing import Dict +import json + +from modules.database.tools.filesystem_tools import ClassroomCopilotFilesystem +from modules.database.schemas.nodes.users import UserNode +from modules.database.tools.neo4j_db_formatter import format_user_email_for_neo_db + +router = APIRouter() + +@router.post("/get_tldraw_user_node_file") +async def read_tldraw_user_node_file(user_node: UserNode): + logging.debug(f"Reading tldraw file for user node: {user_node.user_email}") + + # Format the database name using the email + formatted_email = format_user_email_for_neo_db(user_node.user_email) + db_name = f"cc.users.{formatted_email}" + + fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user") + + logging.debug(f"Filesystem root path: {fs.root_path}") + + # Handle path based on environment + if os.getenv("DEV_MODE") == "true": + # In dev mode, use the full system path from the node + if not user_node.path: + raise HTTPException(status_code=400, detail="Node path not found") + logging.debug(f"Using DEV_MODE path: {user_node.path}") + base_path = os.path.normpath(user_node.path) + else: + # In prod mode, construct path using formatted email + logging.warning(f"Using db_name as base path not ready in prod: {db_name}") + base_path = formatted_email + + # Construct final path including tldraw file + logging.debug(f"Base path: {base_path}") + file_path = os.path.join(base_path, "tldraw_file.json") + logging.debug(f"File path: {file_path}") + file_location = os.path.normpath(os.path.join(fs.root_path, file_path)) + logging.debug(f"File location: {file_location}") + + logging.debug(f"Attempting to read file at: {file_location}") + + if os.path.exists(file_location): + logging.debug(f"File exists: {file_location}") + try: + with open(file_location, "r") as file: + data = json.load(file) + return data + except json.JSONDecodeError as e: + logging.error(f"Failed to parse JSON from file: {e}") + raise HTTPException(status_code=500, detail="Invalid JSON in file") + except Exception as e: + logging.error(f"Error reading file: {e}") + raise HTTPException(status_code=500, detail="Error reading file") + else: + logging.debug(f"File does not exist: {file_location}") + raise HTTPException(status_code=404, detail="File not found") + +@router.post("/set_tldraw_user_node_file") +async def set_tldraw_user_node_file(user_node: UserNode, data: Dict): + logging.debug(f"Setting tldraw file for user node: {user_node.user_email}") + + # Format the database name using the email + formatted_email = format_user_email_for_neo_db(user_node.user_email) + db_name = f"cc.users.{formatted_email}" + + fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user") + + # Handle path based on environment + if os.getenv("ENVIRONMENT") == "dev": + # In dev mode, use the full system path from the node + if not user_node.path: + raise HTTPException(status_code=400, detail="Node path not found") + base_path = os.path.normpath(user_node.path) + else: + # In prod mode, construct path using formatted email + base_path = formatted_email + + # Construct final path including tldraw file + file_path = os.path.join(base_path, "tldraw_file.json") + file_location = os.path.normpath(os.path.join(fs.root_path, file_path)) + + logging.debug(f"Attempting to write file at: {file_location}") + + try: + # Ensure directory exists + os.makedirs(os.path.dirname(file_location), exist_ok=True) + + # Write the file + with open(file_location, "w") as file: + json.dump(data, file) + return {"status": "success"} + except Exception as e: + logging.error(f"Error writing file: {e}") + raise HTTPException(status_code=500, detail="Error writing file") + +@router.get("/get_tldraw_node_file") +async def read_tldraw_node_file(path: str, db_name: str): + logging.debug(f"Reading tldraw file for path: {path}") + + fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user") + + logging.debug(f"Filesystem root path: {fs.root_path}") + + # Handle path based on environment + if os.getenv("DEV_MODE") == "true": + # In dev mode, use the full system path from the node + if not path: + raise HTTPException(status_code=400, detail="Path not provided") + logging.debug(f"Using DEV_MODEpath: {path}") + base_path = os.path.normpath(path) + else: + # In prod mode, construct path + logging.warning(f"Using db_name as base path not ready in prod: {db_name}") + base_path = db_name + + # Construct final path including tldraw file + logging.debug(f"Base path: {base_path}") + file_path = os.path.join(base_path, "tldraw_file.json") + logging.debug(f"File path: {file_path}") + file_location = os.path.normpath(os.path.join(fs.root_path, file_path)) + logging.debug(f"File location: {file_location}") + + logging.debug(f"Attempting to read file at: {file_location}") + + if os.path.exists(file_location): + logging.debug(f"File exists: {file_location}") + try: + with open(file_location, "r") as file: + data = json.load(file) + return data + except json.JSONDecodeError as e: + logging.error(f"Failed to parse JSON from file: {e}") + raise HTTPException(status_code=500, detail="Invalid JSON in file") + except Exception as e: + logging.error(f"Error reading file: {e}") + raise HTTPException(status_code=500, detail="Error reading file") + else: + logging.debug(f"File does not exist: {file_location}") + raise HTTPException(status_code=404, detail="File not found") + +@router.post("/set_tldraw_node_file") +async def set_tldraw_node_file(path: str, db_name: str, data: Dict): + logging.debug(f"Setting tldraw file for path: {path}") + + fs = ClassroomCopilotFilesystem(db_name=db_name, init_run_type="user") + + logging.debug(f"Filesystem root path: {fs.root_path}") + + # Handle path based on environment + if os.getenv("DEV_MODE") == "true": + # In dev mode, use the full system path from the node + if not path: + raise HTTPException(status_code=400, detail="Path not provided") + logging.debug(f"Using DEV_MODEpath: {path}") + base_path = os.path.normpath(path) + else: + # In prod mode, construct path + logging.warning(f"Using db_name as base path not ready in prod: {db_name}") + base_path = db_name + + # Construct final path including tldraw file + logging.debug(f"Base path: {base_path}") + file_path = os.path.join(base_path, "tldraw_file.json") + logging.debug(f"File path: {file_path}") + file_location = os.path.normpath(os.path.join(fs.root_path, file_path)) + logging.debug(f"File location: {file_location}") + + logging.debug(f"Attempting to set file at: {file_location}") + + try: + # Ensure directory exists + os.makedirs(os.path.dirname(file_location), exist_ok=True) + + # Write the file + with open(file_location, "w") as file: + json.dump(data, file) + return {"status": "success"} + except Exception as e: + logging.error(f"Error writing file: {e}") + raise HTTPException(status_code=500, detail="Error writing file") diff --git a/routers/database/tools/worker_structure_router.py b/routers/database/tools/worker_structure_router.py new file mode 100644 index 0000000..31667e5 --- /dev/null +++ b/routers/database/tools/worker_structure_router.py @@ -0,0 +1,190 @@ +import os +from fastapi import APIRouter, HTTPException +from typing import List, Dict, Any, Optional +from datetime import datetime, timedelta +from modules.logger_tool import initialise_logger +from modules.database.tools import neo4j_driver_tools as driver_tools + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +router = APIRouter() + +@router.get("/get-worker-structure") +async def get_worker_structure(db_name: str) -> Dict[str, Any]: + """ + Get the complete worker structure including timetables, classes, lessons, journals, and planners. + """ + try: + # Get all worker-related nodes in a single query + query = """ + // Match all worker-related nodes + MATCH (t:Teacher) + OPTIONAL MATCH (t)-[:TEACHER_HAS_TIMETABLE]->(tt:UserTeacherTimetable) + OPTIONAL MATCH (t)-[:TEACHER_HAS_CLASS]->(c:Class) + OPTIONAL MATCH (t)-[:TEACHER_HAS_LESSON]->(l:TimetableLesson) + OPTIONAL MATCH (t)-[:TEACHER_HAS_JOURNAL]->(j:Journal) + OPTIONAL MATCH (t)-[:TEACHER_HAS_PLANNER]->(p:Planner) + WITH t, tt, c, l, j, p + ORDER BY tt.start_date, c.created, l.created, j.created, p.created + + // Collect all nodes + RETURN { + timetables: collect(DISTINCT { + id: tt.unique_id, + path: tt.path, + title: tt.title, + type: tt.__primarylabel__, + startTime: toString(tt.start_date), + endTime: toString(tt.end_date) + }), + classes: collect(DISTINCT { + id: c.unique_id, + path: c.path, + title: c.title, + type: c.__primarylabel__ + }), + lessons: collect(DISTINCT { + id: l.unique_id, + path: l.path, + title: l.title, + type: l.__primarylabel__ + }), + journals: collect(DISTINCT { + id: j.unique_id, + path: j.path, + title: j.title, + type: j.__primarylabel__ + }), + planners: collect(DISTINCT { + id: p.unique_id, + path: p.path, + title: p.title, + type: p.__primarylabel__ + }) + } as structure + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + record = result.single() + if not record: + raise HTTPException(status_code=404, detail="Worker structure not found") + + structure = record["structure"] + + return { + "status": "success", + "data": { + "timetables": { + "default": structure["timetables"] + }, + "classes": { + "default": structure["classes"] + }, + "lessons": { + "default": structure["lessons"] + }, + "journals": { + "default": structure["journals"] + }, + "planners": { + "default": structure["planners"] + } + } + } + + except Exception as e: + logger.error(f"Error getting worker structure: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-timetables") +async def get_timetables(db_name: str, start_date: str, end_date: str) -> Dict[str, Any]: + """ + Get all timetables in a date range. + """ + try: + query = """ + MATCH (tt:UserTeacherTimetable) + WHERE date(tt.start_date) >= date($start_date) AND date(tt.end_date) <= date($end_date) + RETURN { + id: tt.unique_id, + path: tt.path, + title: tt.title, + type: tt.__primarylabel__, + startTime: toString(tt.start_date), + endTime: toString(tt.end_date) + } as timetable + ORDER BY tt.start_date + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query, start_date=start_date, end_date=end_date) + timetables = [record["timetable"] for record in result] + + return { + "status": "success", + "timetables": timetables + } + + except Exception as e: + logger.error(f"Error getting timetables: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-journals") +async def get_journals(db_name: str) -> Dict[str, Any]: + """ + Get all journals. + """ + try: + query = """ + MATCH (j:Journal) + RETURN { + id: j.unique_id, + path: j.path, + title: j.title, + type: j.__primarylabel__ + } as journal + ORDER BY j.created + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + journals = [record["journal"] for record in result] + + return { + "status": "success", + "journals": journals + } + + except Exception as e: + logger.error(f"Error getting journals: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/get-planners") +async def get_planners(db_name: str) -> Dict[str, Any]: + """ + Get all planners. + """ + try: + query = """ + MATCH (p:Planner) + RETURN { + id: p.unique_id, + path: p.path, + title: p.title, + type: p.__primarylabel__ + } as planner + ORDER BY p.created + """ + + with driver_tools.get_session(database=db_name) as session: + result = session.run(query) + planners = [record["planner"] for record in result] + + return { + "status": "success", + "planners": planners + } + + except Exception as e: + logger.error(f"Error getting planners: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/dev/__init__.py b/routers/dev/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/dev/__pycache__/__init__.cpython-311.pyc b/routers/dev/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0a32b43 Binary files /dev/null and b/routers/dev/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/dev/__pycache__/document_conversion.cpython-311.pyc b/routers/dev/__pycache__/document_conversion.cpython-311.pyc new file mode 100644 index 0000000..21c9e8e Binary files /dev/null and b/routers/dev/__pycache__/document_conversion.cpython-311.pyc differ diff --git a/routers/dev/__pycache__/test_analysis.cpython-311.pyc b/routers/dev/__pycache__/test_analysis.cpython-311.pyc new file mode 100644 index 0000000..47d0cf8 Binary files /dev/null and b/routers/dev/__pycache__/test_analysis.cpython-311.pyc differ diff --git a/routers/dev/document_conversion.py b/routers/dev/document_conversion.py new file mode 100644 index 0000000..b0ec248 --- /dev/null +++ b/routers/dev/document_conversion.py @@ -0,0 +1,139 @@ +from fastapi import APIRouter, UploadFile, File, HTTPException +from typing import List, Optional, Dict +from pathlib import Path +import shutil +import tempfile +from pydantic import BaseModel +from modules.document_processor import DocumentProcessor +import os + +class BatchConvertRequest(BaseModel): + directory: str + output_dir: Optional[str] = None + +router = APIRouter() +doc_processor = DocumentProcessor() + +@router.post("/convert-to-pdf") +async def convert_to_pdf( + files: List[UploadFile] = File(...), + output_format: str = "pdf" +): + """ + Convert uploaded documents to PDF format + """ + results = [] + with tempfile.TemporaryDirectory() as temp_dir: + for file in files: + # Save uploaded file to temp directory + temp_file = Path(temp_dir) / file.filename + with temp_file.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + try: + # Process the document + pdf_content = doc_processor.convert_to_pdf(temp_file) + results.append({ + "filename": file.filename, + "converted_content": pdf_content, + "status": "success" + }) + except Exception as e: + results.append({ + "filename": file.filename, + "error": str(e), + "status": "error" + }) + + return results + +@router.post("/batch-convert") +async def batch_convert( + directory: str, + output_format: str = "pdf" +): + """ + Convert all documents in a directory to PDF format + """ + try: + results = doc_processor.batch_convert_directory(directory) + return results + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/batch-convert-recursive") +async def batch_convert_recursive(request_data: BatchConvertRequest): + """ + Convert all documents in a directory and its subdirectories to PDF + """ + try: + directory_path = Path(request_data.directory) + if not directory_path.exists(): + raise HTTPException(status_code=404, detail=f"Directory not found: {request_data.directory}") + + output_path = None + if request_data.output_dir: + output_path = Path(request_data.output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + results = [] + supported_extensions = doc_processor.supported_extensions.keys() + + # Debug: Print processing info + print(f"Processing directory: {directory_path}") + print(f"Output directory: {output_path}") + print(f"Supported extensions: {list(supported_extensions)}") + + # Count files before processing + all_files = [] + for ext in supported_extensions: + all_files.extend(list(directory_path.rglob(f"*.{ext}"))) + print(f"Found {len(all_files)} files to process") + + # Recursively find all documents + for file_path in all_files: + try: + print(f"Processing: {file_path}") + # Convert the document + pdf_content = doc_processor.convert_to_pdf(file_path) + + # Determine output path + if output_path: + # Preserve directory structure in output_dir + rel_path = file_path.relative_to(directory_path) + out_path = output_path / rel_path.with_suffix('.pdf') + out_path.parent.mkdir(parents=True, exist_ok=True) + else: + out_path = file_path.with_suffix('.pdf') + + # Save the PDF + with open(out_path, 'wb') as f: + f.write(pdf_content) + + results.append({ + "source_file": str(file_path), + "output_file": str(out_path), + "status": "success" + }) + print(f"Successfully converted: {file_path} -> {out_path}") + + except Exception as e: + print(f"Error converting {file_path}: {str(e)}") + results.append({ + "source_file": str(file_path), + "status": "error", + "error": str(e) + }) + + response_data = { + "total_files": len(results), + "successful": sum(1 for r in results if r["status"] == "success"), + "failed": sum(1 for r in results if r["status"] == "error"), + "results": results + } + print(f"Conversion complete: {response_data['successful']} successful, {response_data['failed']} failed") + return response_data + + except Exception as e: + print(f"Error in batch conversion: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/routers/dev/pdf_utils.py b/routers/dev/pdf_utils.py new file mode 100644 index 0000000..f2272be --- /dev/null +++ b/routers/dev/pdf_utils.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, UploadFile, File, HTTPException +from typing import Dict +from pathlib import Path +import shutil +import tempfile +from modules.pdf_utils import PDFUtils + +router = APIRouter() + +@router.post("/extract-text") +async def extract_text( + pdf_file: UploadFile = File(...) +): + """ + Extract text content from a PDF file + """ + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_file = Path(temp_dir) / pdf_file.filename + with temp_file.open("wb") as buffer: + shutil.copyfileobj(pdf_file.file, buffer) + + text = PDFUtils.extract_text_from_pdf(temp_file) + return {"text": text} + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/metadata") +async def get_metadata( + pdf_file: UploadFile = File(...) +): + """ + Get metadata from a PDF file + """ + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_file = Path(temp_dir) / pdf_file.filename + with temp_file.open("wb") as buffer: + shutil.copyfileobj(pdf_file.file, buffer) + + metadata = PDFUtils.get_pdf_metadata(temp_file) + return metadata + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/routers/dev/test_analysis.py b/routers/dev/test_analysis.py new file mode 100644 index 0000000..e08e164 --- /dev/null +++ b/routers/dev/test_analysis.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, UploadFile, File, HTTPException, Form +from typing import Dict +import json +from pathlib import Path +import shutil +import tempfile +from modules.test_analyzer import TestAnalyzer, TestAnalysis +from modules.pdf_utils import PDFUtils + +router = APIRouter() + +@router.post("/analyze", response_model=TestAnalysis) +async def analyze_test( + test_file: UploadFile = File(...), + marks_data: str = Form(...), + api_key: str = Form(...), + mode: str = Form('detailed') +): + """ + Analyze a test PDF and generate feedback based on marks data + """ + try: + print(f"Received request - Mode: {mode}") + marks_data_dict = json.loads(marks_data) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_file = Path(temp_dir) / test_file.filename + with temp_file.open("wb") as buffer: + shutil.copyfileobj(test_file.file, buffer) + + print("File saved, initializing analyzer...") + analyzer = TestAnalyzer(api_key=api_key) + + print("Extracting PDF content...") + pdf_utils = PDFUtils() + pdf_content = pdf_utils.extract_text_from_pdf(temp_file) + + print("Analyzing content...") + analysis = analyzer.analyze_test(pdf_content, marks_data_dict, mode) + + print("Analysis complete") + return analysis + + except json.JSONDecodeError as e: + print(f"JSON decode error: {str(e)}") + raise HTTPException(status_code=422, detail=f"Invalid marks_data JSON format: {str(e)}") + except Exception as e: + print(f"Error in analyze_test: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/routers/dev/tests/__init__.py b/routers/dev/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/dev/tests/__pycache__/__init__.cpython-311.pyc b/routers/dev/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..aa61319 Binary files /dev/null and b/routers/dev/tests/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/dev/tests/__pycache__/timetable_test.cpython-311.pyc b/routers/dev/tests/__pycache__/timetable_test.cpython-311.pyc new file mode 100644 index 0000000..f22a6e2 Binary files /dev/null and b/routers/dev/tests/__pycache__/timetable_test.cpython-311.pyc differ diff --git a/routers/dev/tests/timetable_test.py b/routers/dev/tests/timetable_test.py new file mode 100644 index 0000000..0b63847 --- /dev/null +++ b/routers/dev/tests/timetable_test.py @@ -0,0 +1,37 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +from fastapi import APIRouter + +router = APIRouter() + +@router.post("/run-pytest-timetable") +async def run_pytest_timetable(): + import subprocess + + home_dir = os.environ['HOME_DIR'] + backend_test_dir = os.environ['BACKEND_TEST_DIR'] + logger.debug(f"original home_dir: {home_dir}") + logger.debug(f"original backend_test_dir: {backend_test_dir}") + + if backend_test_dir[0] != '/': + backend_test_dir = '/' + backend_test_dir + + # Convert backslashes to forward slashes for Windows compatibility + home_dir = home_dir.replace('\\', '/') + backend_test_dir = backend_test_dir.replace('\\', '/') + logger.debug(f"new home_dir: {home_dir}") + logger.debug(f"new backend_test_dir: {backend_test_dir}") + + # Join and normalize the path + pytest_dir = os.path.normpath(os.path.join(home_dir, backend_test_dir.lstrip('/'), "pytest_timetable.py")) + pytest_dir = pytest_dir.replace('\\', '/') # Ensure forward slashes + f_string = f"pytest {pytest_dir} --maxfail=1 --disable-warnings -q" + logger.debug(f"f_string: {f_string}") + + result = subprocess.run(f_string, capture_output=True, text=True, shell=True) + logger.debug(f"result: {result}") + + return {"stdout": result.stdout, "stderr": result.stderr} \ No newline at end of file diff --git a/routers/external/__init__.py b/routers/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/external/__pycache__/__init__.cpython-311.pyc b/routers/external/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7df57b9 Binary files /dev/null and b/routers/external/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/external/__pycache__/youtube.cpython-311.pyc b/routers/external/__pycache__/youtube.cpython-311.pyc new file mode 100644 index 0000000..fdee903 Binary files /dev/null and b/routers/external/__pycache__/youtube.cpython-311.pyc differ diff --git a/routers/external/youtube.py b/routers/external/youtube.py new file mode 100644 index 0000000..8411b6b --- /dev/null +++ b/routers/external/youtube.py @@ -0,0 +1,74 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger + +log_name = 'api_routers_external_youtube' +log_dir = os.getenv("LOG_PATH", "/logs") +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +from fastapi import APIRouter, HTTPException +from youtube_transcript_api import YouTubeTranscriptApi +from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound, VideoUnavailable +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +router = APIRouter() + +# Initialize the YouTube API client with API key +youtube = build('youtube', 'v3', developerKey=os.getenv('YOUTUBE_API_KEY')) + +@router.get("/youtube-proxy") +async def youtube_proxy(videoId: str): + try: + # Fetch transcript using youtube-transcript-api + transcript = YouTubeTranscriptApi.get_transcript(videoId, languages=['en']) + transcript_lines = [{"start": entry["start"], "duration": entry["duration"], "text": entry["text"]} for entry in transcript] + + # Fetch video details using YouTube Data API + video_response = youtube.videos().list( + part='snippet,contentDetails,statistics', + id=videoId + ).execute() + + if 'items' in video_response: + video_data = video_response['items'][0] + video_info = { + 'title': video_data['snippet']['title'], + 'author': video_data['snippet']['channelTitle'], + 'publishedAt': video_data['snippet']['publishedAt'], + 'description': video_data['snippet']['description'], + 'viewCount': video_data['statistics']['viewCount'], + 'likeCount': video_data['statistics']['likeCount'], + 'duration': video_data['contentDetails']['duration'], + } + else: + video_info = {} + + return { + "transcript": transcript_lines, + "video_info": video_info + } + + except HttpError as e: + logging.error(f"An HTTP error occurred: {str(e)}") + raise HTTPException(status_code=500, detail="YouTube API error") + except TranscriptsDisabled: + logging.error(f"Transcripts are disabled for video {videoId}") + raise HTTPException(status_code=404, detail="Transcripts are disabled for this video") + except NoTranscriptFound: + logging.error(f"No transcript found for video {videoId}") + raise HTTPException(status_code=404, detail="Transcript not available for this video") + except VideoUnavailable: + logging.error(f"Video {videoId} is unavailable") + raise HTTPException(status_code=404, detail="Video unavailable") + except Exception as e: + logging.error(f"Unexpected error: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/routers/health.py b/routers/health.py new file mode 100644 index 0000000..c3b01c6 --- /dev/null +++ b/routers/health.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, status +from pydantic import BaseModel + +router = APIRouter() + +class HealthCheck(BaseModel): + """Response model for health check endpoint""" + status: str = "healthy" + +@router.get( + "/health", + tags=["Health"], + summary="Perform a Health Check", + response_description="Return health status", + status_code=status.HTTP_200_OK, + response_model=HealthCheck +) +async def health_check() -> HealthCheck: + """ + Endpoint to perform a healthcheck. Used by container orchestration systems + to determine if the service is healthy and ready to receive traffic. + """ + return HealthCheck() \ No newline at end of file diff --git a/routers/langchain/__init__.py b/routers/langchain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/langchain/__pycache__/__init__.cpython-311.pyc b/routers/langchain/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e7400a4 Binary files /dev/null and b/routers/langchain/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/langchain/__pycache__/interactive_langgraph_query.cpython-311.pyc b/routers/langchain/__pycache__/interactive_langgraph_query.cpython-311.pyc new file mode 100644 index 0000000..83bee40 Binary files /dev/null and b/routers/langchain/__pycache__/interactive_langgraph_query.cpython-311.pyc differ diff --git a/routers/langchain/__pycache__/neo4j_graph_qa.cpython-311.pyc b/routers/langchain/__pycache__/neo4j_graph_qa.cpython-311.pyc new file mode 100644 index 0000000..28f06ab Binary files /dev/null and b/routers/langchain/__pycache__/neo4j_graph_qa.cpython-311.pyc differ diff --git a/routers/langchain/interactive_langgraph_query.py b/routers/langchain/interactive_langgraph_query.py new file mode 100644 index 0000000..4cc5cec --- /dev/null +++ b/routers/langchain/interactive_langgraph_query.py @@ -0,0 +1,80 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_interactive_langgraph_query' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import List +from modules.langchain.interactive_langgraph_query import perplexity_clone_graph +from modules.redis_config import get_cached_results, set_cached_results +from langchain_core.messages import HumanMessage + +router = APIRouter() + +class QueryRequest(BaseModel): + query: str + use_cache: bool = False + +class QueryResponse(BaseModel): + response: str + needs_more_info: bool + +@router.post("/query", response_model=QueryResponse) +async def interactive_query(request: QueryRequest): + logging.info(f"Received query: {request.query}") + try: + query_id = generate_random_alphanumeric() + config = {"configurable": {"thread_id": f'{query_id}'}, "recursion_limit": 20} + + inputs = { + "messages": [HumanMessage(content=request.query)], + } + + # Check cache for existing results only if DEV_MODE is false + use_cache = os.getenv("DEV_MODE", "true").lower() == "false" + if use_cache: + cache_key = f"langgraph_query:{request.query}" + cached_result = get_cached_results(cache_key) + if cached_result: + logging.info(f"Found cached result for query: {request.query}") + return cached_result + + logging.debug("Updating state with initial message") + perplexity_clone_graph.update_state(config, inputs) + + logging.debug("Invoking perplexity_clone_graph") + outputs = await perplexity_clone_graph.ainvoke(inputs, config) + + final_response = outputs['messages'][-1].content + needs_more_info = outputs.get('needs_more_info', False) + + logging.info(f"Final response: {final_response}") + logging.info(f"Needs more info: {needs_more_info}") + + response = QueryResponse(response=final_response, needs_more_info=needs_more_info) + + # Cache the result only if DEV_MODE is false + if use_cache: + set_cached_results(cache_key, response.dict()) + + return response + except Exception as e: + logging.error(f"Error in interactive query: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"An error occurred during the query process: {str(e)}") + +def generate_random_alphanumeric(length=4): + import random + import string + characters = string.ascii_letters + string.digits + return ''.join(random.choice(characters) for i in range(length)) diff --git a/routers/langchain/neo4j_graph_qa.py b/routers/langchain/neo4j_graph_qa.py new file mode 100644 index 0000000..bf2c65a --- /dev/null +++ b/routers/langchain/neo4j_graph_qa.py @@ -0,0 +1,153 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_routers_langchain_graph_qa' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from fastapi import APIRouter, HTTPException +from langchain.chains import GraphCypherQAChain +from langchain_community.graphs import Neo4jGraph +from langchain_community.chat_models import ChatOpenAI +from langchain.prompts.prompt import PromptTemplate +from routers.llm.private.ollama.ollama_wrapper import OllamaWrapper + +router = APIRouter() + +# Define the schema for nodes and relationships +node_types = { + "KeyStage": ["merged", "key_stage_name", "unique_id", "created"], + "KeyStageSyllabus": ["ks_syllabus_name", "unique_id", "created", "merged", "ks_syllabus_key_stage", "ks_syllabus_subject"], + "YearGroup": ["created", "merged", "unique_id", "year_group_name"], + "YearGroupSyllabus": ["created", "merged", "yr_syllabus_name", "yr_syllabus_year_group", "yr_syllabus_id", "yr_syllabus_subject"], + "Topic": ["topic_type", "topic_assessment_type", "created", "merged", "unique_id", "topic_id", "total_number_of_lessons_for_topic", "topic_title"], + "Lesson": ["topic_lesson_id", "topic_lesson_type", "created", "merged", "topic_lesson_title", "topic_lesson_length", "topic_lesson_suggested_activities", "topic_lesson_weblinks", "topic_lesson_skills_learned"], + "LearningStatement": ["created", "merged", "lesson_learning_statement", "lesson_learning_statement_id", "lesson_learning_statement_type"] +} + +relationship_types = { + "KEY_STAGE_INCLUDES_KEY_STAGE_SYLLABUS": ["created", "merged"], + "KEY_STAGE_SYLLABUS_INCLUDES_YEAR_GROUP_SYLLABUS": ["created", "merged"], + "YEAR_GROUP_FOLLOWS_YEAR_GROUP": ["created", "merged"], + "KEY_STAGE_FOLLOWS_KEY_STAGE": ["created", "merged"], + "YEAR_SYLLABUS_INCLUDES_TOPIC": ["created", "merged"], + "TOPIC_INCLUDES_LESSON": ["created", "merged"], + "LESSON_INCLUDES_LEARNING_STATEMENT": ["created", "merged"], + "LESSON_FOLLOWS_LESSON": ["created", "merged"] +} + +@router.get("/prompt") +async def query_graph( + database: str, prompt: str, top_k: int = 30, model: str = "gpt-4o", temperature: float = 0, + verbose: bool = False, return_intermediate_steps: bool = False, exclude_types: list = None, include_types: list = None, + return_direct: bool = False, validate_cypher: bool = False, model_type: str = "openai" +): + logging.info(f"Received request with prompt: {prompt}") + if exclude_types is None: + logging.info("No exclude_types provided, using default.") + exclude_types = [] + if include_types is None: + logging.info("No include_types provided, using default.") + include_types = [] + + # Validate include_types and exclude_types + logging.info(f"Validating include_types and exclude_types...") + valid_types = set(node_types.keys()).union(set(relationship_types.keys())) + logging.info(f"Valid types: {valid_types}") + exclude_types = [t for t in exclude_types if t in valid_types] + logging.info(f"Validated exclude_types: {exclude_types}") + include_types = [t for t in include_types if t in valid_types] + logging.info(f"Validated include_types: {include_types}") + + graph = Neo4jGraph( + url=os.environ['APP_BOLT_URL'], + username=os.environ['USER_NEO4J'], + password=os.environ['PASSWORD_NEO4J'], + database=database + ) + + logging.info("Refreshing schema...") + graph.refresh_schema() + logging.info("Schema refreshed.") + schema = graph.schema + logging.info(f"Schema: {schema}") + + CYPHER_GENERATION_TEMPLATE = """Task: Generate a Cypher statement to query a graph database for timetable information. + Role: + You are an assistant in a school for teachers, specializing in querying graph databases to find answers to questions. + The teacher will ask you questions about their timetable. + + Instructions: + 1. Use only the provided relationship types and properties in the schema. + 2. Do not use any other relationship types or properties that are not provided. + + Schema: + {schema} + + Note: + 1. Do not include any explanations or apologies in your responses. + 2. Do not respond to any questions that might ask anything else than for you to construct a Cypher statement. + 3. Do not include any text except the generated Cypher statement. + + The question is: + {question}""" + + CYPHER_GENERATION_PROMPT = PromptTemplate( + input_variables=["schema", "question"], + template=CYPHER_GENERATION_TEMPLATE + ) + + if model_type == "ollama": + ollama_host = os.getenv("OLLAMA_URL") + ollama_port = os.getenv("OLLAMA_PORT") + if not ollama_host or not ollama_port: + raise HTTPException(status_code=500, detail="Ollama host or port not set") + client = OllamaWrapper(host=f'http://{ollama_host}:{ollama_port}') + cypher_llm = client + qa_llm = client + else: + cypher_llm = ChatOpenAI(temperature=temperature, model=model) + qa_llm = ChatOpenAI(temperature=temperature, model=model) + + chain = GraphCypherQAChain.from_llm( + graph=graph, + cypher_llm=cypher_llm, + qa_llm=qa_llm, + top_k=top_k, + verbose=verbose, + cypher_prompt=CYPHER_GENERATION_PROMPT, + return_intermediate_steps=return_intermediate_steps, + exclude_types=exclude_types, + include_types=include_types, + return_direct=return_direct, + validate_cypher=validate_cypher + ) + + formatted_prompt = CYPHER_GENERATION_PROMPT.format(schema=schema, question=prompt) + + logging.info("\n\n") + + logging.info("==================================================") + logging.info("= graph_qa.py =") + logging.info("==================================================") + logging.info(f"Prompt: {prompt}") + logging.info("--------------------------------------------------") + logging.info(f"Schema: \n{schema}\n") + logging.info("--------------------------------------------------") + logging.info(f"Formatted Prompt: \n{formatted_prompt}\n") + logging.info("--------------------------------------------------") + logging.info(f"Cypher prompt: \n{CYPHER_GENERATION_PROMPT}\n") + logging.info("--------------------------------------------------") + logging.info(f"Cypher template: \n{CYPHER_GENERATION_TEMPLATE}\n") + logging.info("--------------------------------------------------") + logging.info(f"Cypher chain: \n{chain}\n") + logging.info("==================================================") + + return chain(prompt) \ No newline at end of file diff --git a/routers/langchain/test.ipynb b/routers/langchain/test.ipynb new file mode 100644 index 0000000..40bbba0 --- /dev/null +++ b/routers/langchain/test.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Running simple query tests with OpenAI:\n", + "\n", + "Testing simple queries using openai model:\n", + "\n", + "Query: What is the history of Maidstone, England?\n", + "Sending query to http://localhost:8000/api/langchain/interactive_langgraph_query/query with payload: {'query': 'What is the history of Maidstone, England?', 'model': 'openai'}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ERROR:root:Error sending query to http://localhost:8000/api/langchain/interactive_langgraph_query/query: 500 Server Error: Internal Server Error for url: http://localhost:8000/api/langchain/interactive_langgraph_query/query\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Response:\n", + "{\n", + " \"error\": \"500 Server Error: Internal Server Error for url: http://localhost:8000/api/langchain/interactive_langgraph_query/query\"\n", + "}\n", + "==================================================\n" + ] + } + ], + "source": [ + "from dotenv import load_dotenv, find_dotenv\n", + "load_dotenv(find_dotenv())\n", + "import os\n", + "import logging\n", + "# Function to send a query and get the response\n", + "import requests\n", + "import json\n", + "\n", + "# Define the URL of your FastAPI server\n", + "BASE_URL = \"http://localhost:8000\" # Adjust this if your server is running on a different port or host\n", + "\n", + "# Define the endpoint\n", + "ENDPOINT = f\"{BASE_URL}/api/langchain/interactive_langgraph_query/query\"\n", + "\n", + "def send_query(query, model=\"ollama\"):\n", + " payload = {\"query\": query, \"model\": model}\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + " print(f\"Sending query to {ENDPOINT} with payload: {payload}\")\n", + " \n", + " try:\n", + " response = requests.post(ENDPOINT, json=payload, headers=headers)\n", + " response.raise_for_status()\n", + " print(f\"Received response from {ENDPOINT}: {response.json()}\")\n", + " return response.json()\n", + " except requests.exceptions.RequestException as e:\n", + " logging.error(f\"Error sending query to {ENDPOINT}: {str(e)}\")\n", + " return {\"error\": str(e)}\n", + "\n", + "def test_simple_queries(model=\"openai\"):\n", + " queries = [\n", + " \"What is the history of Maidstone, England?\"\n", + " ]\n", + " \n", + " print(f\"Testing simple queries using {model} model:\")\n", + " for query in queries:\n", + " print(f\"\\nQuery: {query}\")\n", + " result = send_query(query, model)\n", + " print(\"Response:\")\n", + " print(json.dumps(result, indent=2))\n", + " print(\"=\" * 50)\n", + "\n", + "def test_followup_queries(model=\"openai\"):\n", + " queries = [\n", + " \"What is the latest local news from a particular town?\"\n", + " ]\n", + " \n", + " print(f\"Testing queries requiring follow-up using {model} model:\")\n", + " for query in queries:\n", + " print(f\"\\nInitial Query: {query}\")\n", + " result = send_query(query, model)\n", + " print(\"Initial Response:\")\n", + " print(json.dumps(result, indent=2))\n", + " \n", + " follow_up_count = 0\n", + " max_follow_ups = 3\n", + " \n", + " while result.get(\"needs_more_info\", False) and follow_up_count < max_follow_ups:\n", + " follow_up = input(\"Please provide more information: \")\n", + " follow_up_query = f\"{query} {follow_up}\"\n", + " follow_up_result = send_query(follow_up_query, model)\n", + " print(f\"\\nFollow-up Response {follow_up_count + 1}:\")\n", + " print(json.dumps(follow_up_result, indent=2))\n", + " \n", + " result = follow_up_result\n", + " follow_up_count += 1\n", + " \n", + " if follow_up_count == max_follow_ups:\n", + " print(\"\\nMaximum number of follow-ups reached. Moving to next query.\")\n", + " elif not result.get(\"needs_more_info\", False):\n", + " print(\"\\nFinal Response:\")\n", + " print(json.dumps(result, indent=2))\n", + " \n", + " print(\"=\" * 50)\n", + "\n", + "# Run the tests\n", + "#print(\"Running simple query tests with Ollama:\\n\")\n", + "#test_simple_queries(\"ollama\")\n", + "\n", + "print(\"\\nRunning simple query tests with OpenAI:\\n\")\n", + "test_simple_queries(\"openai\")\n", + "\n", + "#print(\"\\nRunning follow-up query tests with Ollama:\\n\")\n", + "#test_followup_queries(\"ollama\")\n", + "\n", + "#print(\"\\nRunning follow-up query tests with OpenAI:\\n\")\n", + "#test_followup_queries(\"openai\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/routers/llm/private/__init__.py b/routers/llm/private/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/llm/private/__pycache__/__init__.cpython-311.pyc b/routers/llm/private/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6858fa0 Binary files /dev/null and b/routers/llm/private/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/llm/private/ollama/__init__.py b/routers/llm/private/ollama/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/llm/private/ollama/__pycache__/__init__.cpython-311.pyc b/routers/llm/private/ollama/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9664857 Binary files /dev/null and b/routers/llm/private/ollama/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/llm/private/ollama/__pycache__/ollama.cpython-311.pyc b/routers/llm/private/ollama/__pycache__/ollama.cpython-311.pyc new file mode 100644 index 0000000..228eec2 Binary files /dev/null and b/routers/llm/private/ollama/__pycache__/ollama.cpython-311.pyc differ diff --git a/routers/llm/private/ollama/__pycache__/ollama_wrapper.cpython-311.pyc b/routers/llm/private/ollama/__pycache__/ollama_wrapper.cpython-311.pyc new file mode 100644 index 0000000..bb87419 Binary files /dev/null and b/routers/llm/private/ollama/__pycache__/ollama_wrapper.cpython-311.pyc differ diff --git a/routers/llm/private/ollama/ollama.py b/routers/llm/private/ollama/ollama.py new file mode 100644 index 0000000..9d8963b --- /dev/null +++ b/routers/llm/private/ollama/ollama.py @@ -0,0 +1,108 @@ +# Import necessary libraries +import os +from dotenv import load_dotenv, find_dotenv +from fastapi import APIRouter, FastAPI, HTTPException +from pydantic import BaseModel +from typing import List, Dict, Optional +import ollama +from ollama import Client + +load_dotenv(find_dotenv()) + +router = APIRouter() +## client = Client(host='http://localhost:11434') + +ollama_host = os.getenv("HOST_OLLAMA") +ollama_port = os.getenv("PORT_OLLAMA") + +if not ollama_host or not ollama_port: + raise ValueError("Environment variables HOST_OLLAMA or PORT_OLLAMA are not set") + +client = Client(host=f'http://{ollama_host}:{ollama_port}') + +class UserRequest(BaseModel): + question: str + model: str = "llama3" + temperature: Optional[float] = None + top_p: Optional[float] = None + max_tokens: Optional[int] = None + +@router.post("/ollama_text_prompt") +async def ollama_text_prompt(user_request: UserRequest): + model_name = user_request.model + question = user_request.question + options = { + "temperature": user_request.temperature, + "top_p": user_request.top_p, + "max_tokens": user_request.max_tokens, + } + + supported_models = ["llama2", "llama3", "mistral", "llama3"] + if model_name not in supported_models: + raise HTTPException(status_code=400, detail="Model not supported") + + messages = [{"role": "user", "content": question}] + try: + response = client.chat(model=model_name, messages=messages, options=options) + if "message" in response and "content" in response["message"]: + return {"model": model_name, "response": response["message"]["content"]} + else: + raise HTTPException(status_code=500, detail="Invalid response structure from model") + except Exception as e: + print(f"Error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +class GenerateRequest(BaseModel): + model: str + prompt: str + +@router.post("/ollama_generate") +async def ollama_generate(request: GenerateRequest): + try: + response = client.generate(model=request.model, prompt=request.prompt) + return {"model": request.model, "response": response} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +class VisionRequest(BaseModel): + model: str + image_path: str + prompt: str + +@router.post("/ollama_vision_prompt") +async def ollama_vision_prompt(request: VisionRequest): + try: + response = client.vision(model=request.model, image_path=request.image_path, prompt=request.prompt) + return {"model": request.model, "response": response} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +class Message(BaseModel): + role: str + content: str + +class CopilotRequest(BaseModel): + model: str + messages: List[Message] + options: Optional[Dict[str, float]] = None + +@router.post("/ollama_copilot_prompt") +async def ollama_copilot_prompt(request: CopilotRequest): + model_name = request.model + messages = request.messages + options = request.options or {} + print(f"Model: {model_name}, Messages: {messages}, Options: {options}") + + try: + print("Generating response...") + response = ollama.chat(model=model_name, messages=messages, **options) + print(f"Response: {response}") + if "message" in response and "content" in response["message"]: + print(f"Response: {response['message']['content']}") + return {"model": model_name, "response": response["message"]["content"]} + else: + print(f"Invalid response structure from model: {response}") + raise HTTPException(status_code=500, detail="Invalid response structure from model") + except Exception as e: + print(f"Error: {e}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/routers/llm/private/ollama/ollama_wrapper.py b/routers/llm/private/ollama/ollama_wrapper.py new file mode 100644 index 0000000..2df0a90 --- /dev/null +++ b/routers/llm/private/ollama/ollama_wrapper.py @@ -0,0 +1,28 @@ +from typing import Any, Dict +from ollama import Client +from langchain_core.runnables.base import Runnable +from langchain.prompts.base import StringPromptValue + +class OllamaWrapper(Runnable): + def __init__(self, host: str): + self.client = Client(host=host) + + def invoke(self, prompt: Any, config: Dict[str, Any] = None, **kwargs: Any) -> str: + if isinstance(prompt, StringPromptValue): + prompt = prompt.to_string() + + model_name = kwargs.get("model", "llama3") + options = { + "temperature": kwargs.get("temperature"), + "top_p": kwargs.get("top_p"), + "max_tokens": kwargs.get("max_tokens"), + } + messages = [{"role": "user", "content": prompt}] + response = self.client.chat(model=model_name, messages=messages, options=options) + if response and "message" in response and "content" in response["message"]: + return response["message"]["content"] + else: + raise ValueError("Invalid response structure from model") + + async def ainvoke(self, prompt: Any, config: Dict[str, Any] = None, **kwargs: Any) -> str: + return self.invoke(prompt, config, **kwargs) diff --git a/routers/llm/public/__init__.py b/routers/llm/public/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/llm/public/__pycache__/__init__.cpython-311.pyc b/routers/llm/public/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..fb23a30 Binary files /dev/null and b/routers/llm/public/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/llm/public/openai/__init__.py b/routers/llm/public/openai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/llm/public/openai/__pycache__/__init__.cpython-311.pyc b/routers/llm/public/openai/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..654ecdd Binary files /dev/null and b/routers/llm/public/openai/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/llm/public/openai/__pycache__/openai.cpython-311.pyc b/routers/llm/public/openai/__pycache__/openai.cpython-311.pyc new file mode 100644 index 0000000..36ca56b Binary files /dev/null and b/routers/llm/public/openai/__pycache__/openai.cpython-311.pyc differ diff --git a/routers/llm/public/openai/openai.py b/routers/llm/public/openai/openai.py new file mode 100644 index 0000000..3c8dc4c --- /dev/null +++ b/routers/llm/public/openai/openai.py @@ -0,0 +1,77 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import List, Dict, Optional +from openai import OpenAI +import os +import logging + +# Set up logging configuration +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Instantiate the OpenAI client +client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + +router = APIRouter() + +class Message(BaseModel): + role: str + content: str + +class CopilotRequest(BaseModel): + model: str + messages: List[Message] + options: Optional[Dict[str, float]] = None + +@router.post("/openai_copilot_prompt") +async def openai_copilot_prompt(request: CopilotRequest): + logging.info("Received request: %s", request.model_dump_json()) + try: + response = client.chat.completions.create( + model=request.model, + messages=[{"role": msg.role, "content": msg.content} for msg in request.messages], + **(request.options or {}) + ) + logging.info("Received response: %s", response) + return {"model": request.model, "response": response.choices[0].message.content} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +class GeneralOpenAIRequest(BaseModel): + model: str + prompt: str + max_tokens: Optional[int] = 100 + temperature: Optional[float] = 0.7 + top_p: Optional[float] = 1.0 + n: Optional[int] = 1 + stop: Optional[List[str]] = None + +@router.post("/openai_general_prompt") +async def openai_general_prompt(request: GeneralOpenAIRequest): + logging.info("Received general request: %s", request.model_dump_json()) + try: + if "gpt-4" in request.model or "gpt-3.5" in request.model: + messages = [{"role": "user", "content": request.prompt}] + response = client.chat.completions.create( + model=request.model, + messages=messages, + max_tokens=request.max_tokens, + temperature=request.temperature, + top_p=request.top_p, + n=request.n, + stop=request.stop + ) + return {"model": request.model, "response": response.choices[0].message.content} + else: + response = client.completions.create( + model=request.model, + prompt=request.prompt, + max_tokens=request.max_tokens, + temperature=request.temperature, + top_p=request.top_p, + n=request.n, + stop=request.stop + ) + return {"model": request.model, "response": response.choices[0].text} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/msgraph/__init__.py b/routers/msgraph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/msgraph/__pycache__/__init__.cpython-311.pyc b/routers/msgraph/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..b82ade5 Binary files /dev/null and b/routers/msgraph/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/msgraph/__pycache__/router_onenote.cpython-311.pyc b/routers/msgraph/__pycache__/router_onenote.cpython-311.pyc new file mode 100644 index 0000000..b926f81 Binary files /dev/null and b/routers/msgraph/__pycache__/router_onenote.cpython-311.pyc differ diff --git a/routers/msgraph/router_onenote.py b/routers/msgraph/router_onenote.py new file mode 100644 index 0000000..c000a95 --- /dev/null +++ b/routers/msgraph/router_onenote.py @@ -0,0 +1,110 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_router_onenote' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +from fastapi import APIRouter, Header, HTTPException +import httpx +import aiohttp + +MICROSOFT_GRAPH_API = "https://graph.microsoft.com/v1.0" + +router = APIRouter() + +@router.get("/test-microsoft-graph-connection") +async def test_microsoft_graph_connection(): + try: + async with aiohttp.ClientSession() as session: + async with session.get(f"{MICROSOFT_GRAPH_API}/$metadata") as response: + if response.status == 200: + return {"status": "success", "message": "Successfully connected to Microsoft Graph API"} + else: + return {"status": "error", "message": f"Failed to connect to Microsoft Graph API. Status code: {response.status}"} + except Exception as e: + logging.error(f"Error testing connection to Microsoft Graph API: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error testing connection to Microsoft Graph API: {str(e)}") + +@router.get("/onenote/get-onenote-notebooks") +async def get_onenote_notebooks(authorization: str = Header(None)): + if not authorization: + raise HTTPException(status_code=401, detail="Authorization token missing") + try: + scheme, token = authorization.split() + if scheme.lower() != 'bearer': + raise HTTPException(status_code=401, detail="Invalid authentication scheme") + access_token = token + except ValueError: + raise HTTPException(status_code=401, detail="Invalid token format") + + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + get_notebooks_url = f"{MICROSOFT_GRAPH_API}/me/onenote/notebooks" + + async with httpx.AsyncClient(timeout=30.0) as client: + try: + response = await client.get(get_notebooks_url, headers=headers) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error making request to Microsoft Graph API: {str(e)}") + + if response.status_code == 200: + return response.json() + else: + raise HTTPException(status_code=response.status_code, detail=f"Error getting notebooks: {response.text}") + +@router.post("/onenote/create-onenote-notebook") +async def create_onenote_notebook(notebook_name: str, authorization: str = Header(None)): + logging.info(f"Received request to create notebook: {notebook_name}") + if not authorization: + logging.error("Authorization token missing") + raise HTTPException(status_code=401, detail="Authorization token missing") + try: + scheme, token = authorization.split() + if scheme.lower() != 'bearer': + logging.error(f"Invalid authentication scheme: {scheme}") + raise HTTPException(status_code=401, detail="Invalid authentication scheme") + access_token = token + except ValueError: + logging.error("Invalid token format") + raise HTTPException(status_code=401, detail="Invalid token format") + + # logging.debug(f"Extracted access token: {access_token}") + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + create_notebook_url = f"{MICROSOFT_GRAPH_API}/me/onenote/notebooks" + notebook_data = { + "displayName": notebook_name + } + async with httpx.AsyncClient(timeout=30.0) as client: + try: + logging.debug(f"Sending request to: {create_notebook_url}") + logging.debug(f"Headers: {headers}") + logging.debug(f"Data: {notebook_data}") + response = await client.post(create_notebook_url, headers=headers, json=notebook_data) + logging.debug(f"Microsoft Graph API response status: {response.status_code}") + logging.debug(f"Microsoft Graph API response content: {response.text}") + except httpx.ConnectTimeout: + logging.error("Connection timeout when trying to reach Microsoft Graph API") + raise HTTPException(status_code=504, detail="Connection timeout when trying to reach Microsoft Graph API") + except Exception as e: + logging.error(f"Error making request to Microsoft Graph API: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error making request to Microsoft Graph API: {str(e)}") + + if response.status_code == 201: + logging.info("Notebook created successfully") + return {"message": "Notebook created successfully", "data": response.json()} + else: + logging.error(f"Error creating notebook: {response.status_code} - {response.text}") + raise HTTPException(status_code=response.status_code, detail=f"Error creating notebook: {response.text}") \ No newline at end of file diff --git a/routers/rpi/__pycache__/rpi_whisperlive_client.cpython-311.pyc b/routers/rpi/__pycache__/rpi_whisperlive_client.cpython-311.pyc new file mode 100644 index 0000000..e813acd Binary files /dev/null and b/routers/rpi/__pycache__/rpi_whisperlive_client.cpython-311.pyc differ diff --git a/routers/rpi/rpi_whisperlive_client.py b/routers/rpi/rpi_whisperlive_client.py new file mode 100644 index 0000000..5efabb7 --- /dev/null +++ b/routers/rpi/rpi_whisperlive_client.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +import jwt +import os + +router = APIRouter() + +class LoginRequest(BaseModel): + device_id: str + +@router.post("/login") +async def rpi_login(request: LoginRequest): + if request.device_id == "rpi_zero": + token = jwt.encode({"device_id": request.device_id}, os.getenv("FASTAPI_SECRET_KEY"), algorithm="HS256") + return {"token": token} + else: + raise HTTPException(status_code=401, detail="Unauthorized") + diff --git a/routers/solid/__init__.py b/routers/solid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/solid/__pycache__/__init__.cpython-311.pyc b/routers/solid/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ac6a814 Binary files /dev/null and b/routers/solid/__pycache__/__init__.cpython-311.pyc differ diff --git a/routers/solid/__pycache__/pod_provisioner.cpython-311.pyc b/routers/solid/__pycache__/pod_provisioner.cpython-311.pyc new file mode 100644 index 0000000..f0845c2 Binary files /dev/null and b/routers/solid/__pycache__/pod_provisioner.cpython-311.pyc differ diff --git a/routers/solid/pod_provisioner.py b/routers/solid/pod_provisioner.py new file mode 100644 index 0000000..cc68dbb --- /dev/null +++ b/routers/solid/pod_provisioner.py @@ -0,0 +1,142 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from pathlib import Path +import os +from typing import Optional +from modules.logger_tool import initialise_logger + +# Initialize logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL", "info"), os.getenv("LOG_PATH", "/logs")) + +# Create router +router = APIRouter() + +# === CONFIG === +SOLID_BASE_URL = os.getenv("SOLID_BASE_URL", "http://solid.classroomcopilot.test") +SOLID_STORAGE_PATH = os.getenv("SOLID_STORAGE_PATH", "/data/users") # Path relative to Solid server data directory +KEYCLOAK_ISSUER = os.getenv("KEYCLOAK_ISSUER", "http://keycloak.classroomcopilot.test/realms/ClassroomCopilot") + +# === DATA MODEL === +class UserCreateRequest(BaseModel): + username: str + full_name: str + email: Optional[str] = None + +# === UTILITIES === +def create_profile_card(username: str, full_name: str, email: Optional[str] = None) -> str: + webid = f"{SOLID_BASE_URL}/users/{username}/profile/card#me" + profile_content = f"""@prefix foaf: . +@prefix solid: . +@prefix vcard: . + +<#me> a foaf:Person ; + foaf:name "{full_name}" ; + solid:oidcIssuer <{KEYCLOAK_ISSUER}> .""" + + if email: + profile_content += f""" + vcard:hasEmail .""" + + return profile_content + +def create_acl_file(username: str) -> str: + return f"""@prefix acl: . +@prefix foaf: . + +<#authorization> + a acl:Authorization ; + acl:agent <{SOLID_BASE_URL}/users/{username}/profile/card#me> ; + acl:accessTo <./> ; + acl:default <./> ; + acl:mode acl:Read, acl:Write, acl:Control ; + acl:agentClass foaf:Agent .""" + +def create_public_acl() -> str: + return """@prefix acl: . +@prefix foaf: . + +<#authorization> + a acl:Authorization ; + acl:agentClass foaf:Agent ; + acl:accessTo <./> ; + acl:default <./> ; + acl:mode acl:Read .""" + +# === ENDPOINTS === +@router.post("/provision") +async def provision_user(data: UserCreateRequest): + """ + Create a new Solid pod for a user with their profile card and ACL files. + """ + try: + # Create user directory structure + user_base = Path(SOLID_STORAGE_PATH) / data.username + profile_dir = user_base / "profile" + public_dir = user_base / "public" + private_dir = user_base / "private" + + # Create directories + for dir_path in [profile_dir, public_dir, private_dir]: + dir_path.mkdir(parents=True, exist_ok=True) + + # Create profile card + profile_path = profile_dir / "card.ttl" + profile_content = create_profile_card(data.username, data.full_name, data.email) + profile_path.write_text(profile_content) + + # Create ACL files + profile_acl_path = profile_dir / ".acl" + profile_acl_content = create_acl_file(data.username) + profile_acl_path.write_text(profile_acl_content) + + public_acl_path = public_dir / ".acl" + public_acl_content = create_public_acl() + public_acl_path.write_text(public_acl_content) + + private_acl_path = private_dir / ".acl" + private_acl_content = create_acl_file(data.username) + private_acl_path.write_text(private_acl_content) + + webid = f"{SOLID_BASE_URL}/users/{data.username}/profile/card#me" + + logger.info(f"Successfully provisioned Solid pod for user {data.username}") + + return { + "message": "User pod created successfully", + "webid": webid, + "profile_path": str(profile_path), + "pod_structure": { + "profile": str(profile_dir), + "public": str(public_dir), + "private": str(private_dir) + } + } + + except Exception as e: + logger.error(f"Error provisioning Solid pod for user {data.username}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/status/{username}") +async def check_pod_status(username: str): + """ + Check if a user's Solid pod exists and return its status. + """ + try: + user_base = Path(SOLID_STORAGE_PATH) / username + profile_path = user_base / "profile" / "card.ttl" + + if not profile_path.exists(): + return { + "exists": False, + "message": "Pod not found" + } + + return { + "exists": True, + "webid": f"{SOLID_BASE_URL}/users/{username}/profile/card#me", + "profile_path": str(profile_path) + } + + except Exception as e: + logger.error(f"Error checking pod status for user {username}: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/transcribe/__init__.py b/routers/transcribe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/transcribe/utterance.py b/routers/transcribe/utterance.py new file mode 100644 index 0000000..dd3678e --- /dev/null +++ b/routers/transcribe/utterance.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, Request +import os +import queue +from dotenv import load_dotenv +import json + +load_dotenv() + +router = APIRouter() + +@router.post("/handle_whisper_live_eos_utterance/{user_id}") +async def handle_whisper_live_eos_utterance(user_id: str, request: Request): + data = await request.json() + utterance = data.get("utterance") + print(f"Utterance: {utterance}") + start = data.get("start") + end = data.get("end") + print(f"Start: {start}") + print(f"End: {end}") + eos = data.get("eos") + print(f"Eos: {eos}") + user_dir = f"../../data/users/{user_id}/transcripts" + if not os.path.exists(user_dir): + os.makedirs(user_dir) + + log_file = os.path.join(user_dir, "utterances.log") + with open(log_file, "a") as f: + f.write(json.dumps(data) + "\n") + + return {"message": "Utterance logged successfully"} + +@router.get("/get_utterances/{user_id}") +async def get_utterances(user_id: str): + user_dir = f"../../data/users/{user_id}/transcripts" + log_file = os.path.join(user_dir, "utterances.log") + if not os.path.exists(log_file): + return {"utterances": []} + + with open(log_file, "r") as f: + utterances = [json.loads(line) for line in f] + + return {"utterances": utterances} diff --git a/run/__init__.py b/run/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/run/__pycache__/__init__.cpython-311.pyc b/run/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..d82920b Binary files /dev/null and b/run/__pycache__/__init__.cpython-311.pyc differ diff --git a/run/__pycache__/routers.cpython-311.pyc b/run/__pycache__/routers.cpython-311.pyc new file mode 100644 index 0000000..9af8705 Binary files /dev/null and b/run/__pycache__/routers.cpython-311.pyc differ diff --git a/run/__pycache__/setup.cpython-311.pyc b/run/__pycache__/setup.cpython-311.pyc new file mode 100644 index 0000000..4a67832 Binary files /dev/null and b/run/__pycache__/setup.cpython-311.pyc differ diff --git a/run/dependencies.py b/run/dependencies.py new file mode 100644 index 0000000..7190015 --- /dev/null +++ b/run/dependencies.py @@ -0,0 +1,45 @@ +from fastapi import HTTPException, Security +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from datetime import datetime, timedelta +from typing import Optional +from dotenv import load_dotenv +import os + +load_dotenv() +SECRET_KEY = os.getenv("FASTAPI_SECRET_KEY") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +class TokenData: + username: Optional[str] = None + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +def verify_token(token: str, credentials_exception): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + +def admin_dependency(token: str = Security(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=HTTPException, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return verify_token(token, credentials_exception) \ No newline at end of file diff --git a/run/initialization/__init__.py b/run/initialization/__init__.py new file mode 100644 index 0000000..122594d --- /dev/null +++ b/run/initialization/__init__.py @@ -0,0 +1,26 @@ +from .manager import InitializationManager +from .initialization import InitializationSystem +from modules.logger_tool import initialise_logger +import os + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +def initialize_system() -> None: + """Initialize the system if needed""" + init_manager = InitializationManager() + + if not init_manager.check_initialization_needed(): + logger.info("No initialization needed") + return + + logger.info("Starting system initialization...") + + init_system = InitializationSystem(init_manager) + success = init_system.run() + + if success: + logger.info("System initialization completed successfully") + else: + logger.error("System initialization failed") + +__all__ = ['initialize_system', 'InitializationManager', 'InitializationSystem'] \ No newline at end of file diff --git a/run/initialization/__pycache__/__init__.cpython-311.pyc b/run/initialization/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..677b3ae Binary files /dev/null and b/run/initialization/__pycache__/__init__.cpython-311.pyc differ diff --git a/run/initialization/__pycache__/initialization.cpython-311.pyc b/run/initialization/__pycache__/initialization.cpython-311.pyc new file mode 100644 index 0000000..1a12c72 Binary files /dev/null and b/run/initialization/__pycache__/initialization.cpython-311.pyc differ diff --git a/run/initialization/__pycache__/manager.cpython-311.pyc b/run/initialization/__pycache__/manager.cpython-311.pyc new file mode 100644 index 0000000..937c279 Binary files /dev/null and b/run/initialization/__pycache__/manager.cpython-311.pyc differ diff --git a/run/initialization/initialization.py b/run/initialization/initialization.py new file mode 100644 index 0000000..a8b20da --- /dev/null +++ b/run/initialization/initialization.py @@ -0,0 +1,1704 @@ +#!/usr/bin/env python3 +""" +ClassroomCopilot Initialization System +This script orchestrates the initialization of all system components. +""" +import os +import sys +import json +import time +import requests +import csv +from typing import Dict, Any, Optional +from datetime import datetime, timedelta +from modules.database.services.neo4j_service import Neo4jService +from modules.database.supabase.utils.client import SupabaseServiceRoleClient, CreateBucketOptions +from modules.database.init.init_user import NonSchoolUserCreator +from modules.database.init.init_calendar import create_calendar +import modules.database.init.init_school as init_school +import modules.database.init.init_school_timetable as init_school_timetable +import modules.database.init.init_school_curriculum as init_school_curriculum +import modules.database.init.xl_tools as xl +from modules.database.schemas.nodes.schools.schools import SchoolNode +from modules.logger_tool import initialise_logger +from modules.database.tools.neo4j_driver_tools import get_session +import modules.database.schemas.nodes.structures.schools as school_structures +import modules.database.tools.neontology_tools as neon +import modules.database.schemas.relationships.entity_relationships as entity_relationships +import modules.database.schemas.relationships.structures.schools as structure_relationships + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +# Configuration +SUPABASE_URL = os.environ.get("SUPABASE_URL") +SERVICE_ROLE_KEY = os.environ.get("SERVICE_ROLE_KEY") +ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL") +ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD") +ADMIN_NAME = os.environ.get("ADMIN_NAME", "Super Admin") +ADMIN_FULL_NAME = os.environ.get("ADMIN_FULL_NAME", "Super Admin Full Name") +POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD") +POSTGRES_DB = os.environ.get("POSTGRES_DB") +ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME", "superadmin") +ADMIN_DISPLAY_NAME = os.environ.get("ADMIN_DISPLAY_NAME", "Super Admin") + +class InitializationSystem: + """Main initialization system that orchestrates all components""" + + def __init__(self, manager=None): + if manager: + self.manager = manager + self.supabase_headers = manager.supabase_headers + self.status = manager.status + else: + # Fallback to original standalone behavior + self.supabase_headers = { + "apikey": SERVICE_ROLE_KEY, + "Authorization": f"Bearer {SERVICE_ROLE_KEY}", + "Content-Type": "application/json" + } + self.manager = None + self.status = self._load_status() + + self.admin_token = None + self.init_dir = "/app/data/init" + + # Ensure data directory exists + os.makedirs(self.init_dir, exist_ok=True) + + # Copy template files if they don't exist + self._ensure_data_files() + + self.neo4j_service = Neo4jService() + + def _ensure_data_files(self): + """Ensure required data files exist""" + # Check for schools data file + csv_path = os.path.join(self.init_dir, "sample_schools.csv") + if not os.path.exists(csv_path): + logger.warning(f"Schools data file not found at {csv_path}") + + def _load_status(self) -> Dict: + """Load initialization status from file or create default""" + try: + with open("/init/status.json", "r") as f: + status = json.load(f) + + # Verify all required keys exist + default_status = { + "super_admin_created": False, + "admin_token_obtained": False, + "storage": { + "initialized": False, + "buckets": { + "cc.users": False, + "cc.institutes": False + } + }, + "neo4j": { + "initialized": False, + "database_created": False, + "schema_initialized": False, + "schools_imported": False + }, + "cc_database": { + "initialized": False, + "database_created": False, + "schema_initialized": False, + "calendar_created": False + }, + "completed": False, + "timestamp": None, + "steps": [] + } + + # Recursively update status with any missing keys + def update_dict(current: Dict, default: Dict) -> Dict: + for key, value in default.items(): + if key not in current: + current[key] = value + elif isinstance(value, dict) and isinstance(current[key], dict): + current[key] = update_dict(current[key], value) + return current + + status = update_dict(status, default_status) + self._save_status(status) + return status + + except (FileNotFoundError, json.JSONDecodeError): + default_status = { + "super_admin_created": False, + "admin_token_obtained": False, + "storage": { + "initialized": False, + "buckets": { + "cc.users": False, + "cc.institutes": False + } + }, + "neo4j": { + "initialized": False, + "database_created": False, + "schema_initialized": False, + "schools_imported": False + }, + "cc_database": { + "initialized": False, + "database_created": False, + "schema_initialized": False, + "calendar_created": False + }, + "completed": False, + "timestamp": None, + "steps": [] + } + self._save_status(default_status) + return default_status + + def _save_status(self, status: Dict) -> None: + """Save initialization status to file""" + if self.manager: + self.manager._save_status(status) + else: + # Fallback to direct file save + os.makedirs(os.path.dirname("/init/status.json"), exist_ok=True) + with open("/init/status.json", "w") as f: + json.dump(status, f, indent=2) + + def update_status(self, key: str, value: Any) -> None: + """Update a specific status key and save""" + if isinstance(value, dict): + if key not in self.status: + self.status[key] = {} + self.status[key].update(value) + else: + self.status[key] = value + + self.status["timestamp"] = time.time() + self._save_status(self.status) + + def wait_for_services(self) -> bool: + """Wait for required services to be available""" + logger.info("Waiting for services to be available...") + + def check_supabase_endpoint(endpoint: str, description: str) -> bool: + """Check a specific Supabase endpoint with retry logic""" + max_retries = 30 + retry_count = 0 + initial_delay = 5 + max_delay = 30 + + while retry_count < max_retries: + try: + url = f"{SUPABASE_URL}/{endpoint}" + logger.info(f"Checking Supabase {description} at {url}") + + response = requests.get( + url, + headers={"apikey": SERVICE_ROLE_KEY}, + timeout=10 # Add timeout to prevent hanging + ) + + if response.status_code < 500: + logger.info(f"Supabase {description} is available") + return True + + logger.warning( + f"Supabase {description} returned status {response.status_code}: {response.text}" + ) + + except requests.RequestException as e: + logger.warning(f"Error checking Supabase {description}: {str(e)}") + + retry_count += 1 + if retry_count < max_retries: + # Calculate delay with exponential backoff + delay = min(initial_delay * (2 ** (retry_count - 1)), max_delay) + logger.info( + f"Waiting for Supabase {description}... " + f"({retry_count}/{max_retries}, next retry in {delay}s)" + ) + time.sleep(delay) + + logger.error(f"Supabase {description} is not available after {max_retries} attempts") + return False + + # Check multiple Supabase endpoints + endpoints = [ + ("rest/v1/", "REST API"), + ("auth/v1/", "Auth API"), + ("storage/v1/", "Storage API") + ] + + for endpoint, description in endpoints: + if not check_supabase_endpoint(endpoint, description): + logger.error(f"Failed to connect to Supabase {description}") + return False + + logger.info("All Supabase services are available") + return True + + def check_super_admin_exists(self) -> bool: + """Check if super admin exists in both auth and profiles""" + try: + # Ensure Supabase headers are properly set + self._ensure_supabase_headers() + + # 1. Check auth.users table + response = self._supabase_request_with_retry( + 'get', + f"{SUPABASE_URL}/auth/v1/admin/users", + headers=self.supabase_headers + ) + + if response.status_code != 200: + logger.error(f"Failed to check auth users: {response.text}") + return False + + try: + # Parse the response + auth_data = response.json() + + # Check if we have the expected structure + if not isinstance(auth_data, dict) or 'users' not in auth_data: + logger.error(f"Unexpected auth users response structure: {auth_data}") + return False + + # Find our admin in the list of users + auth_user = next( + (user for user in auth_data['users'] + if isinstance(user, dict) and user.get("email") == ADMIN_EMAIL), + None + ) + + if not auth_user: + logger.info("Super admin not found in auth.users") + return False + + user_id = auth_user.get("id") + logger.info(f"Found auth user with ID: {user_id}") + + # Verify the user has the correct metadata + app_metadata = auth_user.get("app_metadata", {}) + if app_metadata.get("role") != "supabase_admin": + logger.info("User exists but is not a supabase_admin") + return False + + # 2. Check public.profiles table + response = self._supabase_request_with_retry( + 'get', + f"{SUPABASE_URL}/rest/v1/profiles", + headers=self.supabase_headers, + params={ + "select": "*", + "email": f"eq.{ADMIN_EMAIL}" + } + ) + + if response.status_code != 200: + logger.error(f"Failed to check profiles: {response.text}") + return False + + try: + profiles = response.json() + if not isinstance(profiles, list): + logger.error(f"Unexpected profiles response format: {profiles}") + return False + + if not profiles: + logger.info("Super admin not found in public.profiles") + return False + + profile = profiles[0] + if not isinstance(profile, dict): + logger.error(f"Unexpected profile format: {profile}") + return False + + # Verify admin status and username + user_type = profile.get("user_type") + username = profile.get("username") + if user_type != "admin" or not username: + logger.info(f"User exists but is not properly configured (type: {user_type}, username: {username})") + return False + + logger.info("Super admin exists and is properly configured") + return True + + except json.JSONDecodeError as e: + logger.error(f"Failed to parse profiles response: {str(e)}") + return False + + except json.JSONDecodeError as e: + logger.error(f"Failed to parse auth users response: {str(e)}") + return False + + except Exception as e: + logger.error(f"Error checking super admin existence: {str(e)}") + return False + + def create_super_admin(self) -> bool: + """Create the super admin user""" + if self.status.get("super_admin_created"): + if self.check_super_admin_exists(): + logger.info("Super admin already exists and is properly configured") + return True + else: + logger.warning("Status indicates super admin created but verification failed") + + logger.info(f"Creating super admin user with email: {os.getenv('ADMIN_EMAIL')}") + + try: + # Create the main users database first + try: + with get_session() as session: + logger.info("Creating main users database cc.users") + session.run("CREATE DATABASE cc.users IF NOT EXISTS") + # Wait for database to be available + time.sleep(2) # Give Neo4j time to create the database + + # Verify database exists + result = session.run("SHOW DATABASES") + databases = [record["name"] for record in result] + if "cc.users" not in databases: + logger.error("Failed to create cc.users database") + return False + logger.info("Successfully created cc.users database") + except Exception as e: + logger.error(f"Failed to create cc.users database: {str(e)}") + return False + + # Create user data structure directly + user_data = { + "email": os.getenv('ADMIN_EMAIL'), + "password": os.getenv('ADMIN_PASSWORD'), + "email_confirm": True, + "user_metadata": { + "name": os.getenv('ADMIN_NAME'), + "username": ADMIN_USERNAME, + "full_name": os.getenv('ADMIN_FULL_NAME'), + "display_name": ADMIN_DISPLAY_NAME, + "user_type": "admin" # Set this explicitly for admin + }, + "app_metadata": { + "provider": "email", + "providers": ["email"], + "role": "supabase_admin" + } + } + + # Create user via Auth API + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/auth/v1/admin/users", + headers=self.supabase_headers, + json=user_data + ) + + if response.status_code not in (200, 201): + logger.error(f"Failed to create admin user: {response.text}") + return False + + user_id = response.json().get("id") + logger.info(f"Created auth user with ID: {user_id}") + + # Add a small delay to ensure user is created + time.sleep(2) + + # Call setup_initial_admin function to set admin profile + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/rest/v1/rpc/setup_initial_admin", + headers=self.supabase_headers, + json={ + "admin_email": os.getenv('ADMIN_EMAIL') + } + ) + + if response.status_code not in (200, 201, 204): + logger.error(f"Failed to set up admin profile: {response.text}") + return False + + admin_profile = response.json() + logger.info(f"Updated user profile to admin type: {admin_profile}") + + # Get admin access token + login_data = { + "email": os.getenv('ADMIN_EMAIL'), + "password": os.getenv('ADMIN_PASSWORD') + } + + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/auth/v1/token?grant_type=password", + headers={ + "apikey": os.getenv('SERVICE_ROLE_KEY'), + "Content-Type": "application/json" + }, + json=login_data + ) + + if response.status_code not in (200, 201): + logger.error(f"Failed to get admin access token: {response.text}") + return False + + admin_access_token = response.json().get("access_token") + if not admin_access_token: + logger.error("No access token in response") + return False + + # Store admin user ID and access token in status for later use + self.status["admin_user_id"] = user_id + self.status["admin_access_token"] = admin_access_token + self._save_status(self.status) + + # Create Neo4j database and graph for super admin + try: + # Create the admin's private database and graph + def safe_parse_date(date_str): + """Parse date string in YYYY-MM-DD format, return None if invalid""" + if not date_str: + return None + try: + return datetime.strptime(date_str, '%Y-%m-%d').date() + except ValueError: + logger.warning(f"Invalid date format: {date_str}. Expected YYYY-MM-DD") + return None + + cc_users_db_name = "cc.users" + user_type = "admin" + worker_type = "superadmin" + user_email = os.getenv('SUPER_ADMIN_EMAIL') + worker_email = os.getenv('SUPER_ADMIN_WORKER_EMAIL') + cc_username = os.getenv('SUPER_ADMIN_USERNAME') + user_name = os.getenv('SUPER_ADMIN_NAME') + worker_name = os.getenv('SUPER_ADMIN_DISPLAY_NAME') + calendar_start_date = os.getenv('SUPER_ADMIN_CALENDAR_START_DATE') # Expected format: 2025-01-01 + calendar_end_date = os.getenv('SUPER_ADMIN_CALENDAR_END_DATE') # Expected format: 2025-01-01 + admin_creator = NonSchoolUserCreator( + user_id=user_id, + cc_users_db_name=cc_users_db_name, # Main users database + user_type=user_type, + worker_type=worker_type, + user_email=user_email, + worker_email=worker_email, + cc_username=cc_username, + user_name=user_name, + worker_name=worker_name, + calendar_start_date=safe_parse_date(calendar_start_date), + calendar_end_date=safe_parse_date(calendar_end_date) + ) + + # Create the user nodes and relationships + user_nodes = admin_creator.create_user(access_token=admin_access_token) + logger.info(f"Initialised super admin user nodes: {user_nodes}") + + if not user_nodes.get(f'default_user_node'): + logger.error("Failed to create admin user node in the default database") + return False + if not user_nodes.get(f'private_user_node'): + logger.error(f"Failed to create admin user node in the {admin_creator.user_type} database") + return False + if not user_nodes.get('worker_node'): + logger.error("Failed to create admin super admin node") + return False + + logger.info(f"Created Neo4j nodes for admin user: {user_nodes}") + + except Exception as e: + logger.error(f"Failed to create admin Neo4j database and graph: {str(e)}") + return False + + # Wait for changes to propagate + logger.info("Waiting for changes to propagate...") + time.sleep(2) + + # Verify the setup + if self.check_super_admin_exists(): + logger.info("Super admin exists and is properly configured") + self.status["super_admin_created"] = True + self._save_status(self.status) + logger.info("Super admin created and verified successfully") + return True + else: + logger.error("Failed to verify super admin setup") + return False + + except Exception as e: + logger.error(f"Error creating super admin: {str(e)}") + return False + + def get_admin_token(self) -> bool: + """Get an access token for the admin user""" + if self.status.get("admin_token_obtained"): + logger.info("Admin token already obtained, skipping...") + return True + + logger.info("Getting admin access token...") + + # Add a small delay to ensure auth system is ready + time.sleep(2) + + # Try multiple times with increasing delays + max_retries = 5 + for retry in range(max_retries): + try: + # Sign in with admin credentials + login_data = { + "email": ADMIN_EMAIL, + "password": ADMIN_PASSWORD + } + + logger.info(f"Attempting to login as {ADMIN_EMAIL} (attempt {retry+1}/{max_retries})") + + # Use the retry mechanism but with custom headers + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/auth/v1/token?grant_type=password", + headers={ + "apikey": SERVICE_ROLE_KEY, + "Content-Type": "application/json" + }, + json=login_data + ) + + if response.status_code in (200, 201): + # Extract the access token + self.admin_token = response.json().get("access_token") + + if self.admin_token: + logger.info("Admin token obtained successfully") + self.update_status("admin_token_obtained", True) + return True + else: + logger.error("No access token in response") + else: + logger.error(f"Failed to get admin token (attempt {retry+1}): {response.text}") + + # Increase delay with each retry + wait_time = (retry + 1) * 2 + logger.info(f"Waiting {wait_time} seconds before next attempt...") + time.sleep(wait_time) + + except Exception as e: + logger.error(f"Error getting admin token: {str(e)}") + time.sleep((retry + 1) * 2) + + # If we get here, all retries failed + logger.error("Failed to get admin token after multiple attempts") + + # As a fallback, try to use the service role key directly + logger.info("Falling back to using service role key for API calls") + self.admin_token = SERVICE_ROLE_KEY + self.update_status("admin_token_obtained", True) + return True + + def log_step(self, step: str, success: bool, message: Optional[str] = None) -> None: + """Log a step in the initialization process""" + step_log = { + "step": step, + "success": success, + "timestamp": time.time(), + "message": message + } + if "steps" not in self.status: + self.status["steps"] = [] + self.status["steps"].append(step_log) + self._save_status(self.status) + + if success: + logger.info(f"Step '{step}' completed successfully") + else: + logger.error(f"Step '{step}' failed: {message}") + + def initialize_storage(self) -> bool: + """Initialize storage buckets and policies""" + if self.status["storage"]["initialized"]: + logger.info("Storage already initialized, skipping...") + return True + + logger.info("Initializing storage buckets and policies...") + + try: + # Initialize storage client with admin user ID and access token + admin_user_id = self.status.get("admin_user_id") + admin_access_token = self.status.get("admin_access_token") + if not admin_user_id or not admin_access_token: + raise ValueError("Admin user ID and access token are required for bucket initialization") + + # Create Supabase client with service role access and admin token + supabase = SupabaseServiceRoleClient.for_admin(admin_access_token) + + # Define core buckets + core_buckets = [ + { + "id": "cc.users", + "name": "CC Users", + "public": False, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + }, + { + "id": "cc.institutes", + "name": "CC Institutes", + "public": False, + "file_size_limit": 50 * 1024 * 1024, # 50MB + "allowed_mime_types": [ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + } + ] + + # Create each core bucket + results = [] + for bucket in core_buckets: + try: + # Create bucket with properly typed options + options = CreateBucketOptions( + public=bucket["public"], + file_size_limit=bucket["file_size_limit"], + allowed_mime_types=bucket["allowed_mime_types"], + name=bucket["name"] + ) + result = supabase.create_bucket(bucket["id"], options=options) + results.append({ + "bucket": bucket["id"], + "status": "success", + "result": result + }) + except Exception as e: + logger.error(f"Error creating bucket {bucket['id']}: {str(e)}") + results.append({ + "bucket": bucket["id"], + "status": "error", + "error": str(e) + }) + + # Update status based on results + for result in results: + bucket_id = result["bucket"] + success = result["status"] == "success" + self.status["storage"]["buckets"][bucket_id] = success + if success: + self.log_step(f"storage_bucket_{bucket_id}", True, "Bucket created successfully") + else: + self.log_step(f"storage_bucket_{bucket_id}", False, f"Failed to create bucket {bucket_id}: {result.get('error', 'Unknown error')}") + return False + + # Check if all buckets were created successfully + if all(self.status["storage"]["buckets"].values()): + logger.info("Storage initialization completed successfully") + self.status["storage"]["initialized"] = True + self._save_status(self.status) + return True + else: + logger.error("Some buckets failed to initialize") + return False + + except Exception as e: + self.log_step("storage_initialization", False, str(e)) + return False + + def create_schools_database(self) -> bool: + """Create the schools Neo4j database""" + if self.status.get("schools_db_created"): + logger.info("Schools database already created, skipping...") + return True + + logger.info("Creating schools Neo4j database...") + + # For now, we'll just mark this as done since we can't easily create the Neo4j database directly + # In a production environment, you would need to use the Neo4j Admin API or a direct connection + logger.info("Schools database creation marked as completed") + self.update_status("schools_db_created", True) + return True + + def initialize_schema(self) -> bool: + """Initialize Neo4j schema (constraints and indexes)""" + if self.status.get("schema_initialized"): + logger.info("Schema already initialized, skipping...") + return True + + logger.info("Initializing Neo4j schema...") + + # For now, we'll just mark this as done since we can't easily initialize the schema directly + # In a production environment, you would need to use the Neo4j Cypher API or a direct connection + logger.info("Schema initialization marked as completed") + self.update_status("schema_initialized", True) + return True + + def import_sample_schools(self) -> bool: + """Import sample schools data""" + if self.status.get("neo4j", {}).get("schools_imported"): + logger.info("Sample schools already imported, skipping...") + return True + + logger.info("Importing sample schools data...") + + try: + # Ensure Supabase headers are properly set + self._ensure_supabase_headers() + + # Check if schools CSV exists + csv_path = os.path.join(self.init_dir, "sample_schools.csv") + if not os.path.exists(csv_path): + logger.warning("No schools CSV file found, skipping import") + self.status["neo4j"]["schools_imported"] = True + self._save_status(self.status) + return True + + # Read and parse the CSV file + with open(csv_path, "r") as f: + csv_reader = csv.DictReader(f) + schools = list(csv_reader) + + logger.info(f"Found {len(schools)} schools in CSV file") + + # Add a date format conversion function + def convert_date_format(date_str: str) -> Optional[str]: + """Convert date from DD-MM-YYYY to YYYY-MM-DD format""" + if not date_str or date_str == "": + return None + try: + if "-" in date_str: + day, month, year = date_str.split("-") + return f"{year}-{month}-{day}" + return None + except: + return None + + # Import each school + success_count = 0 + for school in schools: + try: + # Format the school data + school_data = { + "urn": school.get("URN"), + "establishment_name": school.get("EstablishmentName"), + "la_code": school.get("LA (code)"), + "la_name": school.get("LA (name)"), + "establishment_number": school.get("EstablishmentNumber"), + "establishment_type": school.get("TypeOfEstablishment (name)"), + "establishment_type_group": school.get("EstablishmentTypeGroup (name)"), + "establishment_status": school.get("EstablishmentStatus (name)"), + "reason_establishment_opened": school.get("ReasonEstablishmentOpened (name)"), + "open_date": school.get("OpenDate"), + "reason_establishment_closed": school.get("ReasonEstablishmentClosed (name)"), + "close_date": school.get("CloseDate"), + "phase_of_education": school.get("PhaseOfEducation (name)"), + "statutory_low_age": school.get("StatutoryLowAge"), + "statutory_high_age": school.get("StatutoryHighAge"), + "boarders": school.get("Boarders (name)"), + "nursery_provision": school.get("NurseryProvision (name)"), + "official_sixth_form": school.get("OfficialSixthForm (name)"), + "gender": school.get("Gender (name)"), + "religious_character": school.get("ReligiousCharacter (name)"), + "religious_ethos": school.get("ReligiousEthos (name)"), + "diocese": school.get("Diocese (name)"), + "admissions_policy": school.get("AdmissionsPolicy (name)"), + "school_capacity": school.get("SchoolCapacity"), + "special_classes": school.get("SpecialClasses (name)"), + "census_date": school.get("CensusDate"), + "number_of_pupils": school.get("NumberOfPupils"), + "number_of_boys": school.get("NumberOfBoys"), + "number_of_girls": school.get("NumberOfGirls"), + "percentage_fsm": school.get("PercentageFSM"), + "trust_school_flag": school.get("TrustSchoolFlag (name)"), + "trusts_name": school.get("Trusts (name)"), + "school_sponsor_flag": school.get("SchoolSponsorFlag (name)"), + "school_sponsors_name": school.get("SchoolSponsors (name)"), + "federation_flag": school.get("FederationFlag (name)"), + "federations_name": school.get("Federations (name)"), + "ukprn": school.get("UKPRN"), + "fehe_identifier": school.get("FEHEIdentifier"), + "further_education_type": school.get("FurtherEducationType (name)"), + "ofsted_last_inspection": school.get("OfstedLastInsp"), + "last_changed_date": school.get("LastChangedDate"), + "street": school.get("Street"), + "locality": school.get("Locality"), + "address3": school.get("Address3"), + "town": school.get("Town"), + "county": school.get("County (name)"), + "postcode": school.get("Postcode"), + "school_website": school.get("SchoolWebsite"), + "telephone_num": school.get("TelephoneNum"), + "head_title": school.get("HeadTitle (name)"), + "head_first_name": school.get("HeadFirstName"), + "head_last_name": school.get("HeadLastName"), + "head_preferred_job_title": school.get("HeadPreferredJobTitle"), + "gssla_code": school.get("GSSLACode (name)"), + "parliamentary_constituency": school.get("ParliamentaryConstituency (name)"), + "urban_rural": school.get("UrbanRural (name)"), + "rsc_region": school.get("RSCRegion (name)"), + "country": school.get("Country (name)"), + "uprn": school.get("UPRN"), + "sen_stat": school.get("SENStat") == "true", + "sen_no_stat": school.get("SENNoStat") == "true", + "sen_unit_on_roll": school.get("SenUnitOnRoll"), + "sen_unit_capacity": school.get("SenUnitCapacity"), + "resourced_provision_on_roll": school.get("ResourcedProvisionOnRoll"), + "resourced_provision_capacity": school.get("ResourcedProvisionCapacity") + } + + # Update the data type conversion section + for key, value in school_data.items(): + if value == "": + school_data[key] = None + elif key in ["statutory_low_age", "statutory_high_age", "school_capacity", + "number_of_pupils", "number_of_boys", "number_of_girls", + "sen_unit_on_roll", "sen_unit_capacity", + "resourced_provision_on_roll", "resourced_provision_capacity"]: + try: + if value is not None and value != "": + school_data[key] = int(float(value)) # Handle both integer and decimal strings + except (ValueError, TypeError): + school_data[key] = None + elif key in ["percentage_fsm"]: + try: + if value is not None and value != "": + school_data[key] = float(value) + except (ValueError, TypeError): + school_data[key] = None + elif key in ["open_date", "close_date", "census_date", "ofsted_last_inspection", "last_changed_date"]: + if value and value != "": + # Convert date format + converted_date = convert_date_format(value) + if converted_date: + school_data[key] = converted_date + else: + school_data[key] = None + else: + school_data[key] = None + + # Insert the school into the institute_imports table + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/rest/v1/institute_imports", + headers=self.supabase_headers, + json=school_data + ) + + if response.status_code in (200, 201): + try: + response_data = response.json() + success_count += 1 + logger.info(f"Successfully imported school {school.get('URN')}: {school.get('EstablishmentName')}") + except json.JSONDecodeError as e: + # If we can't parse the response but the status code was successful, consider it a success + if response.status_code in (200, 201): + success_count += 1 + logger.info(f"Successfully imported school {school.get('URN')}: {school.get('EstablishmentName')} (response not JSON)") + else: + logger.error(f"Failed to parse response for school {school.get('URN')}: {e}, Response: {response.text}") + else: + logger.error(f"Failed to import school {school.get('URN')}: {response.text}") + + except Exception as e: + logger.error(f"Error importing school {school.get('URN')}: {str(e)}") + + logger.info(f"Successfully imported {success_count} out of {len(schools)} schools") + + # Mark as completed even if some schools failed + self.status["neo4j"]["schools_imported"] = True + self._save_status(self.status) + return True + + except Exception as e: + logger.error(f"Error importing sample schools: {str(e)}") + return False + + def initialize_calendar_database(self) -> bool: + """Initialize the main calendar database""" + if self.status.get("calendar_database", {}).get("initialized"): + logger.info("Calendar database already initialized, skipping...") + return True + + try: + # Initialize the status dictionaries if they don't exist + self.status.setdefault("calendar_database", {}) + self.status.setdefault("cc_database", {}) + + # 1. Create main calendar database + logger.info("Creating main calendar database...") + result = self.neo4j_service.create_database("cc.calendar") + if result["status"] != "success": + self.log_step("calendar_database_creation", False, result["message"]) + return False + + self.status["calendar_database"]["database_created"] = True + self.log_step("calendar_database_creation", True) + + # 2. Create the cc.calendar storage bucket + logger.info("Creating cc.calendar storage bucket...") + admin_access_token = self.status.get("admin_access_token") + if not admin_access_token: + self.log_step("calendar_storage_creation", False, "Admin access token not found") + return False + + storage_client = SupabaseServiceRoleClient.for_admin(admin_access_token) + try: + bucket_options = CreateBucketOptions( + name="CC Calendar Files", + public=True, + file_size_limit=50 * 1024 * 1024, # 50MB + allowed_mime_types=[ + 'image/*', 'video/*', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats-officedocument.*', + 'text/plain', 'text/csv', 'application/json' + ] + ) + storage_client.create_bucket("cc.calendar", bucket_options) + self.status["calendar_database"]["storage_created"] = True + self.log_step("calendar_storage_creation", True) + except Exception as e: + self.log_step("calendar_storage_creation", False, str(e)) + return False + + # 3. Initialize schema on calendar database + logger.info("Initializing Neo4j schema on calendar database...") + result = self.neo4j_service.initialize_schema("cc.calendar") + if result["status"] != "success": + self.log_step("calendar_schema_initialization", False, result["message"]) + return False + + self.status["calendar_database"]["schema_initialized"] = True + self.log_step("calendar_schema_initialization", True) + + # 4. Create test calendar + logger.info("Creating test calendar...") + start_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + end_date = start_date + timedelta(days=90) # 3 months + + calendar_nodes = create_calendar( + db_name="cc.calendar", + start_date=start_date, + end_date=end_date, + attach_to_calendar_node=True, + owner_node=None, + time_chunk_node_length=0 + ) + + if calendar_nodes: + self.status["cc_database"]["calendar_created"] = True + self.log_step("cc_calendar_creation", True) + else: + self.log_step("cc_calendar_creation", False, "Failed to create calendar") + return False + + self.status["calendar_database"]["initialized"] = True + self.status["cc_database"]["initialized"] = True + self._save_status(self.status) + return True + + except Exception as e: + self.log_step("cc_database_initialization", False, str(e)) + return False + + def initialize_neo4j(self) -> bool: + """Initialize Neo4j databases and schema""" + if self.status["neo4j"]["initialized"]: + logger.info("Neo4j already initialized, skipping...") + return True + + try: + # 1. Create main schools database + logger.info("Creating main schools database...") + result = self.neo4j_service.create_database("cc.institutes") + if result["status"] != "success": + self.log_step("neo4j_database_creation", False, result["message"]) + return False + + self.status["neo4j"]["database_created"] = True + self.log_step("neo4j_database_creation", True) + + # 2. Initialize schema on the custom database + logger.info("Initializing Neo4j schema on cc.institutes database...") + result = self.neo4j_service.initialize_schema("cc.institutes") + if result["status"] != "success": + self.log_step("neo4j_schema_initialization", False, result["message"]) + return False + + self.status["neo4j"]["schema_initialized"] = True + self.log_step("neo4j_schema_initialization", True) + + self.status["neo4j"]["initialized"] = True + self._save_status(self.status) + return True + + except Exception as e: + self.log_step("neo4j_initialization", False, str(e)) + return False + + def initialize_default_school(self) -> bool: + """Initialize the default school structure with public and private databases""" + if self.status.get("default_school", {}).get("initialized"): + logger.info("Default school already initialized, skipping...") + return True + + try: + # Default school configuration + school_config = { + "id": "default", + "type": "development", + "name": "Default School", + "website": "https://example.com", + "timetable_file": "default_institute/default_timetable.xlsx", + "curriculum_file": "default_institute/default_curriculum.xlsx" + } + + # Ensure Supabase headers are properly set + self._ensure_supabase_headers() + + # Define database names following the convention + base_path = f"cc.institutes.{school_config['type']}.{school_config['id']}" + public_db_name = base_path + private_db_name = base_path # Removed .private suffix + curriculum_db_name = f"{base_path}.curriculum" + + # Create public school node first + logger.info("Creating public school node...") + public_school_node = SchoolNode( + unique_id=f'School_{school_config["id"]}', + tldraw_snapshot="", + id=school_config["id"], + school_type=school_config["type"], + name=school_config["name"], + website=school_config["website"] + ) + + # 1. Create storage buckets for school + logger.info("Creating storage buckets for default school...") + admin_access_token = self.status.get("admin_access_token") + if not admin_access_token: + self.log_step("default_school_storage", False, "Admin access token not found") + return False + + bucket_results = init_school.create_school_buckets( + school_id=school_config["id"], + school_type=school_config["type"], + school_name=school_config["name"], + admin_access_token=admin_access_token + ) + + # Check if all buckets were created successfully + expected_buckets = [ + f"{base_path}.public", # Public bucket + f"{base_path}.private", # Private bucket + f"{base_path}.curriculum.public", # Public curriculum bucket + f"{base_path}.curriculum.private" # Private curriculum bucket + ] + + for bucket_id in expected_buckets: + if bucket_id not in bucket_results or bucket_results[bucket_id]["status"] != "success": + self.log_step("default_school_storage", False, f"Failed to create bucket {bucket_id}") + return False + + self.status.setdefault("default_school", {})["storage_created"] = True + self.log_step("default_school_storage", True) + + # 2. Create school node in public database + result = init_school.create_school( + db_name=public_db_name, + id=school_config["id"], + name=school_config["name"], + website=school_config["website"], + school_type=school_config["type"], + is_public=True, + school_node=public_school_node + ) + + if not result: + self.log_step("default_school_public_creation", False, "Failed to create public school node") + return False + + self.status.setdefault("default_school", {})["public_created"] = True + self.log_step("default_school_public_creation", True) + + # 3. Create private school database and node + logger.info(f"Creating private school database: {private_db_name}") + result = self.neo4j_service.create_database(private_db_name) + if result["status"] != "success": + self.log_step("default_school_private_creation", False, result["message"]) + return False + + # Create private school node with more details + logger.info("Creating private school node...") + private_school_node = SchoolNode( + unique_id=f'School_{school_config["id"]}', + tldraw_snapshot="", + id=school_config["id"], + school_type=school_config["type"], + name=school_config["name"], + website=school_config["website"], + # Add required private fields with default values + establishment_number="0000", + establishment_name=school_config["name"], + establishment_type="Development", + establishment_status="Open", + phase_of_education="All", + statutory_low_age=11, + statutory_high_age=18, + school_capacity=1000 + ) + + # Create school node in private database + result = init_school.create_school( + db_name=private_db_name, + id=school_config["id"], + name=school_config["name"], + website=school_config["website"], + school_type=school_config["type"], + is_public=False, + school_node=private_school_node + ) + + if not result: + self.log_step("default_school_private_creation", False, "Failed to create private school node") + return False + + # Create curriculum database + logger.info(f"Creating curriculum database: {curriculum_db_name}") + result = self.neo4j_service.create_database(curriculum_db_name) + if result["status"] != "success": + self.log_step("default_school_curriculum_creation", False, result["message"]) + return False + + # 4. Import timetable data + logger.info("Importing timetable data...") + timetable_path = os.path.join(self.init_dir, school_config["timetable_file"]) + if not os.path.exists(timetable_path): + self.log_step("default_school_timetable", False, f"Timetable file not found: {timetable_path}") + return False + + school_timetable_dataframes = xl.create_dataframes(timetable_path) + init_school_timetable.create_school_timetable( + dataframes=school_timetable_dataframes, + db_name=private_db_name, + school_node=private_school_node + ) + self.status["default_school"]["timetable_imported"] = True + self.log_step("default_school_timetable", True) + + # 5. Import curriculum data + logger.info("Importing curriculum data...") + curriculum_path = os.path.join(self.init_dir, school_config["curriculum_file"]) + if not os.path.exists(curriculum_path): + self.log_step("default_school_curriculum", False, f"Curriculum file not found: {curriculum_path}") + return False + + school_curriculum_dataframes = xl.create_dataframes(curriculum_path) + init_school_curriculum.create_curriculum( + dataframes=school_curriculum_dataframes, + db_name=private_db_name, + curriculum_db_name=curriculum_db_name, + school_node=private_school_node + ) + self.status["default_school"]["curriculum_imported"] = True + self.log_step("default_school_curriculum", True) + + # 6. Add the default school to Supabase institutes table + logger.info("Adding default school to Supabase institutes table...") + try: + # Check if the school already exists in the institutes table + response = self._supabase_request_with_retry( + 'get', + f"{SUPABASE_URL}/rest/v1/institutes", + headers=self.supabase_headers, + params={ + "select": "*", + "name": f"eq.{school_config['name']}" + } + ) + + if response.status_code != 200: + logger.error(f"Failed to check institutes table: {response.text}") + self.log_step("default_school_supabase", False, f"Failed to check institutes table: {response.text}") + return False + + existing_institutes = response.json() + institute_id = None + + if existing_institutes and len(existing_institutes) > 0: + # School already exists, use its ID + institute_id = existing_institutes[0]["id"] + logger.info(f"Default school already exists in institutes table with ID: {institute_id}") + else: + # Create the school in the institutes table + address_json = {"street": "123 Dev Street", "city": "Development City", "postcode": "DEV123"} + metadata_json = { + "school_type": school_config["type"], + "id": school_config["id"] + } + + school_data = { + "name": school_config["name"], + "urn": f"DEV-{school_config['id']}", + "status": "active", + "website": school_config["website"], + "address": json.dumps(address_json), + "metadata": json.dumps(metadata_json), + "neo4j_unique_id": private_school_node.unique_id, + "neo4j_public_sync_status": "synced", + "neo4j_public_sync_at": datetime.now().isoformat(), + "neo4j_private_sync_status": "synced", + "neo4j_private_sync_at": datetime.now().isoformat() + } + + # Log the request data for debugging + logger.debug(f"Sending request to create institute with data: {school_data}") + + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/rest/v1/institutes", + headers=self.supabase_headers, + json=school_data + ) + + if response.status_code not in (200, 201): + logger.error(f"Failed to add default school to institutes table: {response.text}") + self.log_step("default_school_supabase", False, f"Failed to add default school to institutes table: {response.text}") + return False + + # Handle the response more carefully + try: + response_data = response.json() + if isinstance(response_data, list) and len(response_data) > 0: + institute_id = response_data[0]["id"] + elif isinstance(response_data, dict) and "id" in response_data: + institute_id = response_data["id"] + else: + logger.error(f"Unexpected response format: {response_data}") + self.log_step("default_school_supabase", False, f"Unexpected response format: {response_data}") + return False + + logger.info(f"Added default school to institutes table with ID: {institute_id}") + except json.JSONDecodeError as e: + logger.error(f"Failed to parse response from institutes table: {e}, Response: {response.text}") + self.log_step("default_school_supabase", False, f"Failed to parse response from institutes table: {e}") + # If we can't parse the response but the status code was successful, try to continue + # by querying for the school we just created + try: + # Query for the school we just created + query_response = self._supabase_request_with_retry( + 'get', + f"{SUPABASE_URL}/rest/v1/institutes", + headers=self.supabase_headers, + params={ + "select": "*", + "name": f"eq.{school_config['name']}" + } + ) + + if query_response.status_code == 200: + query_data = query_response.json() + if query_data and len(query_data) > 0: + institute_id = query_data[0]["id"] + logger.info(f"Retrieved default school from institutes table with ID: {institute_id}") + else: + logger.error("School was created but could not be retrieved") + self.log_step("default_school_supabase", False, "School was created but could not be retrieved") + return False + else: + logger.error(f"Failed to retrieve created school: {query_response.text}") + self.log_step("default_school_supabase", False, f"Failed to retrieve created school: {query_response.text}") + return False + except Exception as query_error: + logger.error(f"Error retrieving created school: {str(query_error)}") + self.log_step("default_school_supabase", False, f"Error retrieving created school: {str(query_error)}") + return False + + # 7. Add super admin to the default school in Neo4j + logger.info("Adding super admin to default school in Neo4j...") + + # Get the super admin worker node from the status + admin_user_id = self.status.get("admin_user_id") + if not admin_user_id: + logger.error("Admin user ID not found in status") + self.log_step("default_school_admin", False, "Admin user ID not found in status") + return False + + # Initialize Neontology connection + neon.init_neontology_connection() + + # Get the super admin worker node from the admin's private database + admin_db_name = f"cc.users.admin.{os.getenv('SUPER_ADMIN_USERNAME', 'superadmin')}" + + # First, update the worker_db_name property in the super admin node to include the default school + with get_session(database=admin_db_name) as session: + # Get the super admin worker node + result = session.run( + """ + MATCH (n:SuperAdmin {unique_id: $unique_id}) + RETURN n + """, + unique_id=f"SuperAdmin_{admin_user_id}" + ) + + admin_node_record = result.single() + if not admin_node_record: + logger.error(f"Super admin node not found in database {admin_db_name}") + self.log_step("default_school_admin", False, f"Super admin node not found in database {admin_db_name}") + return False + + admin_node = admin_node_record["n"] + + # Update the worker_db_name property to include the default school database + worker_db_name = f"{private_db_name}" + + # Update the worker_db_name property + session.run( + """ + MATCH (n:SuperAdmin {unique_id: $unique_id}) + SET n.worker_db_name = $worker_db_name + """, + unique_id=f"SuperAdmin_{admin_user_id}", + worker_db_name=worker_db_name, + database=admin_db_name + ) + + logger.info(f"Updated super admin worker_db_name to {worker_db_name} in {admin_db_name}") + + # Create a copy of the super admin node in the default school database + from modules.database.schemas.nodes.workers.workers import SuperAdminNode + + # Create the super admin node in the default school database + super_admin_node = SuperAdminNode( + unique_id=f"SuperAdmin_{admin_user_id}", + tldraw_snapshot="", + worker_name=os.getenv('SUPER_ADMIN_DISPLAY_NAME', 'Super Admin'), + worker_email=os.getenv('SUPER_ADMIN_WORKER_EMAIL') or os.getenv('ADMIN_EMAIL', 'admin@example.com'), + worker_db_name=worker_db_name, + worker_type="superadmin" + ) + + # Create the super admin node in the default school database + neon.create_or_merge_neontology_node( + node=super_admin_node, + database=private_db_name, + operation='merge' + ) + + logger.info(f"Created super admin node in default school database {private_db_name}") + + # Create the necessary structure nodes for the school if they don't exist + logger.info("Creating structure nodes for the default school...") + + # Create Staff Structure Node + staff_structure_node = school_structures.StaffStructureNode( + unique_id=f"StaffStructure_{school_config['id']}", + tldraw_snapshot="" + ) + + neon.create_or_merge_neontology_node( + node=staff_structure_node, + database=private_db_name, + operation='merge' + ) + + # Create Student Structure Node + student_structure_node = school_structures.StudentStructureNode( + unique_id=f"StudentStructure_{school_config['id']}", + tldraw_snapshot="" + ) + + neon.create_or_merge_neontology_node( + node=student_structure_node, + database=private_db_name, + operation='merge' + ) + + # Create IT Admin Structure Node + it_admin_structure_node = school_structures.ITAdminStructureNode( + unique_id=f"ITAdminStructure_{school_config['id']}", + tldraw_snapshot="" + ) + + neon.create_or_merge_neontology_node( + node=it_admin_structure_node, + database=private_db_name, + operation='merge' + ) + + # Create relationships between school and structure nodes + # School has Staff Structure + school_staff_rel = structure_relationships.SchoolHasStaffStructure( + source=private_school_node, + target=staff_structure_node + ) + + neon.create_or_merge_neontology_relationship( + relationship=school_staff_rel, + database=private_db_name, + operation='merge' + ) + + # School has Student Structure + school_student_rel = structure_relationships.SchoolHasStudentStructure( + source=private_school_node, + target=student_structure_node + ) + + neon.create_or_merge_neontology_relationship( + relationship=school_student_rel, + database=private_db_name, + operation='merge' + ) + + # School has IT Admin Structure + school_it_admin_rel = structure_relationships.SchoolHasITAdminStructure( + source=private_school_node, + target=it_admin_structure_node + ) + + neon.create_or_merge_neontology_relationship( + relationship=school_it_admin_rel, + database=private_db_name, + operation='merge' + ) + + # Connect super admin to IT Admin Structure + admin_structure_rel = structure_relationships.SuperAdminBelongsToITAdminStructure( + source=super_admin_node, + target=it_admin_structure_node + ) + + neon.create_or_merge_neontology_relationship( + relationship=admin_structure_rel, + database=private_db_name, + operation='merge' + ) + + logger.info("Created structure nodes and relationships for default school") + + # 8. Add super admin to the institute_memberships table in Supabase + logger.info("Adding super admin to institute_memberships table...") + + # Check if the membership already exists + response = self._supabase_request_with_retry( + 'get', + f"{SUPABASE_URL}/rest/v1/institute_memberships", + headers=self.supabase_headers, + params={ + "select": "*", + "profile_id": f"eq.{admin_user_id}", + "institute_id": f"eq.{institute_id}" + } + ) + + if response.status_code != 200: + logger.error(f"Failed to check institute_memberships table: {response.text}") + self.log_step("default_school_admin_membership", False, f"Failed to check institute_memberships table: {response.text}") + return False + + try: + existing_memberships = response.json() + + if not existing_memberships or len(existing_memberships) == 0: + # Create the membership + membership_data = { + "profile_id": admin_user_id, + "institute_id": institute_id, + "role": "admin", + "tldraw_preferences": "{}", # Use string instead of json.dumps({}) + "metadata": json.dumps({ + "worker_type": "superadmin", + "neo4j_unique_id": f"SuperAdmin_{admin_user_id}" + }) + } + + # Log the request data for debugging + logger.debug(f"Sending request to create institute membership with data: {membership_data}") + + response = self._supabase_request_with_retry( + 'post', + f"{SUPABASE_URL}/rest/v1/institute_memberships", + headers=self.supabase_headers, + json=membership_data + ) + + if response.status_code not in (200, 201): + logger.error(f"Failed to add super admin to institute_memberships table: {response.text}") + self.log_step("default_school_admin_membership", False, f"Failed to add super admin to institute_memberships table: {response.text}") + return False + + logger.info("Added super admin to institute_memberships table") + else: + logger.info("Super admin already exists in institute_memberships table") + except json.JSONDecodeError as e: + logger.error(f"Failed to parse response from institute_memberships table: {e}, Response: {response.text}") + self.log_step("default_school_admin_membership", False, f"Failed to parse response from institute_memberships table: {e}") + return False + + self.status["default_school"]["supabase_updated"] = True + self.log_step("default_school_supabase", True) + + except Exception as e: + logger.error(f"Error updating Supabase tables: {str(e)}") + self.log_step("default_school_supabase", False, f"Error updating Supabase tables: {str(e)}") + return False + + # Mark completion + self.status["default_school"]["initialized"] = True + self._save_status(self.status) + return True + + except Exception as e: + self.log_step("default_school_initialization", False, str(e)) + return False + + def _ensure_supabase_headers(self): + """Ensure Supabase headers are properly set""" + if not self.supabase_headers or 'apikey' not in self.supabase_headers: + logger.info("Initializing Supabase headers") + self.supabase_headers = { + "apikey": SERVICE_ROLE_KEY, + "Authorization": f"Bearer {SERVICE_ROLE_KEY}", + "Content-Type": "application/json", + "Prefer": "return=representation" # This ensures Supabase returns the created record + } + elif 'Prefer' not in self.supabase_headers: + # Add the Prefer header if it's missing + self.supabase_headers['Prefer'] = "return=representation" + + logger.debug(f"Using Supabase headers: {self.supabase_headers}") + + def _supabase_request_with_retry(self, method, url, **kwargs): + """Make a request to Supabase with retry logic""" + max_retries = 3 + retry_delay = 2 # seconds + + for attempt in range(max_retries): + try: + if method.lower() == 'get': + response = requests.get(url, **kwargs) + elif method.lower() == 'post': + response = requests.post(url, **kwargs) + elif method.lower() == 'put': + response = requests.put(url, **kwargs) + elif method.lower() == 'delete': + response = requests.delete(url, **kwargs) + else: + raise ValueError(f"Unsupported HTTP method: {method}") + + # If successful or client error (4xx), don't retry + if response.status_code < 500: + return response + + # Server error (5xx), retry after delay + logger.warning(f"Supabase server error (attempt {attempt+1}/{max_retries}): {response.status_code} - {response.text}") + time.sleep(retry_delay * (attempt + 1)) # Exponential backoff + + except requests.RequestException as e: + logger.warning(f"Supabase request exception (attempt {attempt+1}/{max_retries}): {str(e)}") + if attempt == max_retries - 1: + raise + time.sleep(retry_delay * (attempt + 1)) + + # If we get here, all retries failed with server errors + raise requests.RequestException(f"Failed after {max_retries} attempts to {method} {url}") + + def check_completion(self) -> bool: + """Check if all initialization steps are complete""" + try: + # Initialize required status dictionaries if they don't exist + self.status.setdefault("cc_database", {}) + self.status.setdefault("default_school", {}) + + # Add default school check to existing checks + if not (self.status["super_admin_created"] and + self.status["admin_token_obtained"] and + self.status["storage"]["initialized"] and + self.status["neo4j"]["initialized"] and + self.status["neo4j"]["schools_imported"] and + self.status["cc_database"].get("initialized", False) and + self.status["default_school"].get("initialized", False)): + return False + + # Check cc.institutes database + result = self.neo4j_service.check_database_exists("cc.institutes") + if not result.get("exists"): + logger.warning("cc.institutes database does not exist") + self.status["neo4j"]["initialized"] = False + self._save_status(self.status) + return False + + # Check cc database and calendar + result = self.neo4j_service.check_database_exists("cc") + if not result.get("exists"): + logger.warning("cc database does not exist") + self.status["cc_database"]["initialized"] = False + self._save_status(self.status) + return False + + # Verify calendar exists in cc database + try: + result = self.neo4j_service.check_node_exists("cc", "Calendar") + if not result["exists"]: + logger.warning("No calendar found in cc database") + self.status["cc_database"]["calendar_created"] = False + self._save_status(self.status) + return False + except Exception as e: + logger.error(f"Error checking calendar: {str(e)}") + return False + + return True + + except Exception as e: + logger.error(f"Error checking completion: {str(e)}") + return False + + def run(self) -> bool: + """Run the complete initialization process""" + # Check if super admin initialization is required + init_super_admin = os.getenv("SUPER_ADMIN_CHECK", "true").lower() == "true" + + if init_super_admin: + if self.check_completion(): + logger.info("System already initialized, skipping...") + return True + else: + logger.info("Skipping super admin check due to INIT_SUPER_ADMIN being false") + + # Wait for services + if not self.wait_for_services(): + return False + + # Ensure Supabase headers are properly set + self._ensure_supabase_headers() + + # Run initialization steps in order + steps = [ + self.create_super_admin if init_super_admin else lambda: True, + self.get_admin_token, + self.initialize_storage, + self.initialize_neo4j, + self.import_sample_schools, + self.initialize_calendar_database, + self.initialize_default_school + ] + + success = True + for step in steps: + if not step(): + success = False + break + + if success: + logger.info("System initialization completed successfully") + self.status["completed"] = True + self.status["timestamp"] = time.time() + self._save_status(self.status) + else: + logger.error("System initialization failed") + + return success + +if __name__ == "__main__": + init_system = InitializationSystem() + success = init_system.run() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/run/initialization/manager.py b/run/initialization/manager.py new file mode 100644 index 0000000..b3f84eb --- /dev/null +++ b/run/initialization/manager.py @@ -0,0 +1,146 @@ +""" +Initialization manager for ClassroomCopilot +""" +import os +import json +from typing import Dict +import requests + +from modules.logger_tool import initialise_logger + +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) + +class InitializationManager: + def __init__(self): + self.init_dir = "/init" + self.status_file = os.path.join(self.init_dir, "status.json") + self.data_dir = os.path.join(self.init_dir, "data") + self.supabase_url = os.getenv("SUPABASE_URL") + + # Ensure directories exist + os.makedirs(self.init_dir, exist_ok=True) + os.makedirs(self.data_dir, exist_ok=True) + + self.supabase_headers = { + "apikey": os.getenv("SERVICE_ROLE_KEY"), + "Authorization": f"Bearer {os.getenv('SERVICE_ROLE_KEY')}", + "Content-Type": "application/json" + } + + # Define default status structure + self.default_status = { + "super_admin_created": False, + "admin_token_obtained": False, + "storage": { + "initialized": False, + "buckets": { + "cc.users": False, + "cc.institutes": False + } + }, + "neo4j": { + "initialized": False, + "database_created": False, + "schema_initialized": False, + "schools_imported": False + }, + "completed": False, + "timestamp": None, + "steps": [] + } + + self.status = self._load_status() + + def _load_status(self) -> Dict: + """Load or create initialization status""" + try: + with open(self.status_file, "r") as f: + status = json.load(f) + + # Update with any missing keys + def update_dict(current: Dict, default: Dict) -> Dict: + for key, value in default.items(): + if key not in current: + current[key] = value + elif isinstance(value, dict) and isinstance(current[key], dict): + current[key] = update_dict(current[key], value) + return current + + status = update_dict(status, self.default_status) + self._save_status(status) + return status + + except (FileNotFoundError, json.JSONDecodeError): + self._save_status(self.default_status) + return self.default_status.copy() + + def _save_status(self, status: Dict) -> None: + """Save status to file""" + os.makedirs(os.path.dirname(self.status_file), exist_ok=True) + with open(self.status_file, "w") as f: + json.dump(status, f, indent=2) + + def check_admin_exists(self) -> bool: + """Check if super admin already exists""" + try: + responseURL = f"{self.supabase_url}/auth/v1/admin/users" + logger.info(f"Checking admin existence at: {responseURL}") + response = requests.get( + responseURL, + headers=self.supabase_headers + ) + + if response.status_code != 200: + logger.error(f"Error checking admin existence: {response.status_code}") + return False + + data = response.json() + # Fix: response format is {'users': [...], 'aud': 'authenticated'} + users = data.get('users', []) + if not isinstance(users, list): + logger.error(f"Unexpected users format: {users}") + return False + + admin_email = os.getenv('ADMIN_EMAIL') + + # Check for admin in users + admin_user = next( + (user for user in users + if user.get("email") == admin_email + and user.get("app_metadata", {}).get("role") == "supabase_admin"), + None + ) + + if admin_user: + logger.info(f"Super admin {admin_email} already exists") + return True + + return False + + except Exception as e: + logger.error(f"Error checking admin existence: {str(e)}") + return False + + def check_initialization_needed(self) -> bool: + """Check if initialization is needed""" + # First check if admin exists + if self.check_admin_exists(): + logger.info("Super admin exists, skipping initialization") + return False + + # Then check status file + if self.status.get("completed"): + logger.info("Initialization already completed") + return False + + # Check if any step needs completion + incomplete = not all( + v for k, v in self.status.items() + if k not in ("timestamp", "steps") + ) + + if incomplete: + logger.info("Incomplete initialization detected") + return True + + return False \ No newline at end of file diff --git a/run/routers.py b/run/routers.py new file mode 100644 index 0000000..5270183 --- /dev/null +++ b/run/routers.py @@ -0,0 +1,88 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) +from fastapi import FastAPI + +# Import all routers here +from routers.health import router as health_router +from routers.msgraph import router_onenote +from routers.dev.tests import timetable_test +from routers.admin_routes import router as admin_routes_router +from routers.database.init import entity_init, calendar, timetables, curriculum, get_data, schools +from routers.database.tools import get_nodes, get_nodes_and_edges, tldraw_filesystem, get_events, calendar_structure_router, default_nodes_router, worker_structure_router +from routers.assets import powerpoint, word, pdf +from routers.llm.private.ollama import ollama +from routers.llm.public.openai import openai +from routers.connections.arbor_router import router as arbor_router +from routers.langchain.neo4j_graph_qa import router as graph_qa_router +from routers.langchain.interactive_langgraph_query import router as interactive_langgraph_query_router +from routers.rpi import rpi_whisperlive_client +from routers.external import youtube +from routers.solid.pod_provisioner import router as solid_pod_router +from routers.dev.document_conversion import router as document_conversion_router +from routers.dev.test_analysis import router as test_analysis_router + +def register_routes(app: FastAPI): + logger.info("Starting to register routes...") + + # Health check route + app.include_router(health_router, prefix="/health", tags=["Health"]) + + # Admin Routes + app.include_router(admin_routes_router, prefix="/admin", tags=["Admin"]) + + # Microsoft Graph Routes + app.include_router(router_onenote.router, prefix="/msgraph", tags=["Microsoft Graph"]) + + # Database Routes + app.include_router(get_data.router, prefix="/database/upload", tags=["Upload"]) + app.include_router(get_events.router, prefix="/calendar", tags=["Calendar"]) + app.include_router(get_nodes.router, prefix="/database/tools", tags=["Tools"]) + app.include_router(get_nodes_and_edges.router, prefix="/database/tools", tags=["Tools"]) + app.include_router(entity_init.router, prefix="/database/entity", tags=["Entity"]) + app.include_router(calendar.router, prefix="/database/calendar", tags=["Calendar"]) + app.include_router(schools.router, prefix="/database/schools", tags=["Schools"]) + app.include_router(timetables.router, prefix="/database/timetables", tags=["Timetables"]) + app.include_router(curriculum.router, prefix="/database/curriculum", tags=["Curriculum"]) + + # Navigation Routes + app.include_router(calendar_structure_router.router, prefix="/database/calendar-structure", tags=["Calendar"]) + app.include_router(worker_structure_router.router, prefix="/database/worker-structure", tags=["Worker"]) + app.include_router(default_nodes_router.router, prefix="/database/tools", tags=["Navigation"]) + + # Database Filesystem Routes + app.include_router(tldraw_filesystem.router, prefix="/database/tldraw_fs", tags=["TLDraw Filesystem"]) + + # Assets Routes + app.include_router(powerpoint.router, prefix="/assets/powerpoint", tags=["PowerPoint"]) + app.include_router(word.router, prefix="/assets/word", tags=["Word"]) + app.include_router(pdf.router, prefix="/assets/pdf", tags=["PDF"]) + + # LLM Routes + app.include_router(ollama.router, prefix="/llm/private/ollama", tags=["LLM"]) + app.include_router(openai.router, prefix="/llm/public/openai", tags=["LLM"]) + + # Langchain Routes + app.include_router(graph_qa_router, prefix="/langchain/graph_qa", tags=["Langchain"]) + app.include_router(interactive_langgraph_query_router, prefix="/langchain/interactive_langgraph_query", tags=["Langchain"]) + + # External Routes + app.include_router(youtube.router, prefix="/external", tags=["External"]) + + # Arbor Data Routes + app.include_router(arbor_router, prefix="/arbor", tags=["Arbor Data"]) + + # RPi Routes + app.include_router(rpi_whisperlive_client.router, prefix="/rpi", tags=["RPi"]) + + # Solid Pod Provisioner Routes + app.include_router(solid_pod_router, prefix="/solid", tags=["Solid"]) + + # Document Conversion Routes + app.include_router(document_conversion_router, prefix="/dev/documents", tags=["Document Conversion"]) + + # Test Analysis Routes + app.include_router(test_analysis_router, prefix="/dev/tests", tags=["Test Analysis"]) + + # Test Routes + app.include_router(timetable_test.router, prefix="/tests", tags=["Tests"]) diff --git a/run/setup.py b/run/setup.py new file mode 100644 index 0000000..bb38a2b --- /dev/null +++ b/run/setup.py @@ -0,0 +1,31 @@ +import os +from modules.logger_tool import initialise_logger +logger = initialise_logger( + log_name='api_main_fastapi', + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_dir=os.getenv("LOG_PATH", "/logs"), + log_format='default', + runtime=True +) +from fastapi import FastAPI + +def setup_cors(app: FastAPI) -> None: + """Configure CORS middleware for the FastAPI application""" + from fastapi.middleware.cors import CORSMiddleware + origins = [ + os.getenv('SITE_URL'), + os.getenv('APP_SITE_URL'), + os.getenv('APP_API_URL'), + os.getenv('APP_ADMIN_URL'), + ] + logger.info(f"Setting up CORS with origins: {origins}") + + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"] + ) + diff --git a/tests/.archive/pytest_init_calendar.py b/tests/.archive/pytest_init_calendar.py new file mode 100644 index 0000000..f6c309d --- /dev/null +++ b/tests/.archive/pytest_init_calendar.py @@ -0,0 +1,64 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'pytest_calendar' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neontology_tools as neon +import pytest +from fastapi.testclient import TestClient +from routers.database.init.calendar import router +from fastapi import FastAPI +from datetime import datetime, timedelta + +app = FastAPI() +app.include_router(router) + +client = TestClient(app) + +# Define a list of date ranges for testing +date_ranges = [ + (datetime.now(), datetime.now() + timedelta(days=1)), # 1 day + (datetime.now(), datetime.now() + timedelta(days=7)), # 1 week + (datetime.now(), datetime.now() + timedelta(days=30)), # 1 month + (datetime.now(), datetime.now() + timedelta(days=183)),# 6 months + (datetime.now(), datetime.now() + timedelta(days=365)) # 1 year +] + +# Fixture to manage database name increment +@pytest.fixture(scope="function", autouse=True) +def increment_db_name_counter(request): + if not hasattr(request.module, "db_name_counter"): + request.module.db_name_counter = 0 + request.module.db_name_counter += 1 + return request.module.db_name_counter + +@pytest.mark.parametrize("start_date, end_date", date_ranges) +def test_create_calendar(start_date, end_date, increment_db_name_counter): + db_name = f"test_create_calendar_db_{increment_db_name_counter}" + neo_safe_db_name = db_name.replace("_", "") + logging.info(f"Creating calendar for {db_name} from {start_date} to {end_date}") + logging.info(f"Creating calendar for {db_name} from {start_date} to {end_date}") + response = client.post( + "/create-calendar", + params={ + "db_name": neo_safe_db_name, + "start_date": start_date.strftime('%Y-%m-%d'), + "end_date": end_date.strftime('%Y-%m-%d') + } + ) + assert response.status_code == 200 + response_json = response.json() + assert "calendar_year_nodes" in response_json and response_json["calendar_year_nodes"] != 0 + assert "calendar_month_nodes" in response_json and response_json["calendar_month_nodes"] != 0 + assert "calendar_week_nodes" in response_json and response_json["calendar_week_nodes"] != 0 + assert "calendar_day_nodes" in response_json and response_json["calendar_day_nodes"] != 0 \ No newline at end of file diff --git a/tests/.archive/pytest_init_curriculum.py b/tests/.archive/pytest_init_curriculum.py new file mode 100644 index 0000000..48c21df --- /dev/null +++ b/tests/.archive/pytest_init_curriculum.py @@ -0,0 +1,61 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'pytest_init_curriculum' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neontology_tools as neon +import pytest +from fastapi.testclient import TestClient +from routers.database.init.curriculum import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) + +client = TestClient(app) + +db_name = log_name.replace('_', '') +excel_file = os.environ['EXCEL_CURRICULUM_FILE'] + +driver = driver_tools.get_driver(database=db_name) +neon.init_neontology_connection() + +@pytest.fixture +def sample_file(): + # Use the existing Excel file to upload + file_path = excel_file + logging.info(f"Using sample file at {file_path}") + yield file_path + +def test_upload_curriculum(sample_file): + db_name = "test_curriculum_db" + with open(sample_file, "rb") as f: + response = client.post( + "/upload-curriculum", + files={"file": (excel_file, f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}, + data={"db_name": db_name.replace('_', '')} + ) + logging.info(f"Response status code: {response.status_code}") + logging.info(f"Response JSON: {response.json()}") + + assert response.status_code == 200 + response_json = response.json() + logging.info(f"Response JSON keys: {response_json.keys()}") + + # Adjust the assertions based on the actual response structure + assert "status" in response_json or "12" in response_json + if "status" in response_json: + assert response_json["status"] == "Success" + else: + assert "created" in response_json["12"] + assert "merged" in response_json["12"] \ No newline at end of file diff --git a/tests/.archive/pytest_init_school_timetable.py b/tests/.archive/pytest_init_school_timetable.py new file mode 100644 index 0000000..a5b6bf0 --- /dev/null +++ b/tests/.archive/pytest_init_school_timetable.py @@ -0,0 +1,54 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'pytest_timetable' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neontology_tools as neon +import pytest +from fastapi.testclient import TestClient +from routers.database.init.timetable import router +from fastapi import FastAPI +import pandas as pd + +app = FastAPI() +app.include_router(router) + +client = TestClient(app) + +db_name = log_name.replace('_', '') +excel_file = os.environ['EXCEL_TIMETABLE_FILE'] + +@pytest.fixture +def sample_file(): + # Use the existing Excel file to upload + file_path = excel_file + logging.info(f"Using sample file at {file_path}") + yield file_path + +def test_upload_school_timetable(sample_file): + db_name = "pytest_school_timetable_db" + with open(sample_file, "rb") as f: + response = client.post( + "/upload-school-timetable", + data={"db_name": db_name.replace('_', '')}, + files={"file": (excel_file, f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + ) + logging.info(f"Response status code: {response.status_code}") + logging.info(f"Response JSON: {response.json()}") + + assert response.status_code == 200 + response_json = response.json() + assert "calendar_nodes" in response_json + assert "school_timetable_nodes" in response_json + assert response_json["calendar_nodes"] is not None + assert response_json["school_timetable_nodes"] is not None \ No newline at end of file diff --git a/tests/.archive/pytest_init_user.py b/tests/.archive/pytest_init_user.py new file mode 100644 index 0000000..7d6328a --- /dev/null +++ b/tests/.archive/pytest_init_user.py @@ -0,0 +1,44 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'pytest_timetable' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import modules.database.tools.neo4j_driver_tools as driver_tools +import modules.database.tools.neontology_tools as neon +import pytest +from fastapi.testclient import TestClient +from fastapi import FastAPI +import pandas as pd + +# Import the router from entity_init.py +from routers.database.init.entity_init import router + +app = FastAPI() +app.include_router(router) + +client = TestClient(app) + +@pytest.mark.parametrize("username, email, user_id", [ + ("user1", "user1@example.com", "uuid1"), + ("user2", "user2@example.com", "uuid2"), + ("user3", "user3@example.com", "uuid3") +]) +def test_create_user(username, email, user_id): + response = client.post( + "/create-user", + data={"username": username, "email": email, "user_id": user_id} + ) + logging.info(f"Tested creating user {username}. Response status code: {response.status_code}") + response_json = response.json() + logging.info(f"Response JSON: {response_json}") + + assert response.status_code == 200 \ No newline at end of file diff --git a/tests/.archive/pytest_transcribe.py b/tests/.archive/pytest_transcribe.py new file mode 100644 index 0000000..6c2e7c6 --- /dev/null +++ b/tests/.archive/pytest_transcribe.py @@ -0,0 +1,35 @@ +import os +import pytest +from fastapi.testclient import TestClient +from main import app # Adjust the import based on your project structure + +client = TestClient(app) + +@pytest.fixture(autouse=True) +def setup_env(): + os.environ["WHISPERLIVE_HOST"] = "localhost" + os.environ["WHISPERLIVE_PORT"] = "9090" + +def test_start_transcription(): + user_id = "test_user" + response = client.post(f"/transcribe/live/start_transcription/{user_id}") + assert response.status_code == 200 + assert response.json() == {"message": "Transcription started", "user_id": user_id} + +def test_handle_whisper_live_eos_utterance(): + user_id = "test_user" + data = { + "utterance": "Hello, world!", + "start": 0, + "end": 1, + "eos": True + } + response = client.post(f"/transcribe/utterance/handle_whisper_live_eos_utterance/{user_id}", json=data) + assert response.status_code == 200 + assert response.json() == {"message": "Utterance logged successfully"} + +def test_get_utterances(): + user_id = "test_user" + response = client.get(f"/transcribe/utterance/get_utterances/{user_id}") + assert response.status_code == 200 + assert "utterances" in response.json() \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/_transcribe.py b/tests/_transcribe.py new file mode 100644 index 0000000..e26aafe --- /dev/null +++ b/tests/_transcribe.py @@ -0,0 +1,34 @@ +from modules.whisper_live.client import TranscriptionClient +import os +import time + +def setup_directories(user_dir, user_id): + user_transcript_dir = f"{user_dir}/{user_id}/transcripts" + if not os.path.exists(user_transcript_dir): + os.makedirs(user_transcript_dir) + return user_transcript_dir + +def timestamped_callback(text, is_final): + if is_final: + print(f"Timestamp: {time.strftime('%H:%M:%S')}, Transcription: {text}") + +def main(): + user_dir = "../../data/users" + user_id = "kcar" + user_transcript_dir = setup_directories(user_dir, user_id) + + client = TranscriptionClient( + "localhost", + 9090, + lang="en", + translate=False, + use_vad=True, + save_output_recording=True, + output_recording_filename=f"{user_transcript_dir}/output_recording.wav", + output_transcription_path=f"{user_transcript_dir}/output.srt", + ) + + client() + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..566ff48 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,3 @@ +from modules.logger_tool import PytestFormatter + +pytest_formatter = PytestFormatter() \ No newline at end of file diff --git a/tests/formatting.py b/tests/formatting.py new file mode 100644 index 0000000..4073d6a --- /dev/null +++ b/tests/formatting.py @@ -0,0 +1,53 @@ +def ascii_header(): + return r""" +================================================== += = += _______ = += | | _ = += | |____ | | = += | / | | | ___ __ _ _ __ = += | | | | | / _ \/ _` | '_ \ = += | \____| | | | __/ (_| | | | | = += |_______| |_| \___|\__,_|_| |_| = += = += = += _________ = += | | = += | BOOK | = += |_________| = += = += = += /\ = += / \ = += /____\ = += / \ = += / \ = += /__________\ = += = += = += _____________ = += | | = += | COMPUTER | = += |_____________| = += = += = += = += _________ = += | | = += | TEACH | = += |_________| = += = += = += ____ = += / \ = += / \ = += / \ = += /__________\ = += = += = +================================================== += = += classroom-copilot.ai = += = +================================================== +""" \ No newline at end of file diff --git a/tests/pytest_arbor.py b/tests/pytest_arbor.py new file mode 100644 index 0000000..739ce1d --- /dev/null +++ b/tests/pytest_arbor.py @@ -0,0 +1,33 @@ +import os +import requests +import pytest +import json + +# Define the base URL and the tokens +base_url = f"{os.environ.get('APP_API_URL')}/arbor/data" +tokens = { + 1: os.getenv("KS3_COURSE_CLASS_MEMBERSHIP_AUTH"), + 2: os.getenv("TEACHING_GROUP_MEMBERSHIPS_2023_2024_AUTH"), + 3: os.getenv("SCHEDULED_TIMETABLE_SLOTS_AUTH"), + 4: os.getenv("BEHAVIOURAL_INCIDENTS_REPORTING_AUTH"), + 5: os.getenv("Y7_LESSON_TIMETABLE_AUTH") +} + +@pytest.mark.parametrize("id", [1, 2, 3, 4, 5]) +def test_fetch_arbor_data(id): + token = tokens.get(id) + if not token: + pytest.fail(f"Token for ID {id} is not set") + + endpoint = f"{base_url}/{id}" + headers = {"Authorization": f"Basic {token}"} + params = {"token": token} + + response = requests.get(endpoint, headers=headers, params=params) + + if response.status_code == 200: + print(json.dumps(response.json())) + assert response.status_code == 200 + else: + pytest.fail(f"Failed for ID {id}: {response.status_code} {response.text}") + diff --git a/tests/pytest_init_curriculum_graph_qa.py b/tests/pytest_init_curriculum_graph_qa.py new file mode 100644 index 0000000..12d4edc --- /dev/null +++ b/tests/pytest_init_curriculum_graph_qa.py @@ -0,0 +1,123 @@ +import os +import json +import requests +import pytest +from dotenv import load_dotenv, find_dotenv +from .formatting import ascii_header +import modules.logger_tool as logger + +load_dotenv(find_dotenv()) + +log_name = 'api_router_graph_qa_test' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +@pytest.fixture(scope="module") +def config(): + return { + "database": "cc.institutes.kevlarai", + "top_k": 40, + "model": "gpt-4o", + "temperature": 0, + "verbose": False, + "return_intermediate_steps": True, + "return_direct": False, + "validate_cypher": True, + "model_type": "openai" # Default model_type + } + +def load_test_cases(): + with open('backend/app/tests/test_inputs/init_curriculum_db_cases.json', 'r') as f: + return json.load(f) + +test_cases = load_test_cases() + +@pytest.mark.parametrize("case", test_cases["curriculum_cases"]) +def test_curriculum_cases(case, config): + assert run_test_case(case, config) + +@pytest.mark.parametrize("case", test_cases["include_exclude_cases"]["includes"]) +def test_include_cases(case, config): + assert run_test_case(case, config) + +@pytest.mark.parametrize("case", test_cases["include_exclude_cases"]["excludes"]) +def test_exclude_cases(case, config): + assert run_test_case(case, config) + +@pytest.mark.parametrize("case", test_cases["include_exclude_cases"]["includes_excludes"]) +def test_include_exclude_cases(case, config): + assert run_test_case(case, config) + +def run_test_case(case, config): + logging.info(f"Starting test case with prompt: {case['prompt']}") + url = f"{os.environ['APP_API_URL']}/langchain/graph_qa/prompt" + params = { + "database": config["database"], + "prompt": case["prompt"], + "top_k": config["top_k"], + "model": config["model"], + "temperature": config["temperature"], + "verbose": config["verbose"], + "return_intermediate_steps": config["return_intermediate_steps"], + "exclude_types": case["exclude_types"], + "include_types": case["include_types"], + "return_direct": config["return_direct"], + "validate_cypher": config["validate_cypher"], + "model_type": config["model_type"] + } + logging.info(f"Constructed URL: {url}") + logging.info(f"Parameters: {params}") + + try: + logging.info("Sending request to API...") + response = requests.get(url, params=params) + logging.info(f"HTTP Response Status: {response.status_code}") + response.raise_for_status() + data = response.json() + logging.info(f"Response Data: {data}") + + # Log detailed test execution information + logging.info("==================================================") + logging.info("= Test Execution =") + logging.info("==================================================") + logging.info(f"= Prompt: {data.get('query', 'N/A')}") + logging.info("= =") + logging.info(f"= Query: \n{data.get('intermediate_steps', [{'query': 'N/A'}])[0].get('query', 'N/A')}") + logging.info("= =") + logging.info("==================================================") + + # Determine if the test passed or failed + response_text = data.get('result', 'N/A') + context = data.get('intermediate_steps', [{'context': 'N/A'}])[1].get('context', 'N/A') + if "I don't know" in response_text or not context: + logging.error("==================================================") + logging.error("= XX Test Failed XX =") + logging.error("==================================================") + logging.error(f"= Prompt: {case['prompt']}") + logging.error(f"= Context: {context}") + logging.error(f"= Response: {response_text}") + logging.error("==================================================") + return False + else: + logging.info("==================================================") + logging.info("= ** Test Passed ** =") + logging.info("==================================================") + logging.info(f"= Prompt: {case['prompt']}") + logging.info(f"= Context: {context}") + logging.info(f"= Response: {response_text}") + logging.info("==================================================") + return True + except requests.exceptions.RequestException as e: + logging.error("==================================================") + logging.error("= ERROR =") + logging.error("==================================================") + logging.error(f"Error: {e}") + logging.error("==================================================") + return False \ No newline at end of file diff --git a/tests/pytest_init_x.py b/tests/pytest_init_x.py new file mode 100644 index 0000000..2e05ebf --- /dev/null +++ b/tests/pytest_init_x.py @@ -0,0 +1,294 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import pytest +from fastapi.testclient import TestClient +from fastapi import FastAPI +import json +import modules.logger_tool as logger +from routers.database.init.entity_init import router as entity_init_router +from routers.database.init.timetables import router as timetables_router +from routers.database.init.curriculum import router as curriculum_router +from backend.modules.database.schemas.entities import SchoolNode, UserNode +from modules.database.schemas.nodes.calendars import CalendarNode + +# Pytest configuration +def pytest_configure(config): + config.addinivalue_line( + "markers", "school: mark test as part of school creation" + ) + config.addinivalue_line( + "markers", "users: mark test as part of user creation" + ) + config.addinivalue_line( + "markers", "timetable: mark test as part of timetable upload" + ) + config.addinivalue_line( + "markers", "curriculum: mark test as part of curriculum upload" + ) + +# Setup logging +log_name = 'pytest_init_x' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +# Setup FastAPI app and test client +app = FastAPI() +app.include_router(entity_init_router) +app.include_router(timetables_router) +app.include_router(curriculum_router) +client = TestClient(app) + +school_timetable_file = os.environ['EXCEL_TIMETABLE_FILE'] +school_curriculum_file = os.environ['EXCEL_CURRICULUM_FILE'] + +@pytest.fixture(scope="module") +def school_info(): + db_name = "cc.institutes.devschool" + school_data = { + "db_name": db_name, + "school_uuid": "uuid1", + "school_name": "school1", + "school_website": "www.school1.com" + } + return school_data + +@pytest.fixture(scope="module") +def created_school(school_info): + school_data = school_info + response = client.post("/create-school", data=school_data) + logging.info(f"Create school response: {response.json()}") + assert response.status_code == 200 + logging.success("School created successfully") + + response_json = response.json() + school_node = SchoolNode(**response_json["school_node"]) + + logging.success(f"School node created: {school_node}") + return school_node + +@pytest.mark.school +def test_create_school(created_school): + school_node = created_school + assert school_node is not None + +@pytest.mark.users +@pytest.mark.parametrize("user_type, expected_status", [ + ("standard", 200), + ("developer", 200) +]) +def test_create_non_school_user(user_type, expected_status): + db_name = "cc.users.devusers" + user_data = { + "user_type": user_type, + "user_name": f"test_{user_type}", + "user_email": f"test_{user_type}@example.com", + "user_id": f"{user_type}_uuid" + } + response = client.post("/create-user", data=user_data) + assert response.status_code == expected_status + logging.success(f"{user_type.capitalize()} user created successfully") + +@pytest.mark.users +@pytest.mark.parametrize("user_type, expected_status", [ + ("cc_email_school_admin", 200), + ("cc_email_teacher", 200), + ("cc_email_student", 200) +]) +def test_create_school_user(created_school, user_type, expected_status): + school_node = created_school + worker_data = { + "cc_email_school_admin": { + "admin_code": "ADM001", + "admin_name_formal": "Mr. Admin", + "admin_email": "admin@school.com" + }, + "cc_email_teacher": { + "teacher_code": "TCH001", + "teacher_name_formal": "Ms. Teacher", + "teacher_email": "teacher@school.com" + }, + "cc_email_student": { + "student_code": "STU001", + "student_name_formal": "Student Name", + "student_email": "student@school.com" + } + } + user_data = { + "user_type": user_type, + "user_name": f"test_{user_type}", + "user_email": f"test_{user_type}@example.com", + "user_id": f"{user_type}_uuid", + "school_uuid": school_node.school_uuid, + "school_name": school_node.school_name, + "school_website": school_node.school_website, + "school_path": school_node.path, + "worker_data": json.dumps(worker_data[user_type]) + } + logging.info(f"Sending user data: {user_data}") + response = client.post("/create-user", data=user_data) + assert response.status_code == expected_status + logging.success(f"{user_type.capitalize()} user created successfully") + +def test_create_user_invalid_data(): + invalid_user_data = { + "user_type": "invalid_type", + "user_name": "test_invalid", + "user_email": "test_invalid@example.com", + "user_id": "invalid_uuid" + } + response = client.post("/create-user", data=invalid_user_data) + assert response.status_code == 400 + logging.success("Invalid user data handled correctly") + +@pytest.mark.users +def test_create_school_user_without_school_node(): + user_data = { + "user_type": "cc_email_teacher", + "user_name": "test_teacher_no_school", + "user_email": "test_teacher_no_school@example.com", + "user_id": "teacher_no_school_uuid" + } + response = client.post("/create-user", data=user_data) + assert response.status_code == 400 + logging.success("School-related user without school_node handled correctly") + +@pytest.fixture +def sample_file(): + logging.info(f"Using sample file: {school_timetable_file}") + return school_timetable_file + +@pytest.mark.timetable +def test_upload_school_timetable(created_school, sample_file): + school_node = created_school + with open(sample_file, "rb") as f: + response = client.post( + "/upload-school-timetable", + data={ + "db_name": "cc.institutes.devschool", + "unique_id": school_node.unique_id, + "school_uuid": school_node.school_uuid, + "school_name": school_node.school_name, + "school_db_name": school_node.school_db_name, + "school_website": school_node.school_website, + "path": school_node.path + }, + files={"file": (os.path.basename(sample_file), f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + ) + logging.info(f"Timetable upload response: {response.json()}") + assert response.status_code == 200 + logging.success("Timetable uploaded successfully") + + response_json = response.json() + + for key in ["school_node", "school_calendar_nodes", "school_timetable_nodes"]: + assert key in response_json + logging.success(f"{key} present in response") + + school_node = SchoolNode(**response_json["school_node"]) + calendar_node = CalendarNode(**response_json['school_calendar_nodes']['calendar_node']) + + logging.success(f"School node validated: {school_node}") + logging.success(f"Calendar node validated: {calendar_node}") + + for key in ["school_node", "school_calendar_nodes", "school_timetable_nodes"]: + assert response_json[key] is not None + logging.success(f"{key} is not None") + + logging.success("All assertions passed in test_upload_school_timetable") + +@pytest.fixture +def curriculum_sample_file(): + logging.info(f"Using curriculum sample file: {school_curriculum_file}") + return school_curriculum_file + + +@pytest.mark.curriculum +def test_upload_school_curriculum(created_school, curriculum_sample_file): + school_node = created_school + with open(curriculum_sample_file, "rb") as f: + response = client.post( + "/upload-school-curriculum", + data={ + "db_name": "cc.institutes.devschool", + "school_uuid": school_node.school_uuid, + "school_name": school_node.school_name, + "school_website": school_node.school_website, + "school_path": school_node.path + }, + files={"file": (os.path.basename(curriculum_sample_file), f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + ) + + assert response.status_code == 200 + logging.success("Curriculum uploaded successfully") + + response_json = response.json() + + assert "curriculum_node" in response_json + assert "pastoral_node" in response_json + assert "key_stage_nodes" in response_json + assert "year_group_syllabus_nodes" in response_json + assert "topic_nodes" in response_json + assert "topic_lesson_nodes" in response_json + assert "statement_nodes" in response_json + + logging.success("All assertions passed in test_upload_school_curriculum") + +@pytest.mark.users +@pytest.mark.timetable +def test_create_kcar_user_and_upload_timetable(created_school): + school_node = created_school + user_data = { + "user_type": "cc_email_teacher", + "user_name": "K Car", + "user_email": "kcar@example.com", + "user_id": "kcar_uuid", + "school_uuid": school_node.school_uuid, + "school_name": school_node.school_name, + "school_website": school_node.school_website, + "school_path": school_node.path, + "worker_data": json.dumps({ + "teacher_code": "KCAR", + "teacher_name_formal": "Mr. K Car", + "teacher_email": "kcar@example.com" + }) + } + logging.info(f"Creating KCar user with data: {user_data}") + response = client.post("/create-user", data=user_data) + logging.info(f"KCar user creation response: {response.json()}") + assert response.status_code == 200 + logging.success("KCar user created successfully") + kcar_user = UserNode(**response.json()["data"]["user_node"]) + + user_timetable_file = os.environ['KCAR_TIMETABLE_URL'] + logging.info(f"User timetable file: {user_timetable_file}") + with open(user_timetable_file, "rb") as f: + logging.info(f"Uploading teacher timetable for K Car: {user_timetable_file}") + response = client.post( + "/upload-worker-timetable", + data={ + "user_id": kcar_user.user_id, + "db_name": "cc.institutes.devschool" + }, + files={"file": (os.path.basename(user_timetable_file), f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + ) + logging.info(f"Teacher timetable upload response: {response.json()}") + assert response.status_code == 200 + logging.success("K Car teacher timetable uploaded successfully") + + response_json = response.json() + + assert response_json["message"] == "Teacher timetable initialized successfully" + + logging.success("All assertions passed in test_create_kcar_user_and_upload_timetable") + +def pytest_runtest_makereport(item, call): + if call.when == "call" and call.excinfo is None: + logging.success(f"Test passed: {item.name}") \ No newline at end of file diff --git a/tests/pytest_langgraph.py b/tests/pytest_langgraph.py new file mode 100644 index 0000000..3677ca6 --- /dev/null +++ b/tests/pytest_langgraph.py @@ -0,0 +1,85 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import modules.logger_tool as logger +log_name = 'api_modules_interactive_langgraph_query' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) +import pytest +import requests + +# Define the URL of your FastAPI server +BASE_URL = "http://localhost:8000" +ENDPOINT = f"{BASE_URL}/api/langchain/interactive_langgraph_query/query" + +def send_query(query): + payload = {"query": query} + headers = {"Content-Type": "application/json"} + logging.info(f"Sending query to {ENDPOINT} with payload: {payload}") + + try: + response = requests.post(ENDPOINT, json=payload, headers=headers) + response.raise_for_status() + result = response.json() + logging.info(f"Received response from {ENDPOINT}: {result}") + return result + except requests.exceptions.RequestException as e: + logging.error(f"Error sending query to {ENDPOINT}: {str(e)}") + return {"error": str(e)} + +@pytest.mark.simple +def test_simple_queries(): + query = "Describe the relevance of Maidstone, England during the English Civil War." + logging.info(f"Running simple query test with query: {query}") + result = send_query(query) + + logging.info(f"Assertion 1: Checking for absence of error") + assert "error" not in result, f"Error in response: {result.get('error')}" + + logging.info(f"Assertion 2: Checking for presence of response") + assert "response" in result, "Response does not contain an answer" + + logging.info(f"Assertion 3: Checking for non-empty answer") + assert len(result["response"]) > 0, "Answer is empty" + + logging.info(f"All assertions passed. Response: {result['response'][:100]}...") + +@pytest.mark.followup +def test_followup_queries(): + initial_query = "What is the latest local news from a particular town?" + logging.info(f"Running followup query test with initial query: {initial_query}") + result = send_query(initial_query) + + logging.info(f"Assertion 1: Checking for absence of error") + assert "error" not in result, f"Error in response: {result.get('error')}" + + if result.get("needs_more_info", False): + logging.info("Follow-up required. Sending follow-up query.") + follow_up_query = f"{initial_query} The town is Maidstone." + follow_up_result = send_query(follow_up_query) + + logging.info(f"Assertion 2: Checking for absence of error in follow-up") + assert "error" not in follow_up_result, f"Error in follow-up response: {follow_up_result.get('error')}" + + logging.info(f"Assertion 3: Checking for presence of response in follow-up") + assert "response" in follow_up_result, "Follow-up response does not contain an answer" + + logging.info(f"Assertion 4: Checking for non-empty answer in follow-up") + assert len(follow_up_result["response"]) > 0, "Follow-up answer is empty" + + logging.info(f"All follow-up assertions passed. Response: {follow_up_result['response'][:100]}...") + else: + logging.info(f"Assertion 2: Checking for presence of response") + assert "response" in result, "Response does not contain an answer" + + logging.info(f"Assertion 3: Checking for non-empty answer") + assert len(result["response"]) > 0, "Answer is empty" + + logging.info(f"All assertions passed. Response: {result['response'][:100]}...") \ No newline at end of file diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..c548b86 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,179 @@ +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv()) +import os +import sys +import subprocess +from datetime import datetime +import webbrowser +import threading +import shutil +import time + +# Add the parent directory to the Python path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import modules.logger_tool as logger + +# Setup logging +log_name = 'pytest_run_tests' +log_dir = os.getenv("LOG_PATH", "/logs") # Default path as fallback +logging = logger.get_logger( + name=log_name, + log_level=os.getenv("LOG_LEVEL", "DEBUG"), + log_path=log_dir, + log_file=log_name, + runtime=True, + log_format='default' +) + +def find_project_root(): + # Start from the current file location + root = os.path.dirname(os.path.abspath(__file__)) + # Traverse up until you find the .env file + while not os.path.exists(os.path.join(root, '.env')): + new_root = os.path.dirname(root) + if root == new_root: # root directory reached without finding .env + raise Exception("Project root not found.") + root = new_root + return root + +def load_env(): + project_root = find_project_root() + dotenv_path = find_dotenv(os.path.join(project_root, '.env')) + load_dotenv(dotenv_path) + required_vars = ["FIXME"] + for var in required_vars: + if var not in os.environ: + print(f"Error: {var} is not set in the environment.") + sys.exit(1) + +def select_test_file(): + project_root = find_project_root() + test_categories = { + "A": { + "name": "X Copilot Initialization", + "tests": { + "1": os.path.join(project_root, "backend", "app", "tests", "pytest_init_x.py") + } + }, + "B": { + "name": "Graph QA", + "tests": { + "1": os.path.join(project_root, "backend", "app", "tests", "pytest_init_school_timetable_graph_qa.py"), + "2": os.path.join(project_root, "backend", "app", "tests", "pytest_init_curriculum_graph_qa.py"), + "3": os.path.join(project_root, "backend", "app", "tests", "pytest_init_calendar_graph_qa.py") + } + }, + "C": { + "name": "Connections", + "tests": { + "1": os.path.join(project_root, "backend", "app", "tests", "pytest_arbor.py") + } + }, + "D": { + "name": "Transcription", + "tests": { + "1": os.path.join(project_root, "tests", "pytest_transcribe.py") + } + }, + "E": { + "name": "LangGraph", + "tests": { + "1": os.path.join(project_root, "backend", "app", "tests", "pytest_langgraph.py") + } + } + } + + print("Select a test file to run:") + for category_key, category in test_categories.items(): + print(f"\n{category_key}: {category['name']}") + for test_key, test_file in category["tests"].items(): + print(f" {category_key}{test_key}: {os.path.basename(test_file)}") + + choice = input("\nEnter your choice (e.g., A1): ").upper() + if len(choice) == 2 and choice[0] in test_categories and choice[1] in test_categories[choice[0]]["tests"]: + category_key, test_key = choice[0], choice[1] + return test_categories[category_key]["tests"][test_key], choice + + print("Invalid choice.") + sys.exit(1) + +def create_log_dir(choice, project_root): + log_dir = os.path.join(project_root, "logs", "pytests") + if choice[0] == "A": + log_dir = os.path.join(log_dir, "database", "init") + elif choice[0] == "B": + log_dir = os.path.join(log_dir, "database", "langchain", "graph_qa") + elif choice[0] == "C": + log_dir = os.path.join(log_dir, "database", "connections", "arbor") + elif choice[0] == "D": + log_dir = os.path.join(log_dir, "transcribe") + elif choice[0] == "E": + log_dir = os.path.join(log_dir, "langgraph") + else: + print("Invalid choice.") + sys.exit(1) + + os.makedirs(log_dir, exist_ok=True) + return log_dir + +def open_html_report_in_browser(html_path): + """Function to open the HTML report in the default web browser.""" + # Check for the existence of the file every 2 seconds, up to a maximum of 10 checks + for _ in range(10): + if os.path.exists(html_path): + webbrowser.open(html_path) + break + time.sleep(2) + else: + print("HTML report was not generated in time.") + +def run_tests(test_file, log_dir, choice): + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + base_filename = os.path.basename(test_file).replace('.py', '') + html_report = os.path.join(log_dir, f"{base_filename}_pytest_report_{timestamp}.html") + xml_report = os.path.join(log_dir, f"{base_filename}_pytest_report_{timestamp}.xml") + + pytest_command = [ + "pytest", + "-v", + test_file, + f"--junitxml={xml_report}", + f"--html={html_report}", + "--self-contained-html", + "--capture=tee-sys", + "--show-capture=all" + ] + + if choice[0] == "A": + test_components = input("Enter test components to run (school,users,timetable), comma-separated, or 'all': ").lower() + if test_components != 'all': + components = test_components.split(',') + for component in components: + pytest_command.append(f"-m {component}") + + print("Running command:", ' '.join(pytest_command)) + + # Start a thread to open the HTML report, checking for its existence + threading.Thread(target=open_html_report_in_browser, args=(html_report,)).start() + + result = subprocess.run(pytest_command, check=True) + return result + +def main(): + project_root = find_project_root() + load_env() + data_dir = os.path.join(project_root, "APP_DATA") + # TODO: Modify this after initial testing + if os.path.exists(data_dir): + shutil.rmtree(data_dir) + test_file, choice = select_test_file() + if not test_file: + print("Invalid choice.") + sys.exit(1) + + log_dir = create_log_dir(choice, project_root) + run_tests(test_file, log_dir, choice) + +if __name__ == "__main__": + main() diff --git a/tests/test_inputs/init_curriculum_db_cases.json b/tests/test_inputs/init_curriculum_db_cases.json new file mode 100644 index 0000000..a9724ff --- /dev/null +++ b/tests/test_inputs/init_curriculum_db_cases.json @@ -0,0 +1,150 @@ +{ + "curriculum_cases": [ + { + "description": "Retrieve Information About Lessons in a Topic", + "prompt": "What are the lessons in the topic 'Maths Skills For Scientists'?", + "exclude_types": ["KeyStage", "KeyStageSyllabus", "YearGroup", "YearGroupSyllabus"], + "include_types": ["Topic", "Lesson", "LESSON_INCLUDES_LEARNING_STATEMENT"] + }, + { + "description": "Retrieve Information About a Specific Year Group Syllabus", + "prompt": "What is the syllabus for Year 8?", + "exclude_types": ["KeyStage", "KeyStageSyllabus", "Topic", "Lesson", "LearningStatement"], + "include_types": ["YearGroup", "YearGroupSyllabus", "YEAR_SYLLABUS_INCLUDES_TOPIC"] + }, + { + "description": "Retrieve Key Stages and Their Syllabuses", + "prompt": "What are the key stages and their syllabuses?", + "exclude_types": ["YearGroup", "YearGroupSyllabus", "Topic", "Lesson", "LearningStatement"], + "include_types": ["KeyStage", "KeyStageSyllabus", "KEY_STAGE_INCLUDES_KEY_STAGE_SYLLABUS"] + }, + { + "description": "Retrieve Topics Within a Specific Year Group Syllabus", + "prompt": "What are the topics in the Year 8 Science syllabus?", + "exclude_types": ["KeyStage", "KeyStageSyllabus", "Lesson", "LearningStatement"], + "include_types": ["YearGroup", "YearGroupSyllabus", "Topic", "YEAR_SYLLABUS_INCLUDES_TOPIC"] + }, + { + "description": "Retrieve All Learning Statements for a Specific Lesson", + "prompt": "What are the learning statements for the lesson '8P6.R'?", + "exclude_types": ["KeyStage", "KeyStageSyllabus", "YearGroup", "YearGroupSyllabus", "Topic"], + "include_types": ["Lesson", "LearningStatement", "LESSON_INCLUDES_LEARNING_STATEMENT"] + }, + { + "description": "General Information Retrieval Without Exclusions", + "prompt": "Give me an overview of the school curriculum.", + "exclude_types": [], + "include_types": [] + }, + { + "description": "Retrieve Detailed Information About a Specific Node Type", + "prompt": "Give me detailed information about all topics.", + "exclude_types": [], + "include_types": ["Topic"] + }, + { + "description": "Retrieve Relationships Between Specific Node Types", + "prompt": "What are the relationships between Year Groups and their syllabuses?", + "exclude_types": [], + "include_types": ["YearGroup", "YearGroupSyllabus", "KEY_STAGE_SYLLABUS_INCLUDES_YEAR_GROUP_SYLLABUS"] + } + ], + "include_exclude_cases": { + "includes": [ + { + "description": "Include only Lessons", + "prompt": "What are the lessons in the topic 'Maths Skills For Scientists'?", + "exclude_types": [], + "include_types": ["Lesson"] + }, + { + "description": "Include only Topics", + "prompt": "What are the topics in the Year 8 Science syllabus?", + "exclude_types": [], + "include_types": ["Topic"] + }, + { + "description": "Include only Year Groups", + "prompt": "What are the year groups in the school curriculum?", + "exclude_types": [], + "include_types": ["YearGroup"] + }, + { + "description": "Include only Learning Statements", + "prompt": "What are the learning statements for the lesson '8P6.R'?", + "exclude_types": [], + "include_types": ["LearningStatement"] + }, + { + "description": "Include only Key Stages", + "prompt": "What are the key stages in the school curriculum?", + "exclude_types": [], + "include_types": ["KeyStage"] + } + ], + "excludes": [ + { + "description": "Exclude Lessons", + "prompt": "What are the lessons in the topic 'Maths Skills For Scientists'?", + "exclude_types": ["Lesson"], + "include_types": [] + }, + { + "description": "Exclude Topics", + "prompt": "What are the topics in the Year 8 Science syllabus?", + "exclude_types": ["Topic"], + "include_types": [] + }, + { + "description": "Exclude Year Groups", + "prompt": "What are the year groups in the school curriculum?", + "exclude_types": ["YearGroup"], + "include_types": [] + }, + { + "description": "Exclude Learning Statements", + "prompt": "What are the learning statements for the lesson '8P6.R'?", + "exclude_types": ["LearningStatement"], + "include_types": [] + }, + { + "description": "Exclude Key Stages", + "prompt": "What are the key stages in the school curriculum?", + "exclude_types": ["KeyStage"], + "include_types": [] + } + ], + "includes_excludes": [ + { + "description": "Include Lessons, Exclude Topics", + "prompt": "What are the lessons in the topic 'Maths Skills For Scientists'?", + "exclude_types": ["Topic"], + "include_types": ["Lesson"] + }, + { + "description": "Include Topics, Exclude Lessons", + "prompt": "What are the topics in the Year 8 Science syllabus?", + "exclude_types": ["Lesson"], + "include_types": ["Topic"] + }, + { + "description": "Include Year Groups, Exclude Key Stages", + "prompt": "What are the year groups in the school curriculum?", + "exclude_types": ["KeyStage"], + "include_types": ["YearGroup"] + }, + { + "description": "Include Learning Statements, Exclude Lessons", + "prompt": "What are the learning statements for the lesson '8P6.R'?", + "exclude_types": ["Lesson"], + "include_types": ["LearningStatement"] + }, + { + "description": "Include Key Stages, Exclude Year Groups", + "prompt": "What are the key stages in the school curriculum?", + "exclude_types": ["YearGroup"], + "include_types": ["KeyStage"] + } + ] + } +} \ No newline at end of file