Skip to content
Gravity Tables
tutorial

Build a school student records dashboard with Gravity Forms

A complete pattern for K-12 and higher-ed: parent-facing intake, office staff workspace, counselor-only IEP fields, parent self-serve portal, and a year-end audit export. One Gravity Form, role-gated columns, full audit trail.

· 8 min read · By Fahd Murtaza

Student information systems start at $4-$8 per student per year and require an annual implementation. For a 600-student middle school, that’s $2,400-$4,800 plus a sysadmin spending half a year wiring it to the district’s existing tools. For a school that already runs WordPress for the parent newsletter, most of the daily-ops use cases are simpler than that.

This guide ships the practical pattern most schools actually run: parent intake form, office staff workspace, counselor-only fields for sensitive data, parent self-serve updates, and a year-end audit export, on one Gravity Form, with the privacy gates that compliance reviews check for.

What you’ll build#

Three URLs, one Gravity Form, four roles:

  • /parent-portal, parents log in, see and update their own children’s contacts, photo permissions, emergency contacts
  • /staff/students, office staff workspace (the daily working surface)
  • /staff/counselor-view, counselors see all students plus the gated IEP / medical notes columns
  • /staff/audit, admin-only year-end audit export

Roles:

  • parent, sees only their own children’s records, can edit a small set of fields
  • office-staff, sees all students, can edit administrative fields (homeroom, grade, attendance)
  • counselor, sees all students plus the gated medical / IEP / behavioural notes fields, can edit those
  • administrator, sees everything, runs audits

Step 1: shape the form#

Customer-visible (parent submits or updates):

  • student_first_name, student_last_name, student_dob (required, set once at enrollment)
  • parent_first_name, parent_last_name, parent_email (used as the owning user identifier)
  • phone_primary, phone_secondary
  • address, city, state, zip
  • emergency_contact_name, emergency_contact_phone, emergency_contact_relationship
  • photo_permission (checkbox, yes/no for the school’s right to photograph the student)
  • pickup_authorized (paragraph, names of adults authorised to pick the student up)

Office-staff editable:

  • homeroom, single-line text, default empty. Set on enrollment.
  • grade, number field. Updated annually.
  • enrollment_status, dropdown (active, withdrawn, transferred, graduated)
  • attendance_pct, number, computed externally and updated weekly

Counselor-only (gated):

  • medical_notes, paragraph, default empty. Allergies, medications, chronic conditions.
  • iep_status, dropdown (none, 504_plan, iep, pending_assessment)
  • behavioural_notes, paragraph, default empty. Counselor commentary.

Hidden admin-side:

  • parent_user_id, Hidden, default empty. Set via gform_pre_submission hook to the WP user id of the parent submitter.
  • last_audit_export, Date, default empty. Stamped by the audit-export cron.

Step 2: set the parent_user_id on submission#

In your child theme’s functions.php:

add_action('gform_pre_submission_students', function ($form) {
    if (!is_user_logged_in()) return;
    $user = wp_get_current_user();
    // Find the parent_user_id field
    foreach ($form['fields'] as $field) {
        if ($field->adminLabel === 'parent_user_id') {
            $_POST['input_' . $field->id] = $user->ID;
            break;
        }
    }
});

This is the hook that closes the security gap. Without it, filter_user_owns="parent_user_id" would be filtering against a user-editable form field, which a malicious parent could set to another parent’s user id and read their child’s record. The gform_pre_submission hook overwrites the field server-side with the authenticated user’s id, the parent literally cannot change it.

Step 3: build the parent self-serve portal (/parent-portal)#

[gravity_table id="students"
  columns="student_first_name,student_last_name,grade,homeroom,phone_primary,address,emergency_contact_name,emergency_contact_phone,photo_permission,pickup_authorized"
  filter_user_owns="parent_user_id"
  allowed_roles="parent,office-staff,counselor,administrator"
  allow_edit="phone_primary,phone_secondary,address,city,state,zip,emergency_contact_name,emergency_contact_phone,emergency_contact_relationship,photo_permission,pickup_authorized"
  filters=""
  per_page="10"
  audit_log="true"]

Three intentional choices:

  • filter_user_owns="parent_user_id", each parent sees only their own children’s records, scoped server-side. The hook from Step 2 ensures this works correctly.
  • allow_edit is narrow, parents can update contact details, emergency contacts, photo permissions, and pickup authorization. They cannot edit student name, DOB, grade, homeroom, enrollment status, or any counselor field. The columns aren’t in the allow_edit list.
  • audit_log="true", every parent edit is logged with timestamp + user. Year-end audit can answer “who changed this emergency contact and when?”

Parents log in via WordPress’s standard login, land on /parent-portal, and see only what they should.

Step 4: build the office-staff workspace (/staff/students)#

[gravity_table id="students"
  columns="student_first_name,student_last_name,grade,homeroom,enrollment_status,attendance_pct,parent_first_name,parent_email,phone_primary,emergency_contact_name"
  allowed_roles="office-staff,counselor,administrator"
  allow_edit="homeroom,grade,enrollment_status,attendance_pct"
  edit_permissions="enrollment_status:administrator"
  filters="grade,homeroom,enrollment_status"
  bulk="export"
  bulk_permissions="export:office-staff"
  audit_log="true"
  sort="grade:asc,student_last_name:asc"
  per_page="50"]

Notable design choices:

  • Counselor columns omitted, medical_notes, iep_status, behavioural_notes are not in the columns list, so office staff don’t see them at all. Cleaner than rendering-and-redacting; the data is never on the wire.
  • enrollment_status gated to administrator, only the principal can withdraw or graduate a student record. Office staff can update homeroom and grade routinely.
  • Sort by grade then last name, natural reading order for office work
  • Bulk action limited to export, no bulk delete; deleting student records is intentionally a one-at-a-time admin operation in the GF entries screen
  • audit_log every change for the year-end review

Step 5: build the counselor view (/staff/counselor-view)#

[gravity_table id="students"
  columns="student_first_name,student_last_name,grade,homeroom,iep_status,medical_notes,behavioural_notes,parent_email"
  allowed_roles="counselor,administrator"
  allow_edit="iep_status,medical_notes,behavioural_notes"
  filters="grade,homeroom,iep_status"
  audit_log="true"
  sort="iep_status:desc,grade:asc"]

Critical: the allowed_roles does not include office-staff or parent. Counselor data is gated at the table-view level, not just the column level. Anyone without the counselor role sees the standard 403 / login redirect.

sort="iep_status:desc" floats students with active IEPs to the top, counselors’ priority workload.

Step 6: per-counselor scoping (optional, for larger schools)#

For a school with multiple counselors and case-load assignment, add a case_counselor field (which counselor user is assigned to which student) and filter the counselor view per-user:

[gravity_table id="students"
  filter_user_owns="case_counselor"
  ...
  allowed_roles="counselor,administrator"]

The filter_user_owns parameter scopes per-counselor rows for the counselor role; administrator sees all (the override is built into the role gate).

Step 7: year-end audit export#

A WP-Cron task that exports every modification to medical/IEP fields once per year, for compliance review:

add_action('init', function () {
    if (!wp_next_scheduled('gt_school_annual_audit')) {
        // First fire: next August 1st (start of school year for most US districts)
        wp_schedule_event(strtotime('next August 1'), 'yearly', 'gt_school_annual_audit');
    }
});

add_action('gt_school_annual_audit', function () {
    // Use the REST export endpoint to generate a focused audit CSV
    $audit_csv = gt_export_audit_log([
        'form_id' => 'students',
        'fields' => ['medical_notes', 'iep_status', 'behavioural_notes'],
        'from' => 'last August 1',
        'format' => 'csv',
    ]);

    wp_mail(
        ['principal@district.edu', 'compliance@district.edu'],
        'Annual student-record audit, ' . wp_date('Y'),
        'Attached: every modification to medical / IEP / behavioural fields in the past school year.',
        [],
        [$audit_csv]
    );
});

The audit CSV becomes part of the school’s compliance file. Auditors asking “show me the IEP-status change log for the past year” get a single attachment, not a manual database query.

Step 8: what to avoid#

Don’t put medical_notes on the office-staff workspace#

The temptation is to render-and-redact (“show the column for office staff but blank the value”). Don’t. Either the column is in the columns list (data on the wire) or it isn’t. Render-and-redact creates a future scenario where someone disables the redact CSS and the data is exposed.

Don’t trust parent_user_id from form input#

The gform_pre_submission hook in Step 2 overwrites this field server-side. Don’t skip it. The entire per-user filtering pattern depends on the field being authentic.

Don’t enable bulk delete on the student form#

Student records are intentionally hard to bulk-delete. Make deletion a deliberate one-at-a-time admin operation. If your district has a “graduated/withdrawn” archival flow, use enrollment_status="graduated" + a separate filtered view for inactive students, not deletion.

Don’t skip audit_log on any view#

The year-end compliance audit relies on the audit log existing. If a view goes live without audit_log="true", edits made through it are invisible to the auditor, and “we just didn’t have logging on” doesn’t fly with FERPA / GDPR review.

Recipe variations#

Higher education (university registrar)#

Replace homeroom with program and grade with year_of_study. Add transcript_locked boolean for graduates whose records can no longer be edited. Counselor view becomes “advisor view” with advisor_notes.

Pre-school / daycare#

Drop iep_status and behavioural_notes. Keep medical_notes for allergy/medication tracking. Add nap_schedule, pickup_window, family_situation (counselor-gated). Parent portal includes daily-update fields (sleep duration, food intake) read-only for the parent, editable for staff.

After-school enrichment / clubs#

Use the same shape but smaller: student_name, parent_email, club_assignments. Skip the audit cron. Add a roster-export-by-club cron for instructor distribution.

Independent / charter school with custom intake#

Add a tuition_status field (paid / partial / unpaid), gate it to a bursar role. The pattern is the same: per-role columns + per-role views + audit.

Compliance notes#

This pattern implements:

  • Role-gated access to sensitive fields (medical, IEP, behavioural)
  • Server-side per-user filtering so parents see only their own children
  • Audit log of every modification for retroactive review
  • Server-set authentication identifier so the per-user filter cannot be tricked
  • No cross-system data transfer, student data stays in the WordPress database, never sent to third-party analytics or CRM systems

What it doesn’t implement (and a real SIS does):

  • Schedule generation / class assignments, separate workflow
  • Grade book, typically a separate gradebook product
  • Cumulative folder management (transcripts, formal assessments), separate document-management workflow
  • State reporting integration, district-specific, not generic

For most “we need a place for parents to update emergency contacts, and counselors to track IEP status, with proper privacy” jobs, this pattern is the right level of tool.