Read-only reporting access

Provider Reporting Data Model

The tables your reporting login can read, how they link together, and what each field means. Use it to plan the reports you build in your own tools.

How the data connects

Every link uses a UUID: a fixed identifier that never changes, even if a name or email does. Join tables on these, never on names.

Filtered to your organisations Course catalogue, the same for every provider
erDiagram
    po_organisations ||--o{ po_students : "enrols"
    po_programme_aims |o--o{ po_students : "assigned to"
    po_students ||--o{ po_student_course_progress : "has"
    po_students ||--o{ po_student_topic_progress : "has"
    po_students ||--o{ po_initial_assessment_attempts : "sits"
    po_students ||--o{ po_mock_exam_attempts : "sits"
    po_courses ||--o{ po_topic_areas : "contains"
    po_topic_areas ||--o{ po_topics : "contains"
    po_courses ||--o{ po_student_course_progress : "tracked on"
    po_courses ||--o{ po_mock_exam_attempts : "for"
    po_topics ||--o{ po_student_topic_progress : "tracked on"

    po_organisations {
        uuid uuid PK
        text name
    }
    po_students {
        uuid user_uuid
        uuid organisation_uuid FK
        uuid programme_aim_uuid FK
        text learning_status
    }
    po_programme_aims {
        uuid uuid PK
        text name
    }    po_student_course_progress {
        uuid user_uuid FK
        uuid course_uuid FK
        decimal course_progress
    }
    po_student_topic_progress {
        uuid user_uuid FK
        uuid topic_uuid FK
        text topic_status
    }
    po_initial_assessment_attempts {
        uuid attempt_uuid PK
        uuid user_uuid FK
        decimal calculated_level
    }
    po_mock_exam_attempts {
        uuid attempt_uuid PK
        uuid user_uuid FK
        uuid course_uuid FK
        decimal score
    }
    po_courses {
        uuid uuid PK
        text title
    }
    po_topic_areas {
        uuid topic_area_uuid PK
        uuid course_uuid FK
    }
    po_topics {
        uuid topic_uuid PK
        uuid topic_area_uuid FK
    }

The diagram shows key fields only. Every field is listed below. Read a line as "one of this, many of that": one organisation enrols many students, and one course contains many topic areas.

Before you build a report

Count students with DISTINCT

A student in two of your organisations appears once per organisation in po_students. Use COUNT(DISTINCT user_uuid) for headcounts.

The catalogue is everything

Courses, topic areas and topics list every active course we offer, not just the ones your students take. Start from a student table to see only your learners' courses.

Topic progress has no course column

To group topic progress by course, go topic → topic area → course.

Removed records are left out

Deleted students and organisations, and inactive courses, are left out of every table automatically.

It is read-only and live

You can query but not change anything. Figures reflect the platform shortly after they change.

Your organisations and students

Filtered to you

po_organisations

The organisations your login can see.

One row per organisation.

FieldTypeMeaning
uuidKEYuuidOrganisation identifier. Matches po_students.organisation_uuid.
nametextOrganisation name.
websitetextWebsite, if one was given.
created_atdatetimeWhen the organisation was set up.
updated_atdatetimeWhen its details last changed.

po_students

Students in your organisations.

One row per student per organisation.

FieldTypeMeaning
user_uuidKEYuuidStudent identifier. Links to every progress and attempt table.
organisation_uuiduuidThe organisation this row belongs to.
programme_aim_uuiduuid, optionalThe student's programme aim. Empty if none is set.
readable_identifiertextThe student's email address, or their username if they log in without email.
email_enabled1 / 01 if the student logs in with an email address.
learning_statustextactive, on_a_break or archived.
created_atdatetimeWhen the student account was created.
last_logindatetime, optionalMost recent login. Empty if they have never logged in.

po_student_course_progress

How each student is doing on each course they are enrolled on.

One row per student per course.

FieldTypeMeaning
user_uuiduuidThe student.
course_uuiduuidThe course. Matches po_courses.uuid.
course_plantextPremium or Free.
course_progresspercentageHow much of the whole course is complete, 0 to 100.
learning_plan_progresspercentageHow much of the student's recommended learning plan is complete, 0 to 100.
learning_plan_itemsnumberRecommended topics still to do.
initial_levelnumberThe level the student started the course at.
current_levelnumberThe level the student is working at now.
time_spent_on_coursesecondsTotal time recorded answering questions on this course.
planned_end_datedate, optionalThe planned end date for this enrolment, if one was set.
past_planned_end_date1 / 01 if the planned end date has passed.
marked_ready_for_exam1 / 01 if the student has been marked ready for their exam.

po_student_topic_progress

Each student's progress through individual topics.

One row per student per topic they have started.

FieldTypeMeaning
user_uuiduuidThe student.
topic_uuiduuidThe topic. Matches po_topics.topic_uuid.
topic_statustextWhere the student is with this topic.
revision_completion_datedatetime, optionalWhen the student finished the revision material.
test_completion_datedatetime, optionalWhen the student completed the topic test.
test_scorenumber, optionalScore on that topic test.
exam_completion_datedatetime, optionalWhen the student completed the topic exam questions.
exam_scorenumber, optionalScore on those exam questions.

po_initial_assessment_attempts

Initial assessments students have started, and the level each one placed them at.

One row per attempt.

FieldTypeMeaning
attempt_uuidKEYuuidIdentifier for this attempt.
user_uuiduuidThe student.
initial_assessment_uuiduuidWhich initial assessment was taken. Use it to group attempts of the same assessment.
titletextName of the initial assessment.
started_atdatetimeWhen the attempt began.
completed_atdatetime, optionalWhen it was finished. Empty if still in progress.
total_elapsed_timeseconds, optionalTime spent answering questions.
calculated_levelnumber, optionalThe level the assessment placed the student at, to 2 decimal places.

po_mock_exam_attempts

Mock exams students have sat.

One row per attempt.

FieldTypeMeaning
attempt_uuidKEYuuidIdentifier for this attempt.
user_uuiduuidThe student.
course_uuiduuidThe course the mock exam belongs to.
exam_uuiduuidWhich mock exam was taken. Use it to group attempts of the same exam.
exam_titletextName of the mock exam.
scorenumberScore achieved.
hurdlenumber, optionalThe score needed to pass this mock exam, where one is set.
completed_atdatetime, optionalWhen it was finished. Empty if still in progress.

Course catalogue

Same for every provider

Lookup tables for turning UUIDs into readable names, and for grouping progress by course and topic area.

po_courses

One row per active course.

FieldTypeMeaning
uuidKEYuuidCourse identifier.
titletextCourse name.

po_topic_areas

One row per topic area. A topic area is a section of a course.

FieldTypeMeaning
topic_area_uuidKEYuuidTopic area identifier.
topic_area_titletextTopic area name.
course_uuiduuidThe course it belongs to.

po_topics

One row per topic.

FieldTypeMeaning
topic_uuidKEYuuidTopic identifier.
topic_titletextTopic name.
topic_area_uuiduuidThe topic area it belongs to.

po_programme_aims

One row per programme aim.

FieldTypeMeaning
uuidKEYuuidProgramme aim identifier. Matches po_students.programme_aim_uuid.
nametextProgramme aim name.

Example queries

Average course progress per organisation and course

SQL
SELECT o.name AS organisation,
       c.title AS course,
       COUNT(DISTINCT p.user_uuid) AS students,
       ROUND(AVG(p.course_progress), 1) AS avg_progress
FROM po_student_course_progress p
JOIN po_students s ON s.user_uuid = p.user_uuid
JOIN po_organisations o ON o.uuid = s.organisation_uuid
JOIN po_courses c ON c.uuid = p.course_uuid
GROUP BY o.name, c.title;

Topic completion grouped by course and topic area

SQL
SELECT c.title AS course,
       ta.topic_area_title,
       COUNT(*) AS topics_started,
       SUM(tp.test_completion_date IS NOT NULL) AS tests_done
FROM po_student_topic_progress tp
JOIN po_topics t ON t.topic_uuid = tp.topic_uuid
JOIN po_topic_areas ta ON ta.topic_area_uuid = t.topic_area_uuid
JOIN po_courses c ON c.uuid = ta.course_uuid
GROUP BY c.title, ta.topic_area_title;