Code Your Own E-Course Platform on WordPress

E-course dashboard with lesson progress and completed lessons
This is an advanced coding tutorial. Prior PHP and general plugin creation knowledge is required.

When it came time to release my Master Customizer e-course I coded my very own e-course plugin.

Why? Three main reasons:

  1. I wasn’t interested in paying huge fees for a hosted solution (like Teachable).
  2. I wanted to be able to use my merchant account as a payment gateway, something many pre-made plugins don’t support (and no hosted solutions do).
  3. I wanted full control over the appearance of the interface, without having to override tons of existing CSS/templates.

But at the same time, there were some things I wasn’t interested in coding myself:

  1. Shopping carts.
  2. Payment gateways.
  3. Purchase records.
  4. Affiliate programmes.

I wanted the core payment and shopping system done already, but I wanted to add in all the e-course functionality myself.

Using Easy Digital Downloads as the base

I’m a huge fan of Easy Digital Downloads.

  • It’s simple.
  • It’s coded well.
  • It adds very few styles to the front-end (and even those can easily be disabled).
  • It’s so easy to hook into and modify without touching the core code.
  • It has a lot of incredibly useful add-ons.

By starting with Easy Digital Downloads as a base, I’d already have a product/checkout system in place, I’d have the payment gateways done, I’d be able to use the merchant account gateway I already had set up for my plugin and theme shop, and I’d be able to integrate AffiliateWP.

All I had to do was:

  • Set up a back-end area for e-courses and lessons.
  • Restrict all access to lessons, unless the user is logged in and has the can_view_{course_ID} capability.
  • Map each Easy Digital Downloads product to a corresponding e-course.
  • Upon checkout, scan the cart for all products, find their corresponding e-courses, and grant the customer the can_view_{course_ID} capability.
  • Style the corresponding custom post type and custom taxonomy templates in my theme.

Plugin structure

For this project I used the general structure from the WordPress Plugin Boilerplate—with a main plugin class, loader class, and separate public/admin classes. But absolutely any plugin structure will get the job done.

Deciding on the CPT and taxonomy relationship.

This was a bit tricky for me to decide on. It might still be a bit weird, but it works. And on the bright side, it’s easy for you to tweak this to suit your needs.

I decided to use an approach similar to the Post/Category relationship

  • Lessons are the custom post type.
  • Courses are a custom taxonomy (hierarchical).

So the first thing I did was register the CPT and taxonomy inside my NG_ECourse_Public class:

/**
 * Register Post Types
 *
 * Registers the 'course' post type with WordPress.
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function register_post_types() {

	$labels  = array(
		'name'               => _x( 'Courses', 'Post Type General Name', 'ng-ecourse' ),
		'singular_name'      => _x( 'Course', 'Post Type Singular Name', 'ng-ecourse' ),
		'menu_name'          => __( 'Courses', 'ng-ecourse' ),
		'parent_item_colon'  => __( 'Parent Lesson:', 'ng-ecourse' ),
		'all_items'          => __( 'All Lessons', 'ng-ecourse' ),
		'view_item'          => __( 'View Lesson', 'ng-ecourse' ),
		'add_new_item'       => __( 'Add New Lesson', 'ng-ecourse' ),
		'add_new'            => __( 'Add New', 'ng-ecourse' ),
		'edit_item'          => __( 'Edit Lesson', 'ng-ecourse' ),
		'update_item'        => __( 'Update Lesson', 'ng-ecourse' ),
		'search_items'       => __( 'Search Lesson', 'ng-ecourse' ),
		'not_found'          => __( 'Not found', 'ng-ecourse' ),
		'not_found_in_trash' => __( 'Not found in Trash', 'ng-ecourse' ),
	);
	$rewrite = array(
		'slug'       => 'lessons',
		'with_front' => true,
		'pages'      => true,
		'feeds'      => false,
	);
	$args    = array(
		'label'               => __( 'ecourse', 'ng-ecourse' ),
		'description'         => __( 'E-Courses', 'ng-ecourse' ),
		'labels'              => $labels,
		'supports'            => array( 'title', 'editor', 'comments', 'custom-fields', 'page-attributes' ),
		'taxonomies'          => array( 'course-topics' ),
		'hierarchical'        => true,
		'public'              => true,
		'show_ui'             => true,
		'show_in_menu'        => true,
		'show_in_nav_menus'   => false,
		'show_in_admin_bar'   => true,
		'menu_position'       => 26,
		'menu_icon'           => 'dashicons-welcome-learn-more',
		'can_export'          => true,
		'has_archive'         => false,
		'exclude_from_search' => true,
		'publicly_queryable'  => true,
		'rewrite'             => $rewrite,
		'capability_type'     => 'post'
	);
	register_post_type( 'ecourse', $args );

}

/**
 * Register Taxonomies
 *
 * Registers the 'course-topic' taxonomy with WordPress.
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function register_taxonomies() {

	$labels  = array(
		'name'                       => _x( 'Courses', 'Taxonomy General Name', 'ng-ecourse' ),
		'singular_name'              => _x( 'Course', 'Taxonomy Singular Name', 'ng-ecourse' ),
		'menu_name'                  => __( 'Course Topics', 'ng-ecourse' ),
		'all_items'                  => __( 'All Courses', 'ng-ecourse' ),
		'parent_item'                => __( 'Parent Course', 'ng-ecourse' ),
		'parent_item_colon'          => __( 'Parent Course:', 'ng-ecourse' ),
		'new_item_name'              => __( 'New Course Name', 'ng-ecourse' ),
		'add_new_item'               => __( 'Add New Course', 'ng-ecourse' ),
		'edit_item'                  => __( 'Edit Course', 'ng-ecourse' ),
		'update_item'                => __( 'Update Course', 'ng-ecourse' ),
		'separate_items_with_commas' => __( 'Separate courses with commas', 'ng-ecourse' ),
		'search_items'               => __( 'Search Courses', 'ng-ecourse' ),
		'add_or_remove_items'        => __( 'Add or remove courses', 'ng-ecourse' ),
		'choose_from_most_used'      => __( 'Choose from the most used courses', 'ng-ecourse' ),
		'not_found'                  => __( 'Not Found', 'ng-ecourse' ),
	);
	$rewrite = array(
		'slug'         => 'courses/topics',
		'with_front'   => true,
		'hierarchical' => false,
	);
	$args    = array(
		'labels'            => $labels,
		'hierarchical'      => true,
		'public'            => true,
		'show_ui'           => true,
		'show_admin_column' => true,
		'show_in_nav_menus' => true,
		'show_tagcloud'     => true,
		'rewrite'           => $rewrite,
		'has_archive'       => true,
	);
	register_taxonomy( 'course-topics', array( 'ecourse' ), $args );

}

These need to be hooked into the init action with the loader.

$plugin_public = new NG_ECourse_Public( $this->get_plugin_slug(), $this->get_version() );
$this->loader->add_action( 'init', $plugin_public, 'register_post_types' );
$this->loader->add_action( 'init', $plugin_public, 'register_taxonomies' );

This creates the main admin features and interfaces you need to get started building out your course material.

Interface for editing course information.

Interface for adding an e-course lesson, similar to adding a post.

I added in a few snippets of my own, using term meta to add fields to the Edit Course page for managing the modules list and setting a start date. If you’re interested in doing this, just read up on update_term_meta(). Here’s part of my code:

/**
 * Term Meta
 *
 * Adds a custom "meta" field to the "Term Edit" page.
 * The meta field adds a new module management interface.
 *
 * @param object $term
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function edit_tax_meta( $term ) {

	$modules    = get_term_meta( $term->term_id, 'modules', true );
	$start_date = get_term_meta( $term->term_id, 'start_date', true );
	?>
	<tr class="form-field">
		<th scope="row" valign="top">
			<label for="course_modules"><?php _e( 'Modules', $this->plugin_slug ); ?></label>
		</th>

		<td>
			<ul id="modules-list">
				<?php if ( is_array( $modules ) ) : ?>
					<?php foreach ( $modules as $key => $value ) : ?>
						<li data-module-name="<?php echo esc_attr( $value ); ?>">
							<?php echo esc_html( $value ); ?>
							<span class="dashicons dashicons-trash delete-module"></span>
						</li>
					<?php endforeach; ?>
				<?php endif; ?>
			</ul>
			<div id="add-module">
				<input type="text" id="add-module-name">
				<button id="add-module-button" type="button" class="button button-secondary"><?php _e( 'Add Module', $this->plugin_slug ); ?></button>
			</div>
		</td>
	</tr>

	<tr class="form-field">
		<th scope="row" valign="top">
			<label for="course_start_date"><?php _e( 'Start Date', $this->plugin_slug ); ?></label>
		</th>

		<td>
			<input type="text" id="course_start_date" name="course_start_date" placeholder="January 3rd 2016" value="<?php echo esc_attr( $start_date ); ?>">
		</td>
	</tr>
	<?php

}

/**
 * Save Taxonomy Meta
 *
 * Saves our custom taxonomy meta fields.
 *
 * @param int $term_id
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function save_tax_meta( $term_id ) {

	if ( ! isset( $_POST['course_start_date'] ) ) {
		return;
	}

	$start_date = $_POST['course_start_date'];

	update_term_meta( $term_id, 'start_date', sanitize_text_field( $start_date ) );

}

These methods are hooked in via define_admin_hooks() like so:

$this->loader->add_action( 'course-topics_edit_form_fields', $plugin_admin, 'edit_tax_meta', 10, 2 );
$this->loader->add_action( 'edited_course-topics', $plugin_admin, 'save_tax_meta', 10, 2 );

The save_tax_meta() method saves the start date field, but I use JavaScript and ajax to save the modules (not shown).

Protect the lessons

So at this point, anyone can come along and see the lessons. Not good.

Since I’m creating all my own templates, I just need to add a quick check to see if the user has permission before displaying the_content() (and so on).

First, I need two helper functions:

  • nge_can_view_lesson() – This checks to see if they can view this specific lesson. This function calls on the next function…
  • nge_can_view_course() – This checks to see if they’ve purchased the product associated with a given Course Topic (taxonomy term, remember?).

So basically the workflow is like this:

  • nge_can_view_lesson()
    • Is the person an admin? If so, give them access straight away.
    • Get the course topic associated with this lesson and trigger the next function (below).
  • nge_can_view_course()
    • Get the course ID.
    • Check to see if the current user has the view_course_{course_ID} capability. If so, they have permission. If not, they don’t.
/**
 * Permission: Can View Lesson
 *
 * Checks to see if a user can view a specific lesson, using the following
 * checks:
 *
 *  + If they're an administrator, they have permission immediately.
 *
 *  + Then gets all the course topics the lesson is associated with and
 *    performs permission checks on that. If they don't have permission
 *    for any of the topics, then they don't have permission for the lesson.
 *
 *  + Then we do a more specific check on the purchase requirements with
 *    EDD. We check to see if the person purchased the associated product
 *    (if one is selected).
 *
 * @param int $lesson_id
 *
 * @since 1.0.0
 * @return bool True/false if they don't have permission
 */
function nge_can_view_lesson( $lesson_id ) {

	// They're an administrator, so yes!
	if ( current_user_can( 'manage_options' ) ) {
		return true;
	}

	// Get all the topics for this lesson.
	$topics = wp_get_post_terms( $lesson_id, 'course-topics' );

	// There are no topics associated - bail.
	if ( ! is_array( $topics ) ) {
		return false;
	}

	// If they don't have permission for any of these topics - bail.
	foreach ( $topics as $topic ) {
		if ( nge_can_view_course( $topic ) === false ) {
			return false;
		}
	}

	// Otherwise, we're good to go!
	return true;

}
/**
 * Permission: Can View Course
 *
 * Checks to see if a user has permission to view a specific e-course.
 * This applies to the entire e-course topic taxonomy term.
 *
 * @param int|object $course_id Course ID or object.
 *
 * @since 1.0.0
 * @return bool
 */
function nge_can_view_course( $course_id ) {

	if ( is_numeric( $course_id ) ) {
		$course = get_term_by( 'id', $course_id, 'course-topics' );
	} else {
		$course = $course_id;
	}

	// User must either have course permissions or be an administrator ('manage_options').
	if ( current_user_can( 'view_course_' . $course->term_id ) || current_user_can( 'manage_options' ) ) {
		return true;
	}

	return false;

}

Now that we have the two functions, we can just stick them in single-ecourse.php. Here’s a bare bones example:

<?php if ( ! is_user_logged_in() ) : ?>

	<!-- The user isn't logged in, so maybe display a login form. -->

<?php elseif ( nge_can_view_lesson( get_the_ID() ) ) : ?>

	<!-- The user has permission to view the lesson! -->
	
	<article <?php post_class(); ?>>

		<header>
			<?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
		</header>

		<?php the_content(); ?>

	</article>

<?php else : ?>

	<!-- This person is logged in but does not have permission. Show an error message -->
	
	<p><?php _e( 'Oops, you don\'t have permission to view this lesson.', 'ng-ecourse' ); ?>

<?php endif; ?>

Obviously you’ll probably have a lot more going on in your template, but that’s the if/else set up.

Map the course topics to products

We need a way of connecting the Easy Digital Downloads products to the e-course topics. To do that, we just need a simple meta box.

Add the actions to the loader:

$this->loader->add_action( 'add_meta_boxes', $plugin_admin, 'add_meta_boxes' );
$this->loader->add_action( 'save_post', $plugin_admin, 'save_download' );

Then inside the admin class, we build out the add_meta_boxes() method:

/**
 * Add Meta Boxes
 *
 * Registers the meta boxes with WordPress.
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function add_meta_boxes() {

	// EDD Download Meta Box
	add_meta_box( 'ng_ecourse_selection', __( 'E-Course', $this->plugin_slug ), array(
		$this,
		'download_meta_box'
	), 'download', 'side', 'default' );

}

This references yet another method, download_meta_box(). That’s what renders the actual box contents.

/**
 * Meta Box: Download
 *
 * Displays the contents of the E-Course selection meta box.
 *
 * @param WP_Post $post Current post object
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function download_meta_box( $post ) {
	
	$course_topics = get_post_meta( $post->ID, 'ng_ecourse_topic', true );

	if ( ! is_array( $course_topics ) ) {
		$course_topics = array();
	}

	$args  = array(
		'hide_empty' => false,
		'fields'     => 'id=>name'
	);
	$terms = get_terms( 'course-topics', apply_filters( 'ng-ecourse/ecourse-topics-dropdown-args', $args ) );

	if ( ! is_array( $terms ) ) {
		return;
	}

	// Add a nonce field so we can check for it later.
	wp_nonce_field( 'ng_save_ecourse', 'ng_save_ecourse_nonce' );
	?>
	<label for="ng-ecourse-topic"><?php _e( 'Choose the e-course to associate with this product. Customers will be granted access to the course after purchasing this product.', $this->plugin_slug ); ?></label>
	<p>
		<select id="ng-ecourse-topic" name="ng_ecourse_topic[]" multiple>
			<?php foreach ( $terms as $course_id => $course_name ) : ?>
				<option value="<?php echo esc_attr( $course_id ); ?>"<?php echo in_array( $course_id, $course_topics ) ? ' selected' : ''; ?>><?php echo esc_html( $course_name ); ?></option>
			<?php endforeach; ?>
		</select>
	</p>
	<?php
	
}

This just displays the dropdown. You’ll notice I’m using a multi-select box in case I want one product to grant access to multiple courses (good for bundles!).

Associate a product with an e-course dropdown box

Next we need to actually save that data when we update the product. That’s done in the save_download() method we referenced before:

/**
 * Save Download
 *
 * Executes when saving an EDD download. This saves the associated
 * e-course with the product.
 *
 * @param int $post_id ID of the download being saved.
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function save_download( $post_id ) {

	/*
	 * We need to verify this came from our screen and with proper authorization,
	 * because the save_post action can be triggered at other times.
	 */

	// Check if our nonce is set.
	if ( ! isset( $_POST['ng_save_ecourse_nonce'] ) ) {
		return;
	}

	// Verify that the nonce is valid.
	if ( ! wp_verify_nonce( $_POST['ng_save_ecourse_nonce'], 'ng_save_ecourse' ) ) {
		return;
	}

	// If this is an autosave, our form has not been submitted, so we don't want to do anything.
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}

	// Check the user's permissions.
	if ( isset( $_POST['post_type'] ) && 'page' == $_POST['post_type'] ) {

		if ( ! current_user_can( 'edit_page', $post_id ) ) {
			return;
		}

	} else {

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}
	}

	if ( ! isset( $_POST['ng_ecourse_topic'] ) ) {
		return;
	}

	$topics           = $_POST['ng_ecourse_topic'];
	$sanitized_topics = is_array( $topics ) ? array_map( 'intval', $topics ) : intval( $topics );

	update_post_meta( $post_id, 'ng_ecourse_topic', $sanitized_topics );

}

Bam! Easy peasy.

The important bit: granting permission after a purchase

When someone completes a purchase, we need to go through their cart, check each product for a corresponding e-course, and grant access to those e-courses. This is done by hooking into the edd_complete_purchase action:

$this->loader->add_action( 'edd_complete_purchase', $plugin_public, 'edd_complete_purchase' );

And here’s our method inside the public class:

/**
 * Grant E-Course Capabilities
 *
 * Grants a user permission to view an e-course after purchasing
 * the associated EDD product.
 *
 * @param int $payment_id
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function edd_complete_purchase( $payment_id ) {

	$payment   = new EDD_Payment( $payment_id );
	$downloads = $payment->downloads;

	if ( ! is_array( $downloads ) ) {
		return;
	}

	// Add all the download IDs to an array.
	foreach ( $downloads as $item ) {
		$product_ids[] = $item['id'];
	}

	// Loop through each download.
	foreach ( $product_ids as $download_id ) {

		/*
		 * Grant E-Course Permissions
		 */

		// Get the associated e-course.
		$ecourse_topics = get_post_meta( $download_id, 'ng_ecourse_topic', true );

		// If there is no associated e-course, bail.
		if ( empty( $ecourse_topics ) || ! is_array( $ecourse_topics ) ) {
			continue;
		}

		// If the user isn't logged in, bail.
		if ( $payment->user_id == 0 ) {
			continue;
		}

		// Grant the user the capability to view the course.
		foreach ( $ecourse_topics as $ecourse_topic ) {
			$capability = 'view_course_' . $ecourse_topic;
			$user       = new WP_User( $payment->user_id );

			if ( ! empty( $user ) && ! is_wp_error( $user ) ) {
				$user->add_cap( $capability );
				do_action( 'ng-ecourse/edd/complete-purchase/after-capability-grant', $user, $ecourse_topic, $download_id, $payment_id );
			}
		}
		
	}

}

The one thing you’ll notice here is that no capability is granted if the user isn’t logged in. It’s essential that you force registration/login at checkout. We can’t give people without an account access to private material.

Remove access to e-course after refunding a purchase

If you refund someone, you probably want to revoke access to the e-course material. That’s where this nifty snippet comes in. We can use EDD’s edd_update_payment_status action to execute code when the payment status changes (like from completed to refunded).

$this->loader->add_action( 'edd_update_payment_status', $plugin_public, 'edd_update_payment_status', 10, 3 );

And our method:

/**
 * EDD Update Payment Status
 *
 * Monitors when a payment is updated.
 *
 * @param int    $payment_id ID of the payment being updated
 * @param string $new_status New status of the payment
 * @param string $old_status Old status of the payment
 */
public function edd_update_payment_status( $payment_id, $new_status, $old_status ) {

	// Get the variables we need.
	$payment_meta = get_post_meta( $payment_id, '_edd_payment_meta', true );
	$user_info    = $payment_meta['user_info'];
	$cart_details = $payment_meta['cart_details'];
	$product_ids  = array();

	if ( ! is_array( $cart_details ) ) {
		return;
	}

	// Add all the download IDs to an array.
	foreach ( $cart_details as $item ) {
		$product_ids[] = $item['id'];
	}

	// Loop through each download.
	foreach ( $product_ids as $download_id ) {
		// Get the associated e-course.
		$ecourse_topics = get_post_meta( $download_id, 'ng_ecourse_topic', true );

		// If there is no associated e-course, bail.
		if ( empty( $ecourse_topics ) || ! is_array( $ecourse_topics ) ) {
			continue;
		}

		// If the user isn't logged in, bail.
		if ( $user_info['id'] == 0 ) {
			continue;
		}

		foreach ( $ecourse_topics as $ecourse_topic ) {
			$capability = 'view_course_' . $ecourse_topic;
			$user       = new WP_User( $user_info['id'] );

			// Remove course permissions.
			if ( ( $new_status == 'refunded' || $new_status == 'revoked' ) && user_can( $user, $capability ) ) {

				$user->remove_cap( $capability );
				do_action( 'ng-ecourse/edd/revoked-course-permission', $user, $download_id );

			} elseif ( $new_status != 'refunded' && $new_status != 'revoked' && ! user_can( $user, $capability ) ) {

				// Add course permission.
				$user->add_cap( $capability );
				do_action( 'ng-ecourse/edd/add-course-permission', $user, $download_id );

			}
		}
	}

}

Extra: Manually grant and remove permissions

I decided to add in an extra feature that lets me manage someone’s access. That means I can grant them permission to a course without making them pay for it (beta testers!), or I can revoke access without refunding their money. This is done by adding some extra information and fields to the user profile screen.

Checkboxes for granting access to e-courses

These are the actions we need added to the loader:

$this->loader->add_action( 'show_user_profile', $plugin_admin, 'show_user_profile' );
$this->loader->add_action( 'edit_user_profile', $plugin_admin, 'show_user_profile' );
$this->loader->add_action( 'personal_options_update', $plugin_admin, 'save_user_profile' );
$this->loader->add_action( 'edit_user_profile_update', $plugin_admin, 'save_user_profile' );

Then we have two methods to build out in the admin class: show_user_profile() (for displaying the fields) and save_user_profile() (for saving them).

/**
 * User Profile Fields
 *
 * Adds new profile fields for e-course permissions. This allows you to
 * manually grant or remove permission to view an e-course.
 *
 * @param WP_User $user
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function show_user_profile( $user ) {

	if ( ! current_user_can( 'edit_users' ) ) {
		return;
	}

	$topics = get_terms( 'course-topics', array( 'hide_empty' => false ) );

	if ( ! is_array( $topics ) ) {
		return;
	}

	?>
	<hr>
	<h3><?php _e( 'E-Course Permissions', $this->plugin_slug ); ?></h3>
	<table class="form-table">
		<?php foreach ( $topics as $topic ) : ?>
			<?php $capability = user_can( $user, 'view_course_' . $topic->term_id ); ?>
			<tr>
				<th>
					<label for="ng-ecourse-<?php echo intval( $topic->term_id ); ?>"><?php echo esc_html( $topic->name ); ?></label>
				</th>
				<td>
					<input type="checkbox" id="ng-ecourse-<?php echo intval( $topic->term_id ); ?>" name="ng_view_ecourse_<?php echo intval( $topic->term_id ); ?>" value="yes" <?php checked( true, $capability ); ?>> <?php _e( 'Yes', $this->plugin_slug ); ?>
				</td>
			</tr>
		<?php endforeach; ?>
	</table>
	<?php

}

The save method needs to add and remove capabilities based on what options were selected:

/**
 * Save Profile
 *
 * Runs when a user profile is updated. This grants or removes
 * permissions to e-courses.
 *
 * @param int $user_id The ID of the user being saved
 *
 * @access public
 * @since  1.0.0
 * @return void
 */
public function save_user_profile( $user_id ) {

	if ( ! current_user_can( 'edit_user', $user_id ) ) {
		return;
	}

	$user   = new WP_User( $user_id );
	$topics = get_terms( 'course-topics', array( 'hide_empty' => false ) );

	if ( ! is_array( $topics ) ) {
		return;
	}

	foreach ( $topics as $topic ) {
		// Get the value we saved.
		$permission = $_POST[ 'ng_view_ecourse_' . $topic->term_id ];

		// Grant permission.
		if ( $permission == 'yes' && ! user_can( $user, 'view_course_' . $topic->term_id ) ) {
			$user->add_cap( 'view_course_' . $topic->term_id );
		} elseif ( $permission != 'yes' && user_can( $user, 'view_course_' . $topic->term_id ) ) {
			$user->remove_cap( 'view_course_' . $topic->term_id );
		}
	}

}

Customize it to your heart’s desire

E-course interface showing the lesson list for a course

Everything I’ve explained to you so far is the core functionality. I have a lot more going on in my own plugin to get the exact design and functionality I want. Including:

  • Custom meta box on lesson page to assign lessons to modules (loading module list via ajax when course topic is selected).
  • Entirely separate templating system within the plugin so I can activate the plugin on multiple sites without needing to style the templates each time (hooking into template_include).
  • Option to restrict lessons only to specific variable pricing options within a product.
  • Allow students to mark lessons as completed.

But if you’ve gotten this far, then I trust you have the coding know-how to style templates and create some custom functionality all on your own. 😉

Photo of Ashley
I'm a 30-something California girl living in England (I fell in love with a Brit!). My three great passions are: books, coding, and fitness. more »

Don't miss my next post!

Sign up to get my blog posts sent directly to your inbox (plus exclusive store discounts!).

You might like these

14 comments

    1. That hasn’t changed yet, but it will in the coming weeks. 🙂 I’m rewriting the course a bit and once that’s done, I’ll put it on the new platform.

      If you want to check it out, you can sign up for my free course Make WordPress Your Bitch. That’s been moved over to the new platform. 🙂

      1. Ok, was in that free course previously, signed up again..Definitely loving the new look.

        As for the Theme course though, seems that even after finding it in the shop and being logged in, it doesn’t actually take me to course content, it just shows me the buy page, and when I check under my account same thing, just shows me my previous purchase but no way to get to the course details itself. Looking forward to when it’s also moved over and I’m happy videos are going to be included now, that definitely helps too along with explicitly written out codes 🙂

        Sasha-Shae recently posted: Body Confident with Target Style
  1. Ashley, this is an awesome tutorial and definitely gives options to those who don’t want to invest in other platforms.

    I host my courses on Teachable now, for a myriad of reasons, but mainly for the simplicity of the interface (for the most part), but largely for the fact that the hosting of videos is included and I’m not having to use my host bandwidth to host additional files.

    I’ll definitely be keeping your tutorial on hand for future reference, however. As a developer and designer myself, I love the fact that I would be able to customize my own course platform to my hearts desire. Thanks again for sharing!

    1. I totally understand. 🙂 Teachable is a very nice platform.

      I’ve bee using Vimeo to host my videos. I use the Plus plan, which is £7.95 per month. That gives me plenty of space and also lets me lock down my videos to specific domains so they can’t be shared or viewed elsewhere.

  2. I <3 YOU! How in the heck did you know I needed this info? I still have to finish the master course (I got a little stuck…then needed to focus on graduating nutrition school in a couple months). My big vision is putting together a mod course offering mini lessons about everything I've learned. Your course is awesome, professional, and easy to follow along so you totally have my interest. I'm saving this post so that I can find it when I'm ready. Is your master course time sensitive or am I ok taking a small break and getting back to it in May? Thanks!

    1. You have lifetime access to the material so it’s totally okay to take a break and come back to it when you’re ready. 🙂

      Feel free to email me at support@nosegraze.com if you need help with a lesson!

  3. I just started thinking about coding my own course plugin because I don’t want to use outside sites and the plugins that are out there just don’t speak to me. 😀

  4. Great tutorial! This helped me get a bit more clear on how I want to manage my own membership site and ecourse. I had been planning on creating separate custom post types for the membership and course, but I really like your approach of just one custom post type and using a custom taxonomy to distinguish instead.

    Any reason you decided to code custom content restrictions instead of using the EDD restrict content add-on? I was planning to use that and the recurring payments add-on to manage my membership content and eventual ecourse. Will probably still do that since it’s already set up to check for active subscriptions, and I’d like to be able to set up monthly or yearly options for the subscriptions, and payment plans for my ecourse. But I’d love to hear your thoughts when you get a chance!

    1. You definitely could go with EDD Restrict Content! I guess I just wanted more fine-tuned control over things. Plus I thought it would be annoying to have to remember to restrict every single lesson. With my approach you just map the download to the taxonomy, then that’s it. You just need to add each lesson to the right course topic. You don’t need to worry about forgetting to restrict each individual lesson.

      But EDD Restrict Content would still do the trick if you want to use it. 🙂

      1. Ooooooh, good point. I’ll be adding new content regularly for the membership site, so that’s definitely something to consider. But I had also thought about using their shortcode to give non-members content previews anyway. I’ll have to think about which is the greater priority. Thanks!

  5. I totally agree with you on the Teachable fees especially when we AWESOME coders can definitely thump our noses and say “WE have our own solution!” This was my next question to you after taking your master customizer. Damn I knew this could be done. THANK YOU for sharing this!!!!!
    Glad to be a part of your Facebook group!!!

Recent Posts

    Random Posts