<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vlad Ilie</title>
    <description>The latest articles on DEV Community by Vlad Ilie (@vladilie).</description>
    <link>https://dev.to/vladilie</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F528423%2F54793d26-d1f3-4841-bb3e-f62701d9e463.jpeg</url>
      <title>DEV Community: Vlad Ilie</title>
      <link>https://dev.to/vladilie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladilie"/>
    <language>en</language>
    <item>
      <title>Migrate from Thinkific to WordPress. #5 Other adaptations and conclusions</title>
      <dc:creator>Vlad Ilie</dc:creator>
      <pubDate>Wed, 12 Mar 2025 08:32:43 +0000</pubDate>
      <link>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2</link>
      <guid>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi"&gt;fourth part&lt;/a&gt; of this 5-article series, I concluded the implementation of functions for custom import commands for users, progress, and reviews.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;last part&lt;/strong&gt;, I will talk about the customizations I made to the Masterclass online site and specifically go through the adaptations I made to the theme, &lt;code&gt;Tutor LMS&lt;/code&gt; templates, and &lt;code&gt;WooCommerce&lt;/code&gt;. Below is the list of articles in the series:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Table of contents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j"&gt;Part 1: Planning and data preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;Part 2: Users import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em"&gt;Part 3: Progress import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi"&gt;Part 4: Reviews import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Part 5: Other adaptations and conclusions&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;Tutor LMS&lt;/code&gt; dashboard
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Tutor LMS&lt;/code&gt; user dashboard on the Masterclass online site looked something like this:"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplbsi5823apv3lb42wjy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplbsi5823apv3lb42wjy.png" alt="Tutor LMS user control panel" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It would have been a bit complicated to merge the two control panels, the one from &lt;code&gt;WooCommerce&lt;/code&gt; and the one shown in the picture from &lt;code&gt;Tutor LMS&lt;/code&gt;, into a single one, so I just linked them together. If the user needs information about orders, they can find it in the &lt;code&gt;WooCommerce&lt;/code&gt; panel; if they need information about the courses they have enrolled in, they can find it in the &lt;code&gt;Tutor LMS&lt;/code&gt; panel, both accessible through direct links.&lt;/p&gt;

&lt;p&gt;To add the "Order Dashboard" link (marked with green), I overwrote the &lt;code&gt;/dashboard.php&lt;/code&gt; template of &lt;code&gt;Tutor LMS&lt;/code&gt;: I copied the contents of the &lt;code&gt;/wp-content/plugins/tutor/templates&lt;/code&gt; directory to the &lt;code&gt;/wp-content/themes/child-theme/tutor&lt;/code&gt; directory and added the following item to the navigation links list (inside the &lt;code&gt;&amp;lt;ul class="tutor-dashboard-permalinks"&amp;gt;&lt;/code&gt; element):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/tutor/dashboard.php

&amp;lt;li class="tutor-dashboard-menu-item tutor-dashboard-menu-my-profile "&amp;gt;
    &amp;lt;a class="tutor-dashboard-menu-item-link tutor-fs-6 tutor-color-black" href="&amp;lt;?php echo esc_url( wc_get_page_permalink( 'myaccount' ) ); ?&amp;gt;"&amp;gt;
        &amp;lt;span class='tutor-icon-arrow-right-left tutor-dashboard-menu-item-icon'&amp;gt;&amp;lt;/span&amp;gt;
        &amp;lt;span class='tutor-dashboard-menu-item-text tutor-ml-12'&amp;gt;Woo panel&amp;lt;/span&amp;gt;
    &amp;lt;/a&amp;gt;
&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, to remove the links marked with red in the image above, because such functionalities are not needed, I added the following filter to the &lt;code&gt;functions.php&lt;/code&gt; file in the theme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/tutor/functions.php

// Remove the page tabs from the panel sidebar.
add_filter(
    'tutor_dashboard/nav_items',
    function( $defaults ) {
        unset( $defaults['wishlist'] );
        unset( $defaults['my-quiz-attempts'] );
        unset( $defaults['question-answer'] );

        return $defaults;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;WooCommerce&lt;/code&gt; dashboard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsjmekewv3feykowp8wf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsjmekewv3feykowp8wf.png" alt="WooCommerce user control panel" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To add the link to the &lt;code&gt;Tutor LMS&lt;/code&gt; dashboard in the navigation menu of &lt;code&gt;WooCommerce&lt;/code&gt;, for overriding, I copied the template &lt;code&gt;/wp-content/plugins/woocommerce/templates/myaccount/navigation.php&lt;/code&gt; to the directory &lt;code&gt;/wp-content/themes/child-theme/woocommerce/myaccount/navigation.php&lt;/code&gt; and added the following element as the first item in the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/woocommerce/myaccount/navigation.php

&amp;lt;li class="woocommerce-MyAccount-navigation-link woocommerce-MyAccount-navigation-link--dashboard"&amp;gt;
    &amp;lt;a href="&amp;lt;?php echo esc_url( tutor_utils()-&amp;gt;tutor_dashboard_url() ); ?&amp;gt;"&amp;gt;&amp;lt;strong&amp;gt;Courses panel&amp;lt;/strong&amp;gt;&amp;lt;/a&amp;gt;
&amp;lt;/li&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;Tutor LMS&lt;/code&gt; authentication form
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2r0so0i6mzmldjq0x0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy2r0so0i6mzmldjq0x0w.png" alt="Tutor LMS login form" width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During the migration and dealing with user accounts, I didn't want to make it intrusive and, especially, to violate the General Data Protection Regulation. So, I approached this differently by notifying users through a message in the login form that the site has moved to another platform. If they had an account before, they could request a password reset link to log in. To add this message, I used hooks in the &lt;code&gt;functions.php&lt;/code&gt; file of the theme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/functions.php

// Add custom message in the Tutor LMS login form.
add_action(
    'tutor_login_form_middle',
    function () {
        $lost_pass_url = wp_lostpassword_url();
        ?&amp;gt;
        &amp;lt;p&amp;gt;We recently migrated the site to a new platform. If you have an account before, please &amp;lt;a href="&amp;lt;?php echo esc_url( $lost_pass_url ); ?&amp;gt;"&amp;gt;reset your password&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
        &amp;lt;?php
    }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other filters for &lt;code&gt;WooCommerce&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Tutor LMS&lt;/code&gt; being integrated with the &lt;code&gt;WooCommerce&lt;/code&gt; e-commerce engine, each course in &lt;code&gt;Tutor LMS&lt;/code&gt; has a corresponding product linked in &lt;code&gt;WooCommerce&lt;/code&gt;. Unfortunately, &lt;code&gt;Tutor LMS&lt;/code&gt; does not automatically hide &lt;code&gt;WooCommerce&lt;/code&gt; products specifically created for courses.&lt;br&gt;
Consequently, these products could be added to the cart during a purchase, both as products and courses. To avoid such situations, I use the filter below, which hides products from areas like the shop page and related products.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/functions.php

// Hide products from Woo shop page.
add_filter( 'woocommerce_product_is_visible', '__return_false' );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that's not enough.&lt;br&gt;
Products would still be accessible at a direct address, for example: &lt;code&gt;https://mclass.test/product/product-name&lt;/code&gt;. Applying the filter below disables access to product pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/functions.php

// Remove single page for Woo products.
add_filter(
    'woocommerce_register_post_type_product',
    function ( $args ) {
        $args['publicly_queryable'] = false;
        $args['public']             = false;

        return $args;
    },
    12
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A final filter that I used is for removing the "quantity" field from the shopping cart page to prevent increasing the quantity. It wouldn't make sense for a user to purchase a quantity greater than 1 for a course.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/functions.php

// Hide quantity input field from cart.
add_filter( 'woocommerce_is_sold_individually', '__return_true' );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, to remove the quantity field from the cart, I copied the template &lt;code&gt;/wp-content/plugins/woocommerce/templates/cart/cart.php&lt;/code&gt; to &lt;code&gt;/wp-content/themes/child-theme/woocommerce/cart/cart.php&lt;/code&gt; to override it, and then removed the following lines from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /wp-content/themes/child-theme/woocommerce/cart/cart.php

&amp;lt;th class="product-quantity"&amp;gt;&amp;lt;?php esc_html_e( 'Quantity', 'woocommerce' ); ?&amp;gt;&amp;lt;/th&amp;gt;

// AND

&amp;lt;td class="product-quantity" data-title="&amp;lt;?php esc_attr_e( 'Quantity', 'woocommerce' ); ?&amp;gt;"&amp;gt;
&amp;lt;?php
if ( $_product-&amp;gt;is_sold_individually() ) {
    $min_quantity = 1;
    $max_quantity = 1;
} else {
    $min_quantity = 0;
    $max_quantity = $_product-&amp;gt;get_max_purchase_quantity();
}

$product_quantity = woocommerce_quantity_input(
    array(
        'input_name'   =&amp;gt; "cart[{$cart_item_key}][qty]",
        'input_value'  =&amp;gt; $cart_item['quantity'],
        'max_value'    =&amp;gt; $max_quantity,
        'min_value'    =&amp;gt; $min_quantity,
        'product_name' =&amp;gt; $product_name,
    ),
    $_product,
    false
);

echo apply_filters( 'woocommerce_cart_item_quantity', $product_quantity, $cart_item_key, $cart_item ); // PHPCS: XSS ok.
?&amp;gt;
&amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migration to production
&lt;/h2&gt;

&lt;p&gt;After all the changes were made, migrations executed, courses arranged, and the store prepared with all the integrations, I moved the site from the local environment to the &lt;code&gt;production&lt;/code&gt; environment using the &lt;code&gt;All in One WP Migration&lt;/code&gt; plugin. Throughout the development process of the new site, after each major step (creating courses and building their pages with &lt;code&gt;Elementor&lt;/code&gt;, installing and configuring &lt;code&gt;WooCommerce&lt;/code&gt;, running migrations, etc.), I made a backup with &lt;code&gt;All in One WP Migration&lt;/code&gt;. If I had identified any data discrepancies or a malfunctioning part of the site while working on the next step, I would have reverted to the previous state of the site. In essence, I would have performed a &lt;code&gt;roll-back&lt;/code&gt;, and this way, I could avoid losing too much progress.&lt;/p&gt;

&lt;p&gt;When I had a backup very close to a version ready for production, I placed it on the production subdomain, and that's how I had a smooth development-launch process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;WordPress once again provides a solution to a problem I encountered. The related technologies developed around it are truly a breath of fresh air, and if you enjoy this environment, it's worth investing time and energy. Certainly, impressive results can be achieved using &lt;code&gt;WordPress&lt;/code&gt;, even for enterprise projects—no need to say that, Automattic proves it every day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wp-cli.org" rel="noopener noreferrer"&gt;WP-CLI&lt;/a&gt; is a very powerful and flexible tool that can be helpful in many situations like these migrations. I recommend it every time I have the chance; I've used it in dozens of projects, and each time, I've successfully accomplished everything I needed with it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was first published on &lt;a href="https://vladilie.ro/blog/migrate-thinkific-wordpress-part-5-adaptations-conclusions" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>thinkific</category>
      <category>wpcli</category>
      <category>migration</category>
    </item>
    <item>
      <title>Migrate from Thinkific to WordPress. #4 Reviews import</title>
      <dc:creator>Vlad Ilie</dc:creator>
      <pubDate>Wed, 12 Mar 2025 08:32:40 +0000</pubDate>
      <link>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi</link>
      <guid>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em"&gt;previous article&lt;/a&gt;, the third part of this 5-article series, I described the implementation of the &lt;code&gt;WP-CLI&lt;/code&gt; command to import the progress of courses for users.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;part four&lt;/strong&gt;, I will describe the functionality of the &lt;code&gt;WP-CLI&lt;/code&gt; command to import reviews given by users to the courses.&lt;br&gt;
Below, I also provide you with the list of articles in the series:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Table of contents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j"&gt;Part 1: Planning and data preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;Part 2: Users import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em"&gt;Part 3: Progress import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Part 4: Reviews import&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2"&gt;Part 5: Other adaptations and conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Declaring the review import method
&lt;/h2&gt;

&lt;p&gt;I start again by declaring the method and the helper variables for reading from the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

public function import_reviews(): void {
    global $wp_filesystem, $wpdb;

    include_once ABSPATH . 'wp-admin/includes/file.php';
    WP_Filesystem();

    $files = array( 'course-1-reviews.json', 'course-2-reviews.json', 'course-3-reviews', 'course-4-reviews.json' );
    //...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also declared a variable of type array with the list of files containing reviews for courses, which I will iterate through using &lt;code&gt;foreach ($files as $file)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading from file
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;JSON&lt;/code&gt; file structure
&lt;/h3&gt;

&lt;p&gt;I'm putting here again the structure of the review file in &lt;code&gt;JSON&lt;/code&gt; format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// course-*-reviews.json

{
  "@context": "http://schema.org",
  "@type": "Product",
  "name": "&amp;lt;course-name&amp;gt;",
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.6", "reviewCount": 5 },
  "review": [
    {
      "@type": "Review",
      "author": { "@type": "Person", "name": "&amp;lt;reviewer-name&amp;gt;" },
      "description": "&amp;lt;review-description&amp;gt;",
      "name": "&amp;lt;review-title&amp;gt;",
      "date": "November 26, 2021",
      "reviewRating": { "@type": "Rating", "bestRating": 5, "ratingValue": 5, "worstRating": 0 }
    },
    // ...
  ],
    "image": "&amp;lt;course-image-url&amp;gt;"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As in the previous articles, reading from files is done in a similar way, but this time I won't use the helper method &lt;code&gt;parse_csv_content&lt;/code&gt; to compose the vector with lines from the &lt;code&gt;CSV&lt;/code&gt; file.&lt;br&gt;
Instead, I will use the &lt;code&gt;json_decode&lt;/code&gt; function to interpret the &lt;code&gt;JSON&lt;/code&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

$file_path    = plugin_dir_path( __FILE__ ) . '/data/reviews/' . $file;
$file_content = $wp_filesystem-&amp;gt;get_contents( $file_path );
$json         = json_decode( $file_content );

$course_name  = $json-&amp;gt;name;
$review_count = count( $json-&amp;gt;review );

$course = get_page_by_title( $course_name, OBJECT, 'courses' );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also declared a few more variables that will be useful later on.&lt;br&gt;
I made sure that the course &lt;code&gt;names&lt;/code&gt; in the review files in the name field &lt;strong&gt;match&lt;/strong&gt; the names of the courses created in &lt;code&gt;Tutor LMS&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Processing reviews
&lt;/h2&gt;

&lt;p&gt;I then started looping through the reviews in the &lt;code&gt;review&lt;/code&gt; array in the &lt;code&gt;JSON&lt;/code&gt; and proceeded like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php
foreach ( $json-&amp;gt;review as $review ) {
    $user_id = $this-&amp;gt;search_user_by_name( $review-&amp;gt;author-&amp;gt;name );
    $new_comment  = wp_insert_comment(
        array(
            'comment_post_ID'  =&amp;gt; $course-&amp;gt;ID,
            'comment_author'   =&amp;gt; $review-&amp;gt;author-&amp;gt;name,
            'comment_date'     =&amp;gt; gmdate( 'Y-m-d H:i:s', strtotime( $review-&amp;gt;date ) ),
            'comment_content'  =&amp;gt; $review-&amp;gt;description,
            'comment_agent'    =&amp;gt; 'TutorLMSPlugin',
            'comment_type'     =&amp;gt; 'tutor_course_rating',
            'comment_approved' =&amp;gt; 'approved',
            'user_id'          =&amp;gt; $user_id,
            'comment_meta'     =&amp;gt; array( 'tutor_rating' =&amp;gt; $review-&amp;gt;reviewRating-&amp;gt;ratingValue ), // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
        )
    );

    if ( is_wp_error( $new_comment ) ) {
        \WP_CLI::warning( sprintf( 'Review of "%s" for "%s" not imported.', $review-&amp;gt;author-&amp;gt;name, $course_name ) );
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I performed a search for the user's name to retrieve their ID from the database using the helper method &lt;code&gt;search_user_by_name&lt;/code&gt;, described below. Then, I used the &lt;code&gt;wp_insert_comment&lt;/code&gt; function to create the review, to which I passed the necessary values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ID of the course to which the review belongs&lt;/li&gt;
&lt;li&gt;The author's name&lt;/li&gt;
&lt;li&gt;The review date (slightly reformatted)&lt;/li&gt;
&lt;li&gt;The review content&lt;/li&gt;
&lt;li&gt;The custom values of the &lt;code&gt;Tutor LMS&lt;/code&gt; plugin to categorize the review as one of the &lt;code&gt;Tutor LMS&lt;/code&gt; course reviews&lt;/li&gt;
&lt;li&gt;The user's ID&lt;/li&gt;
&lt;li&gt;The review rating&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The method for searching for a user by name and surname is here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

protected function search_user_by_name( string $name ): int {
    $normalized_name = str_replace( '  ', ' ', trim( $name ) );
    $names           = explode( ' ', $normalized_name ); // line 3️⃣
    $last_name       = array_pop( $names );
    $first_name      = implode( ' ', $names ); // line 5️⃣

    $wp_user_query = new WP_User_Query(
        array(
            'role'       =&amp;gt; 'subscriber',
            'meta_query' =&amp;gt; array(
                'relation' =&amp;gt; 'AND',
                array(
                    'key'     =&amp;gt; 'first_name',
                    'value'   =&amp;gt; $first_name,
                    'compare' =&amp;gt; '=',
                ),
                array(
                    'key'     =&amp;gt; 'last_name',
                    'value'   =&amp;gt; $last_name,
                    'compare' =&amp;gt; '=',
                ),
            ),
        )
    );

    $authors = $wp_user_query-&amp;gt;get_results();

    return $authors[0]-&amp;gt;ID;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the initial phase, I performed a small normalization of the name, specifically removing spaces from the ends and replacing double spaces in the name with a single space. I noticed many discrepancies of this kind in the &lt;code&gt;JSON-exported&lt;/code&gt; file obtained from &lt;code&gt;Thinkific&lt;/code&gt; course pages.&lt;/p&gt;

&lt;p&gt;Then (in lines 3-5), I split the name by space to be able to obtain the first and last names, even when the full name consists of 3 names. In this case, I used &lt;code&gt;ChatGPT&lt;/code&gt;, resulting in the combination of the &lt;code&gt;explode&lt;/code&gt;, &lt;code&gt;array_pop&lt;/code&gt;, and &lt;code&gt;implode&lt;/code&gt; functions.&lt;/p&gt;

&lt;p&gt;Furthermore, I used the &lt;code&gt;WP_User_Query&lt;/code&gt; class for the complex query to find the user by their first and last names, and finally, I returned the ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;With the execution of the last &lt;code&gt;WP-CLI&lt;/code&gt; command, we now have the reviews added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ wp thinkific import reviews
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration is complete, and in the &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2"&gt;next article&lt;/a&gt;, I will make a few adjustments to the theme, &lt;code&gt;Tutor LMS&lt;/code&gt;, and &lt;code&gt;WooCommerce&lt;/code&gt; templates."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was first published on &lt;a href="https://vladilie.ro/blog/migrate-thinkific-wordpress-part-4-reviews-import" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>thinkific</category>
      <category>wpcli</category>
      <category>migration</category>
    </item>
    <item>
      <title>Migrate from Thinkific to WordPress. #3 Progress import</title>
      <dc:creator>Vlad Ilie</dc:creator>
      <pubDate>Wed, 12 Mar 2025 08:32:37 +0000</pubDate>
      <link>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em</link>
      <guid>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;part two&lt;/a&gt; of this 5-article series, I discussed how I implemented the &lt;code&gt;WP-CLI&lt;/code&gt; custom command for importing users.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Table of contents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j"&gt;Part 1: Planning and data preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;Part 2: Users import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Part 3: Progress import&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi"&gt;Part 4: Reviews import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2"&gt;Part 5: Other adaptations and conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Defining the progress import method
&lt;/h2&gt;

&lt;p&gt;I have a separate &lt;code&gt;CSV&lt;/code&gt; progress file for each existing course, from which I will read one by one using a recursive statement.&lt;/p&gt;

&lt;p&gt;To begin, I define the progress import method, declare global variables, and include and call the &lt;code&gt;WP_Filesystem&lt;/code&gt; function in &lt;code&gt;WordPress&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

public function import_progress() {
    global $wp_filesystem, $wpdb;

    include_once ABSPATH . 'wp-admin/includes/file.php';
    WP_Filesystem();
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

$files         = array(
    'course-1-progress.csv' =&amp;gt; 10, // Course name 1
    'course-2-progress.csv' =&amp;gt; 11, // Course name 2
    'course-3-progress.csv' =&amp;gt; 12, // Course name 3
    'course-4-progress.csv' =&amp;gt; 13, // Course name 4
);

$file_basepath = plugin_dir_path( __FILE__ ) . 'data/progress/';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And through the &lt;code&gt;foreach ( $files as $file =&amp;gt; $course_id )&lt;/code&gt; instruction, I go through the list of files and start reading from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

$file_content = $wp_filesystem-&amp;gt;get_contents( $file_basepath . $file );
$csv_data     = $this-&amp;gt;parse_csv_content( $file_content );
$i            = 0;
$lesson_names = array();
$time         = time();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reused the &lt;code&gt;parse_csv_content&lt;/code&gt; method to transform the lines from the &lt;code&gt;CSV&lt;/code&gt; into a vector, a method I defined in &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;part two&lt;/a&gt; during the user import. I also defined the variables &lt;code&gt;$i&lt;/code&gt;, &lt;code&gt;$lesson_names&lt;/code&gt;, and &lt;code&gt;$time&lt;/code&gt;, which I will use later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data browsing and processing
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

foreach ( $csv_data as $line ) {
    if ( ! $i ) { // line 2️⃣
        $i++;
        $chapter_names = array_slice( $line, 3 ); // line 4️⃣
        continue;
    } // line 6️⃣

    list($email, $completed_at, $percent_completed) = array_slice( $line, 0, 3 ); // line 8️⃣

    $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 ); // line 1️⃣5️⃣
    $count            = count( $chapter_progress ); // line 1️⃣6️⃣
    for ( $j = 0; $j &amp;lt; $count; $j++ ) {
        if ( '100' === $chapter_progress[ $j ] ) {
            $lesson_id = $wpdb-&amp;gt;get_var(
                $wpdb-&amp;gt;prepare(
                    "SELECT ID
                    FROM $wpdb-&amp;gt;posts
                    WHERE post_parent = (
                        SELECT ID
                        FROM $wpdb-&amp;gt;posts
                        WHERE post_title = %s AND post_parent = %d
                    )",
                    $chapter_names[ $j ],
                    $course_id
                )
            );

            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-&amp;gt;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 ) );
            }
        }
    }

    $i++;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In lines 2-6, I skipped the first line of the &lt;code&gt;CSV&lt;/code&gt; file again, which contains the column names. However, at the same time, I retained the lesson names (line 4) in the variable &lt;code&gt;$lesson_names&lt;/code&gt; because I need them in the query below. If you recall from the first part of &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j"&gt;data preparation&lt;/a&gt;, the first line of the progress &lt;code&gt;CSV&lt;/code&gt; file looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email,Completed At,% Completed,&amp;lt;Chapter-1-name&amp;gt;,&amp;lt;Chapter-2-name&amp;gt;,...,&amp;lt;Chapter-n-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Similarly, on line 15, the &lt;em&gt;lesson progress&lt;/em&gt; was stored.&lt;/p&gt;

&lt;p&gt;On line 8, I used the same function, this time to store &lt;strong&gt;the first&lt;/strong&gt; 3 values.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Finally, starting from line 16, the progress of the lessons is iterated through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing progress chapter - lesson
&lt;/h2&gt;

&lt;p&gt;I mentioned at some point that I made a compromise and processed only the chapters whose progress is &lt;code&gt;100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To clarify: the content of the courses can be organized, both in &lt;code&gt;Thinkific&lt;/code&gt; and &lt;code&gt;Tutor LMS&lt;/code&gt;, 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 &lt;code&gt;CSV&lt;/code&gt; file is generated accordingly by &lt;code&gt;Thinkific&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Now, in &lt;code&gt;Tutor LMS&lt;/code&gt;, 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 &lt;code&gt;_usermeta&lt;/code&gt; table as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| umeta_id | user_id | meta_key                      | meta_value |
| -------- | ------- | ----------------------------- | ---------- |
| 12345    | 678     | _tutor_completed_lesson_id_90 | 1705610847 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search and update lesson progress
&lt;/h2&gt;

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

&lt;p&gt;In the end, the user's &lt;code&gt;_tutor_completed_lesson_id_*&lt;/code&gt; field is updated, and they will see the progress they had on &lt;code&gt;Thinkific&lt;/code&gt; in the &lt;code&gt;WordPress&lt;/code&gt; dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The progress import command is about to be executed, and once again, the magic happens. 🧙🏼‍♂️&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ wp thinkific import progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more step, and the migration is complete.&lt;br&gt;
In &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi"&gt;the next article&lt;/a&gt;, I will use the &lt;code&gt;WordPress&lt;/code&gt; comments API for migrating reviews, so I invite you to check it out. 😎&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was first published on &lt;a href="https://vladilie.ro/blog/migrate-thinkific-wordpress-part-3-progress-import" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>thinkific</category>
      <category>wpcli</category>
      <category>migration</category>
    </item>
    <item>
      <title>Migrate from Thinkific to WordPress. #2 Users import</title>
      <dc:creator>Vlad Ilie</dc:creator>
      <pubDate>Wed, 12 Mar 2025 08:32:21 +0000</pubDate>
      <link>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647</link>
      <guid>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j"&gt;the first part&lt;/a&gt; of this 5-article series, I discussed the project for which I performed the migration, how I planned the migration, and how I exported data from &lt;code&gt;Thinkific&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here, in &lt;strong&gt;the second part&lt;/strong&gt;, I will cover the functionality of the &lt;code&gt;WP-CLI&lt;/code&gt; command that I implemented to import users into &lt;code&gt;WordPress&lt;/code&gt;. Below, I also provide you with the list of articles in the series:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Table of contents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j"&gt;Part 1: Planning and data preparation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Part 2: Users import&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em"&gt;Part 3: Progress import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi"&gt;Part 4: Reviews import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2"&gt;Part 5: Other adaptations and conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;As I mentioned, here begins the most interesting part of the entire process of migrating data from &lt;code&gt;Thinkific&lt;/code&gt; to &lt;code&gt;WordPress&lt;/code&gt;. I intend to create a plugin that encompasses everything needed for custom &lt;code&gt;WP-CLI&lt;/code&gt; commands, specifically the &lt;code&gt;PHP&lt;/code&gt; class I have implemented.&lt;/p&gt;

&lt;p&gt;I will go through the key points of the class one by one and explain the approaches along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declaring custom &lt;code&gt;WP-CLI&lt;/code&gt; commands
&lt;/h2&gt;

&lt;p&gt;To begin with, I defined a method in which I added 3 custom &lt;code&gt;WP-CLI&lt;/code&gt; commands that I implemented further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//class-wpcli.php
// ...

protected function define_commands(): void {
    if ( class_exists( 'WP_CLI' ) ) {
        \WP_CLI::add_command( 'thinkific import users', array( $this, 'import_users' ) );
        \WP_CLI::add_command( 'thinkific import progress', array( $this, 'import_progress' ) );
        \WP_CLI::add_command( 'thinkific import reviews', array( $this, 'import_reviews' ) );
    }
}
// ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These 3 commands will be available by running them in a terminal shell using &lt;code&gt;WP-CLI&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading from file
&lt;/h2&gt;

&lt;p&gt;In all three callbacks derived from the method above, namely &lt;code&gt;import_users&lt;/code&gt;, &lt;code&gt;import_progress&lt;/code&gt;, and &lt;code&gt;import_reviews&lt;/code&gt;, it was necessary to read from data files (2 &lt;code&gt;CSV&lt;/code&gt; files and a &lt;code&gt;JSON&lt;/code&gt; file). For this, I used the &lt;code&gt;API&lt;/code&gt; from the &lt;code&gt;WP_Filesystem&lt;/code&gt; class belonging to &lt;code&gt;WordPress&lt;/code&gt;, which helped me keep the code quite clean and within standard limits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

public function import_users(): void {
    global $wp_filesystem;

    include_once ABSPATH . 'wp-admin/includes/file.php';
    WP_Filesystem();

    $file_path    = plugin_dir_path( __FILE__ ) . 'data/users.csv';
    $file_content = $wp_filesystem-&amp;gt;get_contents( $file_path );
    $csv_data     = $this-&amp;gt;parse_csv_content( $file_content );
    //...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As can be seen, we have implemented another helper method to interpret the &lt;code&gt;CSV&lt;/code&gt; content into an easily traversable vector format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

protected function parse_csv_content( $csv_content ): array {
    $lines = explode( "\n", $csv_content );
    $data  = array();

    foreach ( $lines as $line ) {
        $data[] = str_getcsv( $line );
    }

    return $data;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was then able to simply loop through the lines in the &lt;code&gt;CSV&lt;/code&gt; file and proceed with the data processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

foreach ( $csv_data as $line ) {
    if ( ! $i ) {
        $i++;
        continue;
    }

    list(, , , , , , $courses) = $line;
    $course_arr                = explode( ', ', $courses );
    if ( count( $course_arr ) &amp;amp;&amp;amp; ! empty( $course_arr[0] ) ) {
        $user_id = $this-&amp;gt;create_user( $line );
        $this-&amp;gt;create_woo_order( $line, $user_id );
    }

    $i++;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously, I skipped the header row of the table (lines 2-5).&lt;br&gt;
Then, I processed only the users enrolled in at least one course (line 9). There were a few cases where users were merely registered on the platform but had not purchased any courses. Consequently, migrating this data was unnecessary as it was not relevant.&lt;/p&gt;

&lt;p&gt;Subsequently, for the remaining users, I invoked the two methods: user creation and order creation in &lt;code&gt;WooCommerce&lt;/code&gt; - as mentioned earlier, I needed to integrate with the normal course purchase process through &lt;code&gt;Tutor LMS - WooCommerce&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Create user
&lt;/h2&gt;

&lt;p&gt;In the user creation method, I didn't do anything special.&lt;br&gt;
I passed the information from the &lt;code&gt;CSV&lt;/code&gt; line to the method, and if the user searched by email address did not already exist, I created them. In both cases, the user ID is returned, and only in the case of a user creation error, &lt;code&gt;null&lt;/code&gt; is returned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

protected function create_user( $user_data ) {
    list($first_name, $last_name, , $created_at, $email) = $user_data;
    $user = get_user_by( 'email', $email );

    // User already exists, return current user ID.
    if ( $user ) {
        $user_id = $user-&amp;gt;get( 'ID' );

        return $user-&amp;gt;get( 'id' );
    }

    $user_id = wp_insert_user(
        array(
            'user_login'      =&amp;gt; $email,
            'user_email'      =&amp;gt; $email,
            'first_name'      =&amp;gt; $first_name,
            'last_name'       =&amp;gt; $last_name,
            'use_ssl'         =&amp;gt; true,
            'user_registered' =&amp;gt; str_replace( ' UTC', '', $created_at ),
        )
    );
    if ( is_wp_error( $user_id ) ) {
        \WP_CLI::error( sprintf( 'Error creating user %1$s %2$s &amp;lt;%3$s&amp;gt;: %4$s', $first_name, $last_name, $email, $user_id-&amp;gt;get_error_message() ) );

        return null;
    } else {
        // User created.
        return $user_id;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, when creating the user, I used the data from the file and interpreted the registration date because the provided format is not the same as the one accepted by the structure in the &lt;code&gt;WordPress&lt;/code&gt; user table - I removed the &lt;code&gt;UTC&lt;/code&gt; part from the date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create &lt;code&gt;WooCommerce&lt;/code&gt; order
&lt;/h2&gt;

&lt;p&gt;I hope I've been clear when mentioning "creating a &lt;code&gt;WooCommerce&lt;/code&gt; order". To elaborate, I'm referring to generating an order for products in an online store built with &lt;code&gt;WooCommerce&lt;/code&gt;, which is intended for payment and so forth. Within the &lt;code&gt;WooCommerce&lt;/code&gt; order creation method, I manually matched the IDs of the course products, as seen in lines 2-8.&lt;/p&gt;

&lt;p&gt;IDs 1, 2, 3, and 4 are assigned to products in the database, linked to the respective courses in the &lt;code&gt;Tutor LMS&lt;/code&gt; plugin. Of course, the actual IDs are different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// class-wpcli.php

protected function create_woo_order( array $data, int $user_id ): void {
    $raw_course_list = array(
        // course_title =&amp;gt; product_id.
        'Course title 1' =&amp;gt; 1,
        'Course title 2' =&amp;gt; 2,
        'Course title 3' =&amp;gt; 3,
        'Course title 4' =&amp;gt; 4,
    );

    list($first_name, $last_name, , $created_at, $email, , $courses) = $data;

    $order = wc_create_order(
        array(
            'customer_id' =&amp;gt; $user_id,
            'status'      =&amp;gt; 'wc-completed',
        )
    );

    $course_list = explode( ', ', $courses );
    // Add products to order.
    foreach ( $course_list as $course_name ) {
        $course_id = $raw_course_list[ $course_name ];
        $order-&amp;gt;add_product( wc_get_product( $course_id ), 1 ); // 1 is for quantity.
    }

    $order-&amp;gt;calculate_totals();

    $address = array(
        'first_name' =&amp;gt; $first_name,
        'last_name'  =&amp;gt; $last_name,
        'email'      =&amp;gt; $email,
        'country'    =&amp;gt; 'RO',
    );
    $order-&amp;gt;set_address( $address, 'billing' );
    $order-&amp;gt;set_payment_method( 'cod' );
    $order-&amp;gt;set_date_created( $created_at );
    $order-&amp;gt;set_date_completed( $created_at );
    $order-&amp;gt;set_date_paid( $created_at );

    $order-&amp;gt;save();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of using the variable &lt;code&gt;$raw_course_list&lt;/code&gt;, I could have performed a database search for courses, making the assignment of IDs dynamic. However, this would have introduced an additional database query and complicated things. If there were many more courses, I probably would have done it this way to save time.&lt;/p&gt;

&lt;p&gt;Moving forward, I began preparing the "order", adding courses to the cart, calculating totals, and setting the customer's address, payment method, and date. Finally, I marked the "order" as paid. From that moment, the user becomes a learner on the new e-learning platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;That's about it for the user import; now, all that's left is to run the command, and the magic happens. 🪄&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ wp thinkific import users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em"&gt;The next article&lt;/a&gt; will follow a similar pattern, but there are some interesting points to note there as well.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was first published on &lt;a href="https://vladilie.ro/blog/migrate-thinkific-wordpress-part-2-users-import" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>thinkific</category>
      <category>wpcli</category>
      <category>migration</category>
    </item>
    <item>
      <title>Migrate from Thinkific to WordPress. #1 Planning and data preparation</title>
      <dc:creator>Vlad Ilie</dc:creator>
      <pubDate>Wed, 12 Mar 2025 08:31:51 +0000</pubDate>
      <link>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j</link>
      <guid>https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-1-planning-and-data-preparation-j4j</guid>
      <description>&lt;p&gt;In this series of 5 articles, I propose to write about how I migrated from the e-learning platform &lt;a href="https://thinkific.com" rel="noopener noreferrer"&gt;Thinkific&lt;/a&gt; to &lt;code&gt;WordPress&lt;/code&gt;, how I exported the data from &lt;code&gt;Thinkific&lt;/code&gt;, what technologies I used, how WordPress Command Line Interface (&lt;code&gt;WP-CLI&lt;/code&gt;) helped me, and how I made other minor changes to templates or using &lt;code&gt;WordPress&lt;/code&gt; API hooks.&lt;br&gt;
Below, I leave you the list of articles in the series:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Table of contents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Part 1: Planning and data preparation&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;Part 2: Users import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-3-progress-import-14em"&gt;Part 3: Progress import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-4-reviews-import-1loi"&gt;Part 4: Reviews import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-5-other-adaptations-and-conclusions-20f2"&gt;Part 5: Other adaptations and conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;2023 ended with a small &lt;code&gt;WordPress&lt;/code&gt; challenge for me, to which I happily answered with "Yes!". It involved the content migration on &lt;code&gt;WordPress&lt;/code&gt; of the &lt;a href="https://masterclassonline.ro" rel="noopener noreferrer"&gt;masterclassonline.ro&lt;/a&gt; site belonging to the Cultural Association &lt;a href="https://cartilepefata.ro" rel="noopener noreferrer"&gt;Cărțile pe față&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The e-learning site has been hosted so far on the Thinkific platform, which is dedicated to online courses. The main reason for this migration was price. And for a few courses, such a platform is too much.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;WordPress&lt;/code&gt; fits here like a glove. It's flexible, there are some good e-learning plugins on the market, there's even more flexibility in developing new functionality, and the experience I have is even more enjoyable. It can also bring some performance improvements. Migrating data from one side to another is just a matter of time.&lt;/p&gt;

&lt;p&gt;I chose &lt;code&gt;WP-CLI&lt;/code&gt;, the command line interface for &lt;code&gt;WordPress&lt;/code&gt; because the process is singular. I need real-time monitoring during the migration, I have full control over the data, and the limitations are only those I impose on the local &lt;code&gt;PHP&lt;/code&gt; configurations. I run the migration locally and only then move the site to the production instance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Migration planning
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Assessment of requirements and resources
&lt;/h3&gt;

&lt;p&gt;In my case, the data I need from &lt;code&gt;Thinkific&lt;/code&gt; is as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the courses&lt;/li&gt;
&lt;li&gt;users and their enrollment in courses&lt;/li&gt;
&lt;li&gt;user progress for the courses they follow&lt;/li&gt;
&lt;li&gt;course reviews received from students&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conditions for enrolling users in &lt;code&gt;WordPress&lt;/code&gt; courses that, for example, require the creation of &lt;code&gt;WooCommerce&lt;/code&gt; orders must also met. In addition to these, on the future platform, I need the following functionalities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user registration and authentication&lt;/li&gt;
&lt;li&gt;purchasing courses&lt;/li&gt;
&lt;li&gt;card payment&lt;/li&gt;
&lt;li&gt;possibility of invoicing per legal entity&lt;/li&gt;
&lt;li&gt;users can track the progress of purchased courses&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Create a detailed plan for the migration
&lt;/h3&gt;

&lt;p&gt;For the new home of the Masterclass Online site, I chose the &lt;a href="https://wordpress.org/plugins/tutor" rel="noopener noreferrer"&gt;Tutor LMS&lt;/a&gt; plugin configured with &lt;a href="https://wordpress.org/plugins/woocommerce" rel="noopener noreferrer"&gt;WooCommerce&lt;/a&gt; for order processing, including payment, invoicing, and other integrations. However, I won't go into the details here.&lt;/p&gt;

&lt;p&gt;Next, I studied the e-learning plugin to determine the technical requirements needed for migrating the data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identified filters and actions in &lt;code&gt;Tutor LMS&lt;/code&gt; that I can use to bypass potential checks.&lt;/li&gt;
&lt;li&gt;Examined the relationships between entities in the database.&lt;/li&gt;
&lt;li&gt;Determined the necessary information to be filled in the &lt;code&gt;WordPress&lt;/code&gt; instance for a user to access their courses.&lt;/li&gt;
&lt;li&gt;Identified the fields to which other data &lt;code&gt;Thinkific&lt;/code&gt; must be matched.&lt;/li&gt;
&lt;li&gt;Established the optimal method for migration (I've already decided: &lt;code&gt;WP-CLI&lt;/code&gt; rocks!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Furthermore, I outlined the list of actions that I need to complete:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepared data exported from &lt;code&gt;Thinkific&lt;/code&gt; by normalizing it and removing irrelevant information.&lt;/li&gt;
&lt;li&gt;Ensured that no email notifications were enabled (e.g., for &lt;code&gt;WordPress&lt;/code&gt; account creation or other emails related to &lt;code&gt;WooCommerce&lt;/code&gt; orders).&lt;/li&gt;
&lt;li&gt;❗️ Based on my research on the functionality of the &lt;code&gt;Tutor LMS&lt;/code&gt; plugin, there is a need for a &lt;code&gt;PHP&lt;/code&gt; check to be denied. I delved (probably not enough) into the plugin, and from all the tests I conducted, without this disclaimer, the user wouldn't see their courses in the dashboard. Unfortunately, a filter for making this process easier does not exist in the plugin.&lt;/li&gt;
&lt;li&gt;Executed the import for users, progress, and reviews.&lt;/li&gt;
&lt;li&gt;Reactivated email notifications if they were disabled previously."&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Exporting and preparing data from &lt;code&gt;Thinkific&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I had some challenges with data, but it turned out to be relatively easy to export them.&lt;/p&gt;
&lt;h4&gt;
  
  
  Users
&lt;/h4&gt;

&lt;p&gt;Fortunately, there is a handy method to export the list of users:&lt;br&gt;
Thinkific Dashboard &amp;gt; Student Support &amp;gt; Users &amp;gt; Click on the "Export" button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgddjaygdxkd6gcliwgk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgddjaygdxkd6gcliwgk.png" alt="Export users from Thinkific Dashboard" width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the processing is complete, an email notification will be sent with a link to download the file in &lt;code&gt;CSV&lt;/code&gt; format.&lt;/p&gt;

&lt;p&gt;The header of the exported &lt;code&gt;CSV&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First Name,Last Name,Amount spent,Date created,Email,Enrollments,Enrollments - list,External source,Last sign in,Referred by,Roles,Sign in count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of all the data, I only need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First Name&lt;/li&gt;
&lt;li&gt;Last Name&lt;/li&gt;
&lt;li&gt;Date created&lt;/li&gt;
&lt;li&gt;Email&lt;/li&gt;
&lt;li&gt;Enrollments - list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, I can either normalize the file and remove everything I don't need, or go through it and select only the necessary data (initially, I opted for the second option).&lt;/p&gt;

&lt;h4&gt;
  
  
  Courses
&lt;/h4&gt;

&lt;p&gt;Since there are only 4 courses, I didn't find it necessary to export them. There doesn't seem to be an option for this in the &lt;code&gt;Thinkific&lt;/code&gt; dashboard (or I couldn't locate it).&lt;/p&gt;

&lt;p&gt;I manually created the courses in the e-learning plugin and as products in &lt;code&gt;WooCommerce&lt;/code&gt;. The process was quite fast; the main time consumption was in visually arranging and styling them with &lt;code&gt;Elementor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The way &lt;code&gt;Thinkific&lt;/code&gt; exports the user list is a bit peculiar because, in the &lt;code&gt;Enrollments - list&lt;/code&gt; field of the &lt;code&gt;CSV&lt;/code&gt; file, each user has a separate comma-separated list of course titles they are enrolled in.&lt;br&gt;
As a result, I had to go through each user's list during the import process and manually match them to set up the user-course relationships.&lt;/p&gt;

&lt;p&gt;📛 The only drawback in this regard is that the developed plugin is not entirely "plug-and-play"; it will require adaptation for that manual matching if run on a different dataset.&lt;/p&gt;
&lt;h4&gt;
  
  
  Progress
&lt;/h4&gt;

&lt;p&gt;This concerns the progress of each user in each course they are taking. Once again, the list of each user's progress can be easily exported from the &lt;code&gt;Thinkific&lt;/code&gt; dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fay3nqgiqh5vzft2jj8k2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fay3nqgiqh5vzft2jj8k2.png" alt="Export progress from Thinkific Dashboard" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📛 Another drawback of the &lt;code&gt;Thinkific&lt;/code&gt; platform is that progress can only be exported individually, per course, and not in bulk. So, if you have many courses on the platform and want such a migration, you might have a more serious task on your hands.&lt;/p&gt;

&lt;p&gt;The header of the exported progress &lt;code&gt;CSV&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email,Completed At,% Completed,&amp;lt;Chapter-1&amp;gt;,&amp;lt;Chapter-2&amp;gt;,...,&amp;lt;Chapter-n&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values for the columns with &lt;code&gt;&amp;lt;Chapter-names&amp;gt;&lt;/code&gt; range from 0 to 100.&lt;/p&gt;

&lt;p&gt;You will see further in part three of the article series that I made a compromise and only treated progress with the value of &lt;code&gt;100&lt;/code&gt;. For the remaining values other than 100, being fewer in number, I took the responsibility to update them manually directly in the database.&lt;/p&gt;

&lt;h4&gt;
  
  
  Reviews
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;Thinkific&lt;/code&gt; doesn't have a user-friendly method to export course reviews. Through a quick Google search, I came across &lt;a href="https://martechlms.com/blog/how-to-export-student-reviews-from-thinkific" rel="noopener noreferrer"&gt;this article&lt;/a&gt; that guided me on how to obtain reviews for each course. Again, if you have many courses on the platform and want to migrate them to &lt;code&gt;WordPress&lt;/code&gt;, it will require more manual work.&lt;/p&gt;

&lt;p&gt;Returning to the point, course reviews are available in the page source of each course. So, I fetched them from there (after ensuring they were all validated from the &lt;code&gt;Thinkific&lt;/code&gt; dashboard!) in &lt;code&gt;JSON&lt;/code&gt; format this time and worked with them from there (for the sake of dynamics 😁 - kidding, it seemed easier for me to process them this way).&lt;/p&gt;

&lt;p&gt;The structure of each &lt;code&gt;JSON&lt;/code&gt; object for reviews looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// reviews-course-1.json

{
  "@context": "http://schema.org",
  "@type": "Product",
  "name": "&amp;lt;course-name&amp;gt;",
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.6", "reviewCount": 5 },
  "review": [
    {
      "@type": "Review",
      "author": { "@type": "Person", "name": "&amp;lt;reviewer-name&amp;gt;" },
      "description": "&amp;lt;review-description&amp;gt;",
      "name": "&amp;lt;review-title&amp;gt;",
      "date": "November 26, 2021",
      "reviewRating": { "@type": "Rating", "bestRating": 5, "ratingValue": 5, "worstRating": 0 }
    },
    // ...
  ],
    "image": "&amp;lt;course-image-url&amp;gt;"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Thank you for your patience so far. 🍻 By now, I have the migration plan and all the necessary data from &lt;code&gt;Thinkific&lt;/code&gt; in their respective files.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/vladilie/migrate-from-thinkific-to-wordpress-2-users-import-647"&gt;next article&lt;/a&gt;, I will tackle the part I enjoy the most, implementing one of the &lt;code&gt;WP-CLI&lt;/code&gt; commands. ✌️&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was first published on &lt;a href="https://vladilie.ro/blog/migrate-thinkific-wordpress-part-1-planning-data-preparation" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>thinkific</category>
      <category>wpcli</category>
      <category>migration</category>
    </item>
  </channel>
</rss>
