Last updated July 1st, 2024 at 04:45 pm
This post is inspired by a question that was posed in Facebook MemberPress Unofficial User Group:
Is there a way to see all of the users’ course progress in one place? I know I can access each user and see their progress that way. I was just hoping that there might be an easier way to see all of the members at once. Does anyone have any ideas on how to do this? Thanks in advance!
This is exactly what I implemented — a custom memberpress course progress page — in one of my WordPress/MemberPress websites that has three courses using the MemberPress Courses addon. In my case, I wanted to offer two views of course progress: one grouped by User; the other grouped by Course.
Overview
Screen Shots of the Final Product
Here are two screen shots displaying small sections of the course progress reports. The names and usernames of actual members are blurred out.
Course Progress Grouped by User:

Course Progress Grouped by Course:

Components of the Solution:
1. Custom Function
A custom function in the /wp-content/mu-plugins/ folder which generates the code using a WordPress shortcode: [course_progress]. The shortcode accepts one parameter: group_by, with the possible values of user and course. The full script is provided below.
2. Protected Page
I created a protected page which only site Admins can access, using a rule based on role.
Aside from the page title and a short, 2-line intro paragraph, the page consists of a single containing block — the Kadence Tabs block, with two tabs: one for Course Progress Grouped by User; the other for Course Progress Grouped by Course. I inserted the shortcode [course_progress group_by="user"] in the first tab and [course_progress group_by="course"] in the second tab.
Screen shots of shortcode embeds:


3. Protected Nav Menu Items
We already had a top-level navigation menu item called “Resources”, so I added a link to the protected page there. Using the Nav Menu Roles WordPress plugin, I hid this nav menu item from anyone who is not a logged-in Admin. (I also assigned the class name of “admin-only” to this nav menu link and used CSS to render it with red text over a yellow background to highlight it and remind Admins that no one else can see it.)

The Complete Script:
Here is the complete script. It is in a file called functions_courses.php in the wp-content/mu-plugins/ (“Must Use Plugins”) folder. I almost always use the mu-plugins folder for functionality that is unrelated to a site’s theme. You may want to read the official WordPress documentation about Must Use Plugins.
I disabled wrapping of lines because I find that hurts more than it helps. If you want to inspect the code more closely, it’s probably best to click on the “view source” button, and then copy/paste it into your script editor. Comments within the script may be enough to explain what’s going on here. If you have questions or comments, please write them in the comments section below. (Big hat tip to MemberPress support staff who have shared code snippets with me over the years, especially TylerG.)
Notes:
There are a couple of places where you will have to make replacements to match your situation.
- Line 10: I have two memberships: Follow Through Introductory Program (which is free) and Follow Through Master Program (which is paid, or premium). I’m using the Members plugin (originally by Justin Tadlock, now part of the MemberPress family) to assign custom roles to members of each membership (
customer_freeandcustomer_premium, respectively). Thus you see those role slugs in therole__inarray. - Lines 64 and 71: I’m using a ternary conditional on line 64 to obtain CSS class names for rendering each course’s information differently. The product ID for the “Master” membership is 203. If the current item in the loop is for the Master membership, the table cell will have the class of “master”; otherwise, it will have the class of “intro”.
The Code, in Sections:
Start out and initialize the “group_by” parameter (either User or Course)
function shortcode_nwb_course_progress($atts) {
$atts = shortcode_atts( array(
'group_by' => 'user',
), $atts, 'course_progress' );
$group_by = $atts['group_by'];
Fetch all users in the two memberships:
$users = get_users( /* Roles assigned to users with the two memberships; replace as needed */ array( 'role__in' => array( 'customer_free', 'customer_premium'), 'orderby' => 'name', 'order' => 'ASC', ) );
Grab all published Courses:
// Grab all courses $course_posts = get_posts(array( 'post_type' => 'mpcs-course', 'post_status' => 'publish', 'numberposts' => -1, 'orderby' => 'title', 'order' => 'ASC'), );
Start the output and start the conditional based on the value of the “group_by” parameter:
$output = ''; /* Init $output */
switch ( $group_by ) {
If group_by is “user”:
case 'user':
if(!empty($users)) {
/* Start a div */
$output .= '<div class="course-progress-wrap">';
$output .= '<h2>Course Progress Grouped by User</h2>';
$output .= '<table class="course-progress__table" style="width:auto;">';
foreach($users as $user) {
$courses = array(); // I moved down
$user_id = $user->ID;
$mepr_user = new MeprUser($user_id);
$registration_timestamp = strtotime($user->user_registered); // 2023-03-22 14:02:55
$registration_date = date('m-d-Y', $registration_timestamp);
$elapsed_days = round((time() - $registration_timestamp) / (24*60*60));
/* Now get memberships */
$this_user = MeprUtils::get_user_by('id', $user_id);
$his_products = array(); // Init
$his_product_ids = array(); // Init
$active_products = $mepr_user->active_product_subscriptions('products');
if(!empty($active_products)) {
foreach($active_products as $membership_id) {
$his_products[] = $membership_id->post_title;
$his_product_ids[] = $membership_id->ID; // for class name
}
$his_products_cos = implode(', ' , $his_products);
}
/* Product Class (YMMV)
* Since there are two courses, I want to style them differently.
* Product ID 203 is for the 'Master" class.
*/
$product_class = ( in_array(203, $his_product_ids) ) ? 'master' : 'intro';
// Now loop through courses.
if(!empty($course_posts)) {
$output .= '<tr>';
/* Add user_login */
$output .= '<td colspan="2" class="' . $product_class . '"><h3>' . $user->display_name . ' <span class="user_login">(' . $user->user_login . ')</span> - <span class="program_name ' . $product_class . '">' . $his_products_cos . '</span></h3>';
$output .= 'Registered: <span class="registration_date">' . $registration_date . '</span>';
$output .= '&nbsp;(<span class="elapsed-days">' . $elapsed_days . ' days ago</span>)';
$output .= '</td>';
foreach($course_posts as $course_post) {
if(!MeprRule::is_locked_for_user($mepr_user, $course_post)) {
$courses[] = $course_post;
}
}
foreach($courses as $course) {
$course = new memberpress\courses\models\Course($course->ID);
$progress = $course->user_progress($user_id);
$prog_pct = $progress / 100;
$output .= '<tr>';
$output .= '<td class="col-1">' . $course->post_title .'</td>';
$output .= '<td><span class="course-progress">' .
'<span class="user-progress" data-value="'. $progress . '" style="width: ' . $progress . '%;">' .
$progress .
'</span>
</span>
<span>%</span></td>';
}
$output .= '</tr>';
}
}
}
$output .= '</table>';
$output .= '</div>';
break;
If group_by is course:
case 'course':
if ( !empty($course_posts) ) {
$output .= '<div class="course-progress-wrap">';
$output .= '<h2>Course Progress Grouped by Course</h2>';
$output .= '<table class="course-progress__table" style="width:auto;">';
foreach($course_posts as $course_post) {
$courses = array();
$courses[] = $course_post;
foreach ( $courses as $course ) {
$course = new memberpress\courses\models\Course($course->ID);
$output .= '<tr>';
$output .= '<td colspan="2"><h3>' . $course->post_title . '</h3></td>';
$output .= '</tr>';
foreach($users as $user) {
$user_id = $user->ID;
$mepr_user = new MeprUser($user_id);
$output .= '<tr>';
$progress = $course->user_progress($user_id);
/* Add user_login */
$output .= '<td class="col-1">' . $user->display_name . ' (' . $user->user_login . ')</td>';
$output .= '<td><span class="course-progress">' .
'<span class="user-progress" data-value="'. $progress . '" style="width: ' . $progress . '%;">' .
$progress .
'</span>
</span>
<span>%</span>
</td>';
$output .= '</tr>';
}
}
}
$output .= '</table>';
$output .= '</div><!-- end wrap -->';
}
break;
}
Return the output and set up the shortcode:
return $output;
}
add_shortcode('course_progress', 'shortcode_nwb_course_progress');
The Full Script:
function shortcode_nwb_course_progress($atts) {
$atts = shortcode_atts( array(
'group_by' => 'user',
), $atts, 'course_progress' );
$group_by = $atts['group_by'];
$users = get_users(
/* Roles assigned to users with the two memberships; replace as needed */
array(
'role__in' => array( 'customer_free', 'customer_premium'),
'orderby' => 'name',
'order' => 'ASC',
)
);
// Grab all courses and see if the sub account has access.
$course_posts = get_posts(array(
'post_type' => 'mpcs-course',
'post_status' => 'publish',
'numberposts' => -1,
'orderby' => 'title',
'order' => 'ASC'),
);
$output = ''; /* Init $output */
switch ( $group_by ) {
case 'user':
if(!empty($users)) {
/* Start a div */
$output .= '<div class="course-progress-wrap">';
$output .= '<h2>Course Progress Grouped by User</h2>';
$output .= '<table class="course-progress__table" style="width:auto;">';
foreach($users as $user) {
$courses = array(); // I moved down
$user_id = $user->ID;
$mepr_user = new MeprUser($user_id);
$registration_timestamp = strtotime($user->user_registered); // 2023-03-22 14:02:55
$registration_date = date('m-d-Y', $registration_timestamp);
$elapsed_days = round((time() - $registration_timestamp) / (24*60*60));
/* Now get memberships */
$this_user = MeprUtils::get_user_by('id', $user_id);
$his_products = array(); // Init
$his_product_ids = array(); // Init
$active_products = $mepr_user->active_product_subscriptions('products');
if(!empty($active_products)) {
foreach($active_products as $membership_id) {
$his_products[] = $membership_id->post_title;
$his_product_ids[] = $membership_id->ID; // for class name
}
$his_products_cos = implode(', ' , $his_products);
}
/* Product Class (YMMV)
* Since there are two courses, I want to style them differently.
* Product ID 203 is for the 'Master" class.
*/
$product_class = ( in_array(203, $his_product_ids) ) ? 'master' : 'intro';
// Now loop through courses.
if(!empty($course_posts)) {
$output .= '<tr>';
/* Add user_login */
$output .= '<td colspan="2" class="' . $product_class . '"><h3>' . $user->display_name . ' <span class="user_login">(' . $user->user_login . ')</span> - <span class="program_name ' . $product_class . '">' . $his_products_cos . '</span></h3>';
$output .= 'Registered: <span class="registration_date">' . $registration_date . '</span>';
$output .= '&nbsp;(<span class="elapsed-days">' . $elapsed_days . ' days ago</span>)';
$output .= '</td>';
foreach($course_posts as $course_post) {
if(!MeprRule::is_locked_for_user($mepr_user, $course_post)) {
$courses[] = $course_post;
}
}
foreach($courses as $course) {
$course = new memberpress\courses\models\Course($course->ID);
$progress = $course->user_progress($user_id);
$prog_pct = $progress / 100;
$output .= '<tr>';
$output .= '<td class="col-1">' . $course->post_title .'</td>';
$output .= '<td><span class="course-progress">' .
'<span class="user-progress" data-value="'. $progress . '" style="width: ' . $progress . '%;">' .
$progress .
'</span>
</span>
<span>%</span></td>';
}
$output .= '</tr>';
}
}
}
$output .= '</table>';
$output .= '</div>';
break;
case 'course':
if ( !empty($course_posts) ) {
$output .= '<div class="course-progress-wrap">';
$output .= '<h2>Course Progress Grouped by Course</h2>';
$output .= '<table class="course-progress__table" style="width:auto;">';
foreach($course_posts as $course_post) {
$courses = array();
$courses[] = $course_post;
foreach ( $courses as $course ) {
$course = new memberpress\courses\models\Course($course->ID);
$output .= '<tr>';
$output .= '<td colspan="2"><h3>' . $course->post_title . '</h3></td>';
$output .= '</tr>';
foreach($users as $user) {
$user_id = $user->ID;
$mepr_user = new MeprUser($user_id);
$output .= '<tr>';
$progress = $course->user_progress($user_id);
/* Add user_login */
$output .= '<td class="col-1">' . $user->display_name . ' (' . $user->user_login . ')</td>';
$output .= '<td><span class="course-progress">' .
'<span class="user-progress" data-value="'. $progress . '" style="width: ' . $progress . '%;">' .
$progress .
'</span>
</span>
<span>%</span>
</td>';
$output .= '</tr>';
}
}
}
$output .= '</table>';
$output .= '</div><!-- end wrap -->';
}
break;
}
return $output;
}
add_shortcode('course_progress', 'shortcode_nwb_course_progress');
In Closing
I hope that the solution I’ve offered here might be of help to some of you. If so, let me know in the comments.
If you build membership websites with MemberPress, you might want to see other MemberPress-related posts I’ve written.
Like what you see? Share with others and join my mailing list. No long-term commitment, unsubscribe any time.
This was every helpful. I added the ability to open/close the list of users in each course. So the page shows the Course titles and when you select the title it opens the list of users in the course.
I would like to not show courses that has settings “Include in course listing” turned off.
Thank you!
Found it just add
$course = new memberpress\courses\models\Course($course->ID);
if ($course->status == ‘enabled’) {
}
to only show courses that has settings “Include in course listing” enabled.
Well done on both counts!
Very usefull. Let me ask a noob question, how do you call the file with the function? You said in what folder it goes, but not how i should call it! Thanks in advance!
Thanks for the feedback. Although I did explain how I embedded the shortcodes in the Protected Page section of the post, I could have made it more clear. So, I’ve added a couple of screen shots. I hope this helps.
Do you know if there’s a way to include the Reset Progress link? Best I can do so far is link to the user’s profile where I can scroll down to the Course Information table.
Interesting question. I don’t know of a better way. When I inspect the “Reset Progress” URLs in the user’s profile, they are identical for both courses. So that wouldn’t work. If you find a solution, do tell. Thanks.
Hi Jeff, thanks for the reply. The problem here is only my noobness, i’ve be certain you were clear enought for anyone who understants wordpress better than me. Here’s what i’ve done, i hope you can help me troubleshot my issue:
1 – Created the directory the folder mu-plugins inside wp-content
2 – Created a file called course_progress.php inside this folder with the code you supplied
3 – Created a admin-only accessible page for the board
3 – Created the shortcuts [course_progress group_by=”user”] and [course_progress group_by=”course”] on this page.
4- Refreshed and tryed, nothing
5 – Deleted all cache, and refreshed again, still didn’t worked.
PS: I’m using DIVI on this site (i now, it sucks!), but also tryed to create the page with the internal page builder, both didn’t worked.
Any idea of what i’m doing wrong? Sorry, i really new on this but really really need this funcionality!
And sorry for my shitty english, i hope you can understand me. If you visit Paraty in Rio de Janeiro/Brazil, let me know and i will do all my effort to make you feel very welcome! Its a very beautiful city! Greetings!
Hello, Guido – Thanks for the question. It helped me realize that I hadn’t adequately pointed out code segments you need to customize to your situation. Thanks to your question, I edited the post and added some notes (above the code section) that should help. Let me know if you have other questions. PS: Your English is fine!
Hey Jeff!
I am new to this and trying to set this up for our non-profit. I had a quick question. The code you provided, where are you putting that? Is this something you have to use an FTP for or are you inserting it somewhere from your WordPress dashboard?
Hi, Chris. I put this code in a dedicated file in the
mu-pluginsfolder. And, yes, it requires FTP’ing the file up to the server. I updated the post to make this more clear. Jump up to here for that explanation.There are plugins that allow you to add the code without having to use FTP, but it’s not my practice to use them.
Thanks Jeff!