Building an ACF-Based Grade Book Plugin in 30 Minutes or Less

Origin Story

Kathy asked me if I had any grade book plugins that they could use with a faculty development course. The goal was to show faculty where they were in the course. I knew I did not wish to use Learn Dash for any number of reasons– grade book module costs extra, grade book module is super awkward, learn dash requires a fresh multisite install etc. I also had WPLMS from a few years ago when I think Jon asked to try it out. This felt pretty bloated for what we wanted and it wasn’t entirely obvious if it had a grade book in any case. The WordPress plugin repository shows three plugins and the most up to date has not been updated in 3 years.

Not a cheerful landscape. I wondered what it would take to build an ACF-based grade book relying mostly on the repeater field and simple shortcode for displaying the grades to the user. I wasn’t looking to do math or anything. I just wanted a pretty simple interface and a way for users to see their information while administrators could see all the content. That does require people to have accounts and be logged in. A bit of hassle but there is no free lunch.1 This grade book isn’t adding up things or doing weird weighting etc. It’s just a simple way to log people + assignments + scores.2 It is now known as progress book and it lives on github.

The ACF Side

We can do just about everything I can think of with one repeater field as the parent. The sub-fields will be the participant’s name, the assignment name, and the score on a scale of 1 to 100. Since we’re requiring participants to be member of the site, the participant field will be a relationship field tied to the site users. That makes the list of students automatic and makes for an easy way to recognize them when we want to show them their progress. The assignment field is a select field that has the assignments pre-populated. You’ll have to go into ACF to add assignments which isn’t optimal but the only field that lets you add choices in that interface is the checkbox field. That ends up taking up too much room if you’ve got 10 or so assignments. In this case, we should know the assignments ahead of time and even if alternations are needed, adding them via ACF is pretty straightforward. The final piece is just a slider using the range field.

That’s it. Maybe ten minutes of work with the majority of it looking at the assignments piece and trying to find a slightly nicer way to add custom values on the fly. You end up with an interface like the one below that I set to show when a page is called grade book.

One other nice piece I added was the json sync function for the plugin. It takes an extra step compared to doing this in a theme but it keeps your ACF fields nice and sync’d between development and production. This is very handy and prevents all kinds of hassle.

add_filter('acf/settings/save_json', 'my_acf_json_save_point');
function my_acf_json_save_point( $path ) {
    // update path
    $path = plugin_dir_path( __FILE__ )  . '/acf-json';
    // return
    return $path;

add_filter('acf/settings/load_json', 'my_acf_json_load_point');

function my_acf_json_load_point( $paths ) {
    // remove original path (optional)
    // append path
    $paths[] = plugin_dir_path( __FILE__ )  . '/acf-json';
    // return
    return $paths;

Displaying Grades Shortcode

The following chunk of PHP is how we display the ACF data on the front end. It’s tied to a shortcode and builds a little table-like display and uses a tiny bit of logic to show either just your stuff if you’re not an admin or all if the stuff if you are an admin– current_user_can(‘admin’) || get_current_user_id() === get_sub_field(‘student’).

function get_progress_by_user(){
		$html = '';
		if( have_rows('progress') ):
	 	// loop through the rows of data
		$html .= '<div id="your-progress" class="progress-row"><div class="progress-title">Name</div><div class="progress-title">Assignment</div><div class="progress-title">Score</div></div>';
	    while ( have_rows('progress') ) : the_row();
	    	if (current_user_can('admin') || get_current_user_id() === get_sub_field('student')){
		    	$stu_id = get_sub_field('student');
		    	$name =  get_userdata(get_sub_field('student'))->display_name;
		    	$safe_name =  sanitize_title(get_userdata(get_sub_field('student'))->display_name);
		    	$safe_assign = sanitize_title(get_sub_field('assignment'));
		        // display a sub field value
		        $html .= '<div class="progress-row ' . $safe_name . ' ' . $safe_assign .'">';
		        $html .= '<div class="progress-student" data-student="' . $safe_name . '">' . $name . '</div>';
		        $html .= '<div class="progress-assignment" data-assign="' . $safe_assign . '">' . get_sub_field('assignment') . '</div>';
		        $html .= '<div class="progress-grade">' . get_sub_field('grade') . '</div>';
		        $html .= '</div>';


	    return $html;

	else :

	    // no rows found



To make things a little easier to parse for the admins, I added a little javascript that lets you click on a participant name to hide everyone else or to click on an assignment name to hide all the other assignments.

if (document.querySelectorAll('.progress-assignment')){
	  el.addEventListener('click', function() {

	function hideStudents(theStudent){
		let names = document.querySelectorAll('.progress-student');
			if (student.dataset.student != theStudent){

	  el.addEventListener('click', function() {

	function hideAssign(theAssign){
		let names = document.querySelectorAll('.progress-assignment');
			if (assign.dataset.assign != theAssign){

1 That’s actually how I measure how bad an experience will be. If they’re willing to give you free food as bait . . . expect the worst.

2 It wouldn’t take a ton of work to figure all that out but I’m not going to encourage the facade that grading requires that kind of complexity. It’s like building a custom kitchen so you can keep making instant noodles. All the fancy math in the world isn’t going to fix the fact that assessment is fundamentally broken.

Privacy Statement