Learn Dash Gradebook Customization

Origin Story

We had a group get in touch with us who were fairly far down the road using LearnDash for a project. I recalled seeing it in the past but had never used it myself. There were a couple things they wanted it to do that they were having trouble with and so they reached out to us. Joining in on a project late in the game is never much fun. It is best to just go with the flow rather than thinking dark thoughts around any/all of the choices made prior to your arrival. That is the path I am on. Here are choices made. Proceed to find a path forward.

As I write this I also realize how much I’ve already forgotten about the wandering path this took. One more reason to keep the blog posts rolling.

The Issues

There were three major elements they wanted to work/work differently.

  1. Etherpad integration – integrate etherpad creation and sharing with the group function in LearnDash. Jeff handled this and got it working well.
  2. Quiz results/Group integration – the goal here was to show the choices made by all individuals in the same group on the completion of the quiz. I did this but am still not in the mood to write about it.
  3. Proctor Grading/Commenting – build a minimalist interface to let proctors evaluate particular assignments for the LearnDash assigned group(s)1 they supervise.

I’ll focus on this third piece in this post but some of the other stuff might get referenced. If nothing else, I hope it helps someone else wandering around in this stuff.

Where is this data?

Turns out LearnDash documentation is not what I’d hoped. So I opted to start browsing the database directly. I’m not a fan of browsing in terminal so I used SequelPro. Even with GUI help, it’s not necessarily evident where things live. Database tables are titled fairly obtuse things like wp_wp_pro_quiz_statistic_ref.2

Things started of messy. Initially there was an another group plugin active which resulted in me finding where that plugin stored group data instead of where LearnDash stored its data. So another reminder to myself to make sure I fully understand what people have done prior to starting.

After some more sifting around, I found the data for quizzes. It’s in a couple of related tables that are fairly well labeled (wp_wp_pro_quiz_statistic, wp_wp_pro_quiz_question). I won’t get into those details here but it’s nice to have an early success so you don’t fall into deep despair.

I kept looking around and never really found any good places to find gradebook or group data. That compelled me to try some other approaches. I went into the regular WordPress backend and went to the gradebook. I looked around, looked at the URL (…/wp-admin/admin.php?page=learndash-gradebook) but it wasn’t until I clicked into a student to edit the user’s grades that I saw what I needed in the URL.

…/wp-admin/admin.php?page=learndash-gradebook-user-grades&gradebook=785&user=3&return=gradebook&referrer=%2FVCU%2Fwp-admin%2Fadmin.php%3Fpage%3Dlearndash-gradebook#ld-gb-gradebook-anchor

In that tangled mess, you’ll see the number 785. If I go to wp_posts and look it up in the database I see that it has the same title as the gradebook we’re using. Good. post_type is equal to gradebook. Super. But there’s not a ton more data there.

My next stop is wp_postmeta. I’m again looking for matches where post_id = 785. I’ve got some hits.
MySql table screenshot showing a variety of metafields populated.

The field ld_gb_components seems to be where our data is held. It starts with a:12:{i:0;a:7:{s:6:”weight”;s:1:”6″;s:2:”id . . . which is serialized data. I can maybe_userialize and dump that somewhere or I can use a site like unserialize to more easily read what this stuff says. Turns out it’s useful but not really what I want or need.

A bit more fooling around and I move to the wp_users and then the wp_usermeta tables. Here we finally hit pay dirt. We have a meta_key which is ld_gb_manual_grades_785_9 and in that is another serialized chunk of data but we also have other numbers like ld_gb_manual_grades_785_3 which unserialize like so.

While I’m in here I also find the LearnDash group membership (learndash_group_users_779) and group leadership information (learndash_group_leaders_779).

So now we know where stuff lives and we know how it’s written. I end up writing emails to people like this.

Just further documenting where stuff lives . . . (this is less obvious) but it looks like controlling the gradebook through alternate means is possible.

see function add_manual_grade( $grade ) in admin/class-ld-gb-adminpage-user-grades.php

gradebook titles/additional info . . .
in table post_meta
post_id = 785
gradebook titles/components are in ld_gb_components

actual grades
in table wp_usermeta
ld_gb_manual_grades_785_(ITEM ID)

data looks like a:1:{i:0;a:4:{s:5:”score”;d:82;s:4:”name”;s:6:”asdasd”;s:6:”status”;s:0:””;s:9:”component”;s:1:”1″;}}

Why did they write the data like that?

I do not know.

It seems really strange to me to have variables in the meta_key name that are then repeated in the meta_value. It makes it a bit of a hassle to query for as well.3

I can say I ended up writing this to deal with some of the quiz stuff.

function alt_ipd_join_stats_tables_join($user_ids, $quiz_id){
	$quiz_id = (int)$quiz_id;
	global $wpdb;
	$results = $wpdb->get_results( "SELECT wp_wp_pro_quiz_statistic_ref.statistic_ref_id, wp_wp_pro_quiz_statistic_ref.quiz_id, wp_wp_pro_quiz_statistic_ref.user_id, wp_wp_pro_quiz_statistic.statistic_ref_id, wp_wp_pro_quiz_statistic.answer_data AS answer_choice, wp_wp_pro_quiz_statistic.question_id, wp_wp_pro_quiz_statistic.correct_count, wp_wp_pro_quiz_question.title, wp_wp_pro_quiz_question.question, wp_wp_pro_quiz_question.answer_data FROM wp_wp_pro_quiz_statistic_ref INNER JOIN wp_wp_pro_quiz_statistic ON wp_wp_pro_quiz_statistic_ref.statistic_ref_id = wp_wp_pro_quiz_statistic.statistic_ref_id JOIN wp_wp_pro_quiz_question ON wp_wp_pro_quiz_question.id = wp_wp_pro_quiz_statistic.question_id WHERE (wp_wp_pro_quiz_statistic_ref.quiz_id =" . $quiz_id . " AND wp_wp_pro_quiz_statistic_ref.user_id IN (" . $user_ids . ")) ORDER BY question_id ASC");
	return $results;
}

What it is

A brief interaction with the learn dash gradebook. It is a lot of colored boxes and popup windows.

This is the default gradebook interaction for manual grade entry. It’s a bit intimidating for unskilled people and doesn’t hide people that the proctors shouldn’t be grading. So our goal is to build something simple that will let them choose on a scale of 0 to 3 and insert basic comments. The interface will only show the students they supervise and will translate their 0 to 3 ratings into the corresponding 100 point scale that goes into the gradebook.

We end up with something like this. Dropdowns restrict the entry to the four choices. A comment field exists (but has been left to the other group to style). Very simple and hopefully very easy to use.
A screenshot of a grid showing a list of students with a list of assignments across the top. Dropdown elements for grades and comments fields are there for each student/assignment.

Making Stuff Do Stuff

At this point I’m just going to do a bunch of stuff to get the right data in a place. Then I’m going to enable the right people to manipulate that data. I did some very, very wrong things with key value pairs in PHP that made my life harder but my life still went on and it all works. I’ll detail how it all works below but the audience for this is even smaller than my normal content.4

The Proctor View

I added this as a shortcode that will take the ID of the current user and display the right students (assuming they have any). That allows us to stick the shortcode on a page somewhere and point all the proctors at the same URL. It simplifies making directions etc.


function ipe_proctor_view(){
	$html = '';
	$group_members = alt_ipe_get_group_members_leader();
	$proctor_scores = [];
	$gradebook_contents = get_post_meta(785,'ld_gb_components', true);//all the gradebook info - associated w post ID which you can find via https://ipecase.org/VCU/wp-admin/edit.php?post_type=gradebook
	$gradebook = maybe_unserialize($gradebook_contents);
	$proctor_assignments = [];
	foreach ($gradebook as $key => $assignment) {
		$assignment_name = strtolower($assignment['name']);//make it lower case for match below
		if (strpos($assignment_name, 'proctor') !== false ){
			array_push($proctor_assignments, array($assignment['name'] => $assignment['id']));
		}
	}
	$html = '<div class="proctor-grades"><div class="empty-cell assignment-title assignment-cell"></div>';
	foreach ($proctor_assignments as $key => $assignment) {
		$html .= '<div class="column assignment-title">' . key($assignment) . '</div>';
	}
	foreach ($group_members as $key => $member) {
		if (isset($group_members[$key-1]['group'])){
			$check = $group_members[$key-1]['group'];
		} else {
			$check = 'foo';
		}
		if ($member['group'] !=  $check && $key != 0 ){
			$html .= '<div class="cover"> <h2>'. $member['group'] .'</h2></div>';
		}
		$html .= '<div class="proctor-assignment-cell proctor-student-name">'. key($member) . '</div>';
		$user_id = $member[key($member)];
		foreach ($proctor_assignments as $key => $assignment) {
			$assignment_id = $assignment[key($assignment)];
			$score = return_assignment_score($user_id, $assignment_id);
			$comment = return_assignment_comment($user_id, $assignment_id);     
			$html .= '<div class="proctor-assignment assignment-cell">' . selected_proctor_score($score, $user_id, $assignment_id, $comment) . '</div>';
		}

	}
	//grades are in wp_usermeta at patters like ld_gb_manual_grades_785_1 (785 being the gradebook) and 1 being the item
	//print("<pre>".print_r($proctor_assignments,true)."</pre>");	
	//print("<pre>".print_r($group_members,true)."</pre>");	
	return $html;
}
add_shortcode( 'proctor', 'ipe_proctor_view' );

You can also see in functions like the one below how having variables in the metafield names led to some weird query patterns.

//GET GRP ID FROM LEARN DASH GROUPS which is in the metadata for the logged in user
function alt_ipe_get_group_members_leader(){
	global $user;
	$user_id = get_current_user_id();//get logged in user
	$user = get_user_meta($user_id);	//get user ID
	$all_users = [];
	foreach($user as $key => $value){//cycle through metadata looking for learndash partial match
	  $i = substr($key,0,24);
	 
	  if("learndash_group_leaders_" == substr($key,0,24)){ //such a mess to do partial match	   		
	   		$users = alt_ipd_get_group_users($value[0]);//get other users who have this metadata field	
	   		$group_id =  $value[0];
	   		//print("<pre>".print_r(alt_ipd_users_for_proctor_view($users),true)."</pre>");	
	   		foreach ($users as $key => $student) {
				$name =  $student->display_name;
				array_push($all_users, array($name =>$student->ID, 'group'=>$group_id));
			}
		  }
		}	
		//print("<pre>".print_r($all_users,true)."</pre>");	
	 return $all_users;//get user ids with matching groups
	}

Building our proctor score dropdown was kind of neat. It entwines with a chunk of javascript to set the variables that are referenced on change.

function selected_proctor_score($score, $user_id, $assignment_id, $assignment_comment){
	$scores = [
			'unscored'=>'unscored',
			'0 - unsatisfactory' => 50, 
			'1 - needs improvement' => 75, 
			'2 - satisfactory' => 85, 
			'3 - excellent' => 100];
	$html = '<select name="proctor-grade" data-user="'.$user_id.'" data-assignment="'.$assignment_id.'" data-comment="'.$assignment_comment.'">' ;
		foreach ($scores as $key => $value) {
			if ($value == $score ){
				$selected = 'selected="selected"';
			} else {
				$selected = '';
			}
			$html .= '<option value="'. $value .'"' . $selected . '>' . $key . '</option>';
			//  <option value="100">3 - excellent</option>
		}
	$html .= '</select>';
	$html .= '<input class="assignment-comment" type="text" name="comment" id="comment-' . $user_id . '" value="' . $assignment_comment . '">';
	return $html;
}

function updateProctorScores(){
	console.log(this.value);
	console.log(this.dataset.user);
	console.log(this.dataset.assignment);
	console.log(this.dataset.comment);
	var score = this.value;
	var assignment_id = this.dataset.assignment;
	var user_id = this.dataset.user;
	var assignment_comment = this.dataset.comment;
	jQuery.ajax({
		url : proctor_score.ajax_url,
		type : 'post',
		data : {
			action : 'update_proctor_grades',
			user_id : user_id,
			assignment_score : score,
			assignment_id : assignment_id,
			assignment_comment : assignment_comment,
		},
		success : function( response ) {
			alert('update success')
		}
	});
}


jQuery( 'select' ).change( updateProctorScores)

let commentBoxes = document.querySelectorAll('input')
commentBoxes.forEach(function(commentBox){
	commentBox.addEventListener('input', function(evt){
    console.log(this.value)
    console.log(this.parentNode.childNodes[0].setAttribute('data-comment', this.value))
  })
})


And finally the ajax stuff that knits it together.


add_action( 'wp_ajax_update_proctor_grades', 'update_proctor_grades' );

function update_proctor_grades(){
	$user_id = $_POST['user_id'];
	$assignment_id =  $_POST['assignment_id'];
	$score =  $_POST['assignment_score'];
	$comment = $_POST['assignment_comment'];
	$db_score = array();
	array_push($db_score, array('score'=>$score, 'name'=>$comment, 'status'=>'', 'component'=>1));
	$serialized = $db_score; //it appears that it's serializing it without me
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { 
	 	 update_user_meta($user_id, 'ld_gb_manual_grades_785_' . $assignment_id, $serialized);
	 	}
	 	die();
}

This was my scratch pad for some of the layout and javascript interactions. I didn’t end up putting some of the CSS stuff in there but it’s kind of interesting to see that you could do that easily.

See the Pen
proctor layout
by Tom (@twwoodward)
on CodePen.


1 That ‘s’ was a late addition and required some reworking of the code so I write it with a degree of emphasis.

2 I also suspect that the doubling of the wp_ prefix is because of a mistake in how some stuff is written which didn’t make me feel too optimistic.

3 BUT I AM FOCUSING ON THE THINGS I CAN CONTROL! My sub-footnote is that I actually don’t care. I am like water but people like emotion in posts and I am nothing if not an audience pleaser.

4 I believe we have achieved negative numbers here.