How to migrate from Thinkific to WordPress. Part 3: Progress import

How to migrate from Thinkific to WordPress. Part 3: Progress import

4 months ago ·
· 7 min read

In part two of this 5-article series, I discussed how I implemented the WP-CLI custom command for importing users.

In part three, I will describe the functionality of the WP-CLI command for importing the progress of users regarding the courses they are enrolled in on the Thinkific platform. Below, I also provide you with the list of articles in the series:

Table of contents

Defining the progress import method

I have a separate CSV progress file for each existing course, from which I will read one by one using a recursive statement.

To begin, I define the progress import method, declare global variables, and include and call the WP_Filesystem function in WordPress.

public function import_progress() {
    global $wp_filesystem, $wpdb;
    include_once ABSPATH . 'wp-admin/includes/file.php';
    // ...

Next, I define an associative array with the list of progress files from which reading will be made and the course ID for which that progress corresponds:

$files         = array(
    'course-1-progress.csv' => 10, // Course name 1
    'course-2-progress.csv' => 11, // Course name 2
    'course-3-progress.csv' => 12, // Course name 3
    'course-4-progress.csv' => 13, // Course name 4
$file_basepath = plugin_dir_path( __FILE__ ) . 'data/progress/';

And through the foreach ( $files as $file => $course_id ) instruction, I go through the list of files and start reading from them:

$file_content = $wp_filesystem->get_contents( $file_basepath . $file );
$csv_data     = $this->parse_csv_content( $file_content );
$i            = 0;
$lesson_names = array();
$time         = time();

I reused the parse_csv_content method to transform the lines from the CSV into a vector, a method I defined in part two during the user import. I also defined the variables $i, $lesson_names, and $time, which I will use later.

Data browsing and processing

To avoid getting too tangled up, I'll include the entire section on traversing and processing the data, and then I'll add comments afterward:

foreach ( $csv_data as $line ) {
    if ( ! $i ) {
        $chapter_names = array_slice( $line, 3 );
    list($email, $completed_at, $percent_completed) = array_slice( $line, 0, 3 );
    $user = get_user_by( 'email', $email );
    if ( ! $user ) {
        \WP_CLI::error( sprintf( 'The user with email address %s does not exist.', $email ) );
    $chapter_progress = array_slice( $line, 3 );
    $count            = count( $chapter_progress );
    for ( $j = 0; $j < $count; $j++ ) {
        if ( '100' === $chapter_progress[ $j ] ) {
            $lesson_id = $wpdb->get_var(
                    "SELECT ID
                    FROM $wpdb->posts
                    WHERE post_parent = (
                        SELECT ID
                        FROM $wpdb->posts
                        WHERE post_title = %s AND post_parent = %d
                    $chapter_names[ $j ],
            if ( ! $lesson_id ) {
                \WP_CLI::error( sprintf( 'The lesson named „%s” of the course „” not found.', $chapter_names[ $j ], $course_id ) );
            $updated = update_user_meta( $user->ID, '_tutor_completed_lesson_id_' . $lesson_id, $time );
            if ( ! $updated ) {
                \WP_CLI::warning( sprintf( 'Update status: „%s”. That means failure or the value already exists.', $updated ) );

In lines 2-6, I skipped the first line of the CSV file again, which contains the column names. However, at the same time, I retained the lesson names (line 4) in the variable $lesson_names because I need them in the query below. If you recall from the first part of data preparation, the first line of the progress CSV file looked like this:

Email,Completed At,% Completed,<Nume-capitol-1>,<Nume-capitol-2>,...,<Nume-capitol-n>

With the array_slice function, I skipped the first 3 fields (Email, Completed At, and % Completed), and the remaining values were returned in the variable $lesson_names. Here, the chapter names of the course were stored.

Similarly, on line 15, the lesson progress was stored.

On line 8, I used the same function, this time to store the first 3 values.

Then, a search is performed based on the user's email address. In principle, there should be no issues with this search, as users should have been created during the user import step. Nevertheless, for any eventuality, I included a check with an error message.

Finally, starting from line 16, the progress of the lessons is iterated through.

Processing progress chapter - lesson

I mentioned at some point that I made a compromise and processed only the chapters whose progress is 100.

To clarify: the content of the courses can be organized, both in Thinkific and Tutor LMS, into chapters with lessons, topics with lessons, or whatever you want to call them. So, there are only two levels of hierarchy. Progress refers only to chapters (or topics), not to the lessons within them, and the CSV file is generated accordingly by Thinkific.

For example, a chapter with 3 lessons and a progress of 67% implies that 2 of its lessons have been completed. In reality, I don't know which of the 3 lessons have been completed, but I assumed it's the first 2, considering the natural order of progressing through a course.

Now, in Tutor LMS, the approach is as follows: progress must be set per lesson and only in the state of either completed or not completed. The field in the database for the progress of a user's course is stored in the _usermeta table as follows:

| umeta_id | user_id | meta_key                      | meta_value |
| -------- | ------- | ----------------------------- | ---------- |
| 12345    | 678     | _tutor_completed_lesson_id_90 | 1705610847 |

The user with ID 678 completed the lesson with ID 90 on the date with Unix timestamp 1705610847 (which corresponds to Thursday, January 18, 2024, at 22:47:27 GMT+0200). Lesson with ID 90 itself has a parent ID. This parent represents the chapter of that lesson.

In the Masterclass project, a single course has a chapter with 3 lessons. The rest of the chapters have one lesson each. From the perspective of progress migration, having only 8 cases where the progress of that chapter was not 100 (if I remember correctly 🤔), my job was easy. Manual updating of that progress in the database took only 2 minutes.

Search and update lesson progress

The variable $lesson_id retained, in plain language, the "ID of the post whose post_parent field is the post with the ID whose post_title is the name of the chapter AND post_parent is the ID of the course from the current iteration". I know, it sounds quite strange, but that's how the query translates 😁.

In the end, the user's _tutor_completed_lesson_id_* field is updated, and they will see the progress they had on Thinkific in the WordPress dashboard.

Closing thoughts

The progress import command is about to be executed, and once again, the magic happens. 🧙🏼‍♂️

$ wp thinkific import progress

One more step, and the migration is complete. In the next article, I will use the WordPress comments API for migrating reviews, so I invite you to check it out. 😎

If you need such a migration, click below, and let's discuss.

Contact me

Share on: