• Skip to main content
  • Skip to primary sidebar
  • Skip to footer

nSiteful Web Builders

Building a Better Web - One Site at a Time.

  • Home
  • About
    • Testimonials
    • Resources
    • Partner With Me
    • Frequently Asked Questions
  • Web Sites
  • Online Marketing
  • WordPress Support
    • Customized WordPress Training
    • 60-for-60 Sessions
  • Web Applications
  • Blog
    • At-A-Glance
    • Blog Articles Grouped by Category
    • Case Studies
    • General
    • Portfolio
    • Reviews
    • Snippets
    • Techniques
    • Projects
  • Contact
    • Purchase Retainer Consulting Hours
    • About Retainer Consulting Hours
    • Book a Meeting with Jeff
    • Tell Me About Your Web Project
    • nSiteful Newsletter Archives
    • nSiteful Rewards

By Jeff - 8/30/2023
Cats: Snippets · Tags: MemberPress, MemberPress Courses, Membership Sites, WordPress

snippets

MemberPress: How to Create a Custom MemberPress Course Progress Page

On This Page...

Toggle
  • Overview
    • Screen Shots of the Final Product
      • Course Progress Grouped by User:
      • Course Progress Grouped by Course:
    • Components of the Solution:
      • 1. Custom Function
      • 2. Protected Page
      • 3. Protected Nav Menu Items
  • The Complete Script:
    • Notes:
    • The Code, in Sections:
    • The Full Script:
  • In Closing

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:

Custom MemberPress Course Progress Page
Course Progress Grouped by User

Course Progress Grouped by Course:

Custom MemberPress Course Progress Page
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_free and customer_premium, respectively). Thus you see those role slugs in the role__in array.
  • 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 .= '&amp;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 .= '&amp;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.

Related Posts

  1. How to Override MemberPress Template Files
  2. Avoid Direct Styling; Use CSS Instead
  3. Give Gutenberg a Chance
  4. How to Make a Stale Website Fresh
  5. MemberPress: Create a Custom Shortcode to Display a MemberPress Membership Price ANYWHERE on Your Website

Reader Interactions

Comments

  1. Jim Young says

    December 5, 2023 at 5:15 pm

    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!

    Reply
  2. Jim Young says

    December 6, 2023 at 10:30 am

    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.

    Reply
    • Jeff Cohan says

      February 9, 2024 at 10:11 am

      Well done on both counts!

      Reply
  3. Guido says

    February 7, 2024 at 4:46 am

    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!

    Reply
    • Jeff Cohan says

      February 7, 2024 at 10:09 am

      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.

      Reply
  4. David says

    February 9, 2024 at 1:07 am

    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.

    Reply
    • Jeff Cohan says

      February 9, 2024 at 10:24 am

      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.

      Reply
  5. Guido says

    February 9, 2024 at 7:40 am

    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!

    Reply
    • Guido says

      February 9, 2024 at 7:49 am

      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!

      Reply
    • Jeff Cohan says

      February 9, 2024 at 10:10 am

      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!

      Reply
  6. Chris says

    February 16, 2024 at 12:25 pm

    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?

    Reply
    • Jeff Cohan says

      February 16, 2024 at 1:22 pm

      Hi, Chris. I put this code in a dedicated file in the mu-plugins folder. 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.

      Reply
      • Chris says

        February 16, 2024 at 1:34 pm

        Thanks Jeff!

        Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Primary Sidebar

mailchimp signup

Subscribe to get notified when new articles are published. Unsubscribe any time. No spam. I promise. Check out my newsletter archives.

social

Twitter Facebook LinkedIn

Recent Articles

  • Post-Specific CSS for WordPress October 5, 2026
  • You built an online course, but they haven’t come. September 16, 2026
  • Custom MemberPress Pricing Pages December 19, 2025
  • CSS Data Attribute Override December 11, 2025
  • MemberPress Rules Using Custom Taxonomies and Custom Roles December 10, 2025

Filter By Category/Tag

Categories

  • Case Studies (8)
  • For Staff (1)
  • General (73)
  • Portfolio (7)
  • Reviews (14)
  • Snippets (24)
  • Techniques (52)

Popular Tags

Advanced Custom Fields Blogging Child Themes Content Marketing CSS Customer Service Custom Fields Custom Post Types Diagnostics Domain Names Facebook FooGallery Genesis Hosting HTML Images iPhone Libra Live Chat Marketing Media MemberPress MemberPress Courses Membership Sites Mobile-Friendly MySQL Photo Gallery php Pinterest Plugins Post Formats Pricing Project Management SEBA SEO Seth Godin Shortcodes Social Networking Surveys Taxonomies Twitter Video Web design Web forms WordPress

Footer

Background

Web Sites | WordPress Support | Web Applications.

Formally trained in liberal arts and education (I have a B.A. in Government from Harvard and studied Secondary Education at Rutgers Graduate School), I have honed my skills in the communication arts and sciences as a teacher, trainer, instructional designer, writer, photographer, calligrapher, helpdesk manager, database programmer, and multimedia developer.

(I've also been a group counselor, waiter, bartender, bicycle messenger boy, computer salesman, carpenter's helper, financial analyst, and school board president.)

Tech

Systems since 1983.
Web sites since 1994.
PHP since 2001.
WordPress since 2007.

Contact

Book Meeting
770-772-5134
Email Jeff
Send Money
All Ways

Copyright 2026, nSiteful Web Builders, Inc.

Cookies Consent
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes. The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
View preferences
  • {title}
  • {title}
  • {title}
https://iframe.mediadelivery.net/embed/392008/42d18bc1-2adc-4741-b733-053d08d09c32
https://vz-000c5976-3ab.b-cdn.net/42d18bc1-2adc-4741-b733-053d08d09c32/play_720p.mp4

Receive occasional emails from Jeff