diff --git a/.changelogs/feature_course-level-lesson-drip.yml b/.changelogs/feature_course-level-lesson-drip.yml new file mode 100644 index 0000000000..db57e00f45 --- /dev/null +++ b/.changelogs/feature_course-level-lesson-drip.yml @@ -0,0 +1,3 @@ +significance: minor +type: added +entry: Adds course-level lesson drip settings. diff --git a/assets/js/builder/Schemas/Lesson.js b/assets/js/builder/Schemas/Lesson.js index 55b612ea94..2be8c6f852 100644 --- a/assets/js/builder/Schemas/Lesson.js +++ b/assets/js/builder/Schemas/Lesson.js @@ -87,6 +87,26 @@ define( [], function() { return this.get_available_prereq_options(); }, }, + ], [ + { + label: LLMS.l10n.translate( 'Course Drip Method' ), + id: 'course-drip', + type: 'heading', + condition: function() { + return ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ); + }, + detail: LLMS.l10n.translate( 'Drip settings are currently set at the course level, under the Restrictions settings tab. Disable to allow lesson level drip settings.' ) + ' ' + LLMS.l10n.translate( 'Edit Course' ) + '', + }, + ], [ + { + label: LLMS.l10n.translate( 'Course Drip Method' ), + id: 'course-drip', + type: 'heading', + condition: function() { + return ( ! this.get_course() || 'yes' !== this.get_course().get( 'lesson_drip' ) || ! this.get_course().get( 'drip_method' ) ); + }, + detail: LLMS.l10n.translate( 'Drip settings can be set at the course level to release course content at a specified interval, in the Restrictions settings tab.' ) + ' ' + LLMS.l10n.translate( 'Edit Course' ) + '', + }, ], [ { attribute: 'drip_method', @@ -94,6 +114,9 @@ define( [], function() { label: LLMS.l10n.translate( 'Drip Method' ), switch_attribute: 'drip_method', type: 'select', + condition: function() { + return ( ! this.get_course() || 'yes' !== this.get_course().get( 'lesson_drip' ) || ! this.get_course().get( 'drip_method' ) ); + }, options: function() { var options = [ @@ -132,6 +155,10 @@ define( [], function() { { attribute: 'days_before_available', condition: function() { + if ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) { + return false; + } + return ( -1 !== [ 'enrollment', 'start', 'prerequisite' ].indexOf( this.get( 'drip_method' ) ) ); }, id: 'days-before-available', @@ -143,6 +170,10 @@ define( [], function() { attribute: 'date_available', date_format: 'Y-m-d', condition: function() { + if ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) { + return false; + } + return ( 'date' === this.get( 'drip_method' ) ); }, id: 'date-available', @@ -153,6 +184,10 @@ define( [], function() { { attribute: 'time_available', condition: function() { + if ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) { + return false; + } + return ( 'date' === this.get( 'drip_method' ) ); }, datepicker: 'false', diff --git a/assets/js/llms-builder.js b/assets/js/llms-builder.js index eea455aade..3858dacdfa 100644 --- a/assets/js/llms-builder.js +++ b/assets/js/llms-builder.js @@ -3707,6 +3707,28 @@ define( 'Schemas/Lesson',[], function() { return this.get_available_prereq_options(); }, }, + ], [ + { + label: LLMS.l10n.translate( 'Course Drip Method' ), + id: 'course-drip', + type: 'heading', + condition: function() { + return ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ); + }, + // TODO: see if we can get rid of this hack. this.get_course() is not available at this point to use window.llms_builder.admin_url. + detail: LLMS.l10n.translate( 'Drip settings are currently set at the course level, under the Restrictions settings tab. Disable to allow lesson level drip settings.' ) + ' ' + LLMS.l10n.translate( 'Edit Course' ) + '', + }, + ], [ + { + label: LLMS.l10n.translate( 'Course Drip Method' ), + id: 'course-drip', + type: 'heading', + condition: function() { + return ( ! this.get_course() || 'yes' !== this.get_course().get( 'lesson_drip' ) || ! this.get_course().get( 'drip_method' ) ); + }, + // TODO: see if we can get rid of this hack. this.get_course() is not available at this point to use window.llms_builder.admin_url. + detail: LLMS.l10n.translate( 'Drip settings can be set at the course level to release course content at a specified interval, in the Restrictions settings tab.' ) + ' ' + LLMS.l10n.translate( 'Edit Course' ) + '', + }, ], [ { attribute: 'drip_method', @@ -3714,6 +3736,9 @@ define( 'Schemas/Lesson',[], function() { label: LLMS.l10n.translate( 'Drip Method' ), switch_attribute: 'drip_method', type: 'select', + condition: function() { + return ( ! this.get_course() || 'yes' !== this.get_course().get( 'lesson_drip' ) || ! this.get_course().get( 'drip_method' ) ); + }, options: function() { var options = [ @@ -3752,6 +3777,10 @@ define( 'Schemas/Lesson',[], function() { { attribute: 'days_before_available', condition: function() { + if ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) { + return false; + } + return ( -1 !== [ 'enrollment', 'start', 'prerequisite' ].indexOf( this.get( 'drip_method' ) ) ); }, id: 'days-before-available', @@ -3763,6 +3792,10 @@ define( 'Schemas/Lesson',[], function() { attribute: 'date_available', date_format: 'Y-m-d', condition: function() { + if ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) { + return false; + } + return ( 'date' === this.get( 'drip_method' ) ); }, id: 'date-available', @@ -3773,6 +3806,10 @@ define( 'Schemas/Lesson',[], function() { { attribute: 'time_available', condition: function() { + if ( this.get_course() && 'yes' === this.get_course().get( 'lesson_drip' ) && this.get_course().get( 'drip_method' ) ) { + return false; + } + return ( 'date' === this.get( 'drip_method' ) ); }, datepicker: 'false', diff --git a/includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php b/includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php index 45d9251f9a..2470cb3f9f 100644 --- a/includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php +++ b/includes/admin/post-types/meta-boxes/class.llms.meta.box.course.options.php @@ -328,7 +328,51 @@ public function get_fields() { 'label' => __( 'Choose Prerequisite Course Track', 'lifterlms' ), 'value' => $course_tracks, ), - + array( + 'type' => 'checkbox', + 'label' => __( 'Enable Lesson Drip', 'lifterlms' ), + 'desc' => __( 'Set global drip restrictions so lesson content becomes available at an interval you define for the course.', 'lifterlms' ), + 'id' => $this->prefix . 'lesson_drip', + 'is_controller' => true, + 'value' => 'yes', + 'class' => '', + 'desc_class' => 'd-3of4 t-3of4 m-1of2', + ), + array( + 'class' => 'llms-select2', + 'controller' => '#' . $this->prefix . 'lesson_drip', + 'controller_value' => 'yes', + 'is_controller' => true, + 'type' => 'select', + 'id' => $this->prefix . 'drip_method', + 'label' => __( 'Drip Method', 'lifterlms' ), + 'value' => array( + array( + 'key' => 'start', + 'title' => __( 'After course start or enrollment', 'lifterlms' ), + ), + ), + ), + array( + 'controller' => '#' . $this->prefix . 'lesson_drip', + 'controller_value' => 'yes', + 'class' => 'input-full', + 'id' => $this->prefix . 'ignore_lessons', + 'label' => __( 'Number of lessons to make immediately available on course start', 'lifterlms' ), + 'type' => 'number', + 'step' => 1, + 'min' => 1, + ), + array( + 'controller' => '#' . $this->prefix . 'lesson_drip', + 'controller_value' => 'yes', + 'class' => 'input-full', + 'id' => $this->prefix . 'days_before_available', + 'label' => __( 'Delay (in days) ', 'lifterlms' ), + 'type' => 'number', + 'step' => 1, + 'min' => 1, + ), array( 'is_controller' => true, 'type' => 'checkbox', diff --git a/includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php b/includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php index 5863f1353f..c4fe3b02b8 100644 --- a/includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php +++ b/includes/admin/post-types/meta-boxes/class.llms.meta.box.lesson.php @@ -73,7 +73,7 @@ public function get_fields() { unset( $methods['start'] ); } - return array( + $fields = array( array( 'title' => __( 'General', 'lifterlms' ), 'fields' => array( @@ -134,44 +134,13 @@ public function get_fields() { ), ), ), - array( + 'drip' => array( 'title' => __( 'Drip Settings', 'lifterlms' ), 'fields' => array( array( - 'class' => 'llms-select2', - 'desc_class' => 'd-all', - 'id' => $this->prefix . 'drip_method', - 'is_controller' => true, - 'label' => __( 'Method', 'lifterlms' ), - 'type' => 'select', - 'value' => $methods, - ), - array( - 'controller' => '#' . $this->prefix . 'drip_method', - 'controller_value' => 'lesson,enrollment,start,prerequisite', - 'class' => 'input-full', - 'id' => $this->prefix . 'days_before_available', - 'label' => __( 'Delay (in days) ', 'lifterlms' ), - 'type' => 'number', - 'step' => 1, - 'min' => 0, - ), - array( - 'controller' => '#' . $this->prefix . 'drip_method', - 'controller_value' => 'date', - 'class' => 'llms-datepicker', - 'id' => $this->prefix . 'date_available', - 'label' => __( 'Date Available', 'lifterlms' ), - 'type' => 'date', - ), - array( - 'controller' => '#' . $this->prefix . 'drip_method', - 'controller_value' => 'date', - 'class' => '', - 'desc' => __( 'Optionally enter a time when the lesson should become available. If no time supplied, lesson will be available at 12:00 AM. Format must be HH:MM AM', 'lifterlms' ), - 'id' => $this->prefix . 'time_available', - 'label' => __( 'Time Available', 'lifterlms' ), - 'type' => 'text', + 'type' => 'custom-html', + 'id' => $this->prefix . 'drip_course_settings_info', + 'value' => $this->get_drip_course_settings_info_html( $course ), ), ), ), @@ -191,8 +160,67 @@ public function get_fields() { ), ), ); + + if ( 'yes' !== $course->get( 'lesson_drip' ) || ! $course->get( 'drip_method' ) ) { + $fields['drip']['fields'][] = array( + 'class' => 'llms-select2', + 'desc_class' => 'd-all', + 'id' => $this->prefix . 'drip_method', + 'is_controller' => true, + 'label' => __( 'Method', 'lifterlms' ), + 'type' => 'select', + 'value' => $methods, + ); + $fields['drip']['fields'][] = array( + 'controller' => '#' . $this->prefix . 'drip_method', + 'controller_value' => 'lesson,enrollment,start,prerequisite', + 'class' => 'input-full', + 'id' => $this->prefix . 'days_before_available', + 'label' => __( 'Delay (in days) ', 'lifterlms' ), + 'type' => 'number', + 'step' => 1, + 'min' => 0, + ); + $fields['drip']['fields'][] = array( + 'controller' => '#' . $this->prefix . 'drip_method', + 'controller_value' => 'date', + 'class' => 'llms-datepicker', + 'id' => $this->prefix . 'date_available', + 'label' => __( 'Date Available', 'lifterlms' ), + 'type' => 'date', + ); + $fields['drip']['fields'][] = array( + 'controller' => '#' . $this->prefix . 'drip_method', + 'controller_value' => 'date', + 'class' => '', + 'desc' => __( 'Optionally enter a time when the lesson should become available. If no time supplied, lesson will be available at 12:00 AM. Format must be HH:MM AM', 'lifterlms' ), + 'id' => $this->prefix . 'time_available', + 'label' => __( 'Time Available', 'lifterlms' ), + 'type' => 'text', + ); + } + + return $fields; } + /** + * Helpful messaging depending on whether the course for this lesson has drip settings enabled or not. + * + * @since [version] + * + * @param LLMS_Course $course Course object. + * @return string + */ + public function get_drip_course_settings_info_html( $course ) { + $output = 'yes' === $course->get( 'lesson_drip' ) && $course->get( 'drip_method' ) ? + __( 'Drip settings are currently set at the course level, under the Restrictions settings tab. If you would like to set individual drip settings for each lesson, you must disable the course level drip settings first.', 'lifterlms' ) + : + __( 'Drip settings can be set at the course level to release course content at a specified interval, in the Restrictions settings tab.', 'lifterlms' ); + + $output .= ' ' . __( 'Edit Course', 'lifterlms' ) . ''; + + return $output; + } } new LLMS_Meta_Box_Lesson(); diff --git a/includes/admin/views/builder/settings-fields.php b/includes/admin/views/builder/settings-fields.php index 71ccbff421..11f36382ce 100644 --- a/includes/admin/views/builder/settings-fields.php +++ b/includes/admin/views/builder/settings-fields.php @@ -138,6 +138,9 @@ class="llms-input standard" {{{ field.label_after }}} <# } #> + <# if ( field.detail ) { #> +
{{{ field.detail }}}
+ <# } #> <# } ); #> diff --git a/includes/models/model.llms.course.php b/includes/models/model.llms.course.php index c7be21d38a..6ca974e4d0 100644 --- a/includes/models/model.llms.course.php +++ b/includes/models/model.llms.course.php @@ -87,6 +87,10 @@ class LLMS_Course extends LLMS_Post_Model implements LLMS_Interface_Post_Instruc 'tile_featured_video' => 'yesno', 'time_period' => 'yesno', 'start_date' => 'text', + 'lesson_drip' => 'yesno', + 'drip_method' => 'text', + 'ignore_lessons' => 'absint', + 'days_before_available' => 'absint', // Private. 'temp_calc_data' => 'array', diff --git a/includes/models/model.llms.lesson.php b/includes/models/model.llms.lesson.php index 550be566dc..7de5e36795 100644 --- a/includes/models/model.llms.lesson.php +++ b/includes/models/model.llms.lesson.php @@ -117,7 +117,7 @@ public function __construct( $model, $args = array() ) { } /** - * Get the date a course became or will become available according to element drip settings + * Get the date a lesson became or will become available according to element drip settings * * If there are no drip settings, the published date of the element will be returned. * @@ -142,6 +142,35 @@ public function get_available_date( $format = '' ) { // Default availability is the element's post date. $available = $this->get_date( 'date', 'U' ); + // get the course setting first, if any. + $course = $this->get_course(); + if ( $course && 'yes' === $course->get( 'lesson_drip' ) ) { + $course_drip_method = $course->get( 'drip_method' ); + + switch ( $course_drip_method ) { + case 'start': + $ignore_lessons = intval( $course->get( 'ignore_lessons' ) ); + $course_lessons = $course->get_lessons( 'ids' ); + $lesson_number = array_search( $this->get( 'id' ), $course_lessons ) + 1; + + $course_days = $course->get( 'days_before_available' ) * DAY_IN_SECONDS; + $course_start_date = $course->get_date( 'start_date', 'U' ); + $course_enrollment_date = llms_get_student() ? llms_get_student()->get_enrollment_date( $course->get( 'id' ), 'enrolled', 'U' ) : false; + + // If it's one of the first X lessons in a course, return availability based on published date. + if ( $lesson_number <= $ignore_lessons ) { + return date_i18n( $format, $available ); + } + + if ( $course_start_date || $course_enrollment_date ) { + $available = ( ( $lesson_number - $ignore_lessons ) * $course_days ) + ( $course_start_date ? $course_start_date : $course_enrollment_date ); + + return date_i18n( $format, $available ); + } + break; + } + } + switch ( $drip_method ) { // Available on a specific date / time. @@ -453,16 +482,17 @@ public function has_quiz() { public function is_available() { $drip_method = $this->get( 'drip_method' ); + $course_drip_method = $this->get_course() ? 'yes' === $this->get_course()->get( 'lesson_drip' ) && $this->get_course()->get( 'drip_method' ) : ''; - // Drip is no enabled, so the element is available. - if ( ! $drip_method ) { + // Drip is not enabled, so the element is available. + if ( ! $drip_method && ! $course_drip_method ) { return true; } $available = $this->get_available_date( 'U' ); $now = llms_current_time( 'timestamp' ); - return ( $now > $available ); + return ( $now >= $available ); } diff --git a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php b/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php index e321f15790..78c53e0d09 100644 --- a/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php +++ b/tests/phpunit/unit-tests/admin/post-types/meta-boxes/class-llms-test-meta-box-lesson.php @@ -47,18 +47,18 @@ public function test_get_fields() { // as the course has no start date set. foreach ( $this->metabox->get_fields() as $index => $f ) { if ( 'Drip Settings' === $f['title'] ) { - $this->assertFalse( array_key_exists( 'start', $f['fields'][0]['value'] ) ); + $this->assertFalse( array_key_exists( 'start', $f['fields'][1]['value'] ) ); break; } } - // set a course start date./* + // set a course start date. $course->set( 'start_date', current_time( 'm/d/Y' ) ); // check the lessons Drip Settings methods list contains 'start', // as the course now has a start date set. foreach ( $this->metabox->get_fields() as $index => $f ) { if ( 'Drip Settings' === $f['title'] ) { - $this->assertTrue( array_key_exists( 'start', $f['fields'][0]['value'] ) ); + $this->assertTrue( array_key_exists( 'start', $f['fields'][1]['value'] ) ); break; } } diff --git a/tests/phpunit/unit-tests/class-llms-test-functions-access.php b/tests/phpunit/unit-tests/class-llms-test-functions-access.php index 1c105a2cf2..bdcf41bb42 100644 --- a/tests/phpunit/unit-tests/class-llms-test-functions-access.php +++ b/tests/phpunit/unit-tests/class-llms-test-functions-access.php @@ -39,6 +39,7 @@ public function test_llms_is_post_restricted_by_drip_settings() { $course_id = $this->generate_mock_courses( 1, 1, 2, 0 )[0]; $course = llms_get_post( $course_id ); $lesson = $course->get_lessons()[0]; + $second_lesson = $course->get_lessons()[1]; $lesson_id = $lesson->get( 'id' ); $student = $this->get_mock_student(); wp_set_current_user( $student->get_id() ); @@ -78,6 +79,31 @@ public function test_llms_is_post_restricted_by_drip_settings() { llms_tests_mock_current_time( '+4 days' ); $this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) ); + // Test course-level drip settings. + + // Ensure first lesson is not available due to lesson-level drip settings. + llms_tests_reset_current_time(); + $this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) ); + + // Set individual drip settings on the second lesson and ensure it is available. + $second_lesson->set( 'drip_method', 'date' ); + $second_lesson->set( 'date_available', date( 'm/d/Y', current_time( 'timestamp' ) - DAY_IN_SECONDS ) ); + $this->assertFalse( llms_is_post_restricted_by_drip_settings( $second_lesson->get( 'id' ) ) ); + + // Now set course-level drip settings and ensure the first lesson is available. + $course->set( 'drip_method', 'start' ); + $course->set( 'lesson_drip', 'yes' ); + $course->set( 'days_before_available', 10 ); + $course->set( 'ignore_lessons', 1 ); + $this->assertFalse( llms_is_post_restricted_by_drip_settings( $lesson_id ) ); + // second not available until 10 days from now. + $this->assertEquals( $second_lesson->get( 'id' ), llms_is_post_restricted_by_drip_settings( $second_lesson->get( 'id' ) ) ); + + // If lesson drip turned off the rest of the course drip settings should be ignored. + $course->set( 'lesson_drip', '' ); + $this->assertEquals( $lesson_id, llms_is_post_restricted_by_drip_settings( $lesson_id ) ); + $this->assertFalse( llms_is_post_restricted_by_drip_settings( $second_lesson->get( 'id' ) ) ); + } /** diff --git a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php index c2332cc847..4286ae9f58 100644 --- a/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php +++ b/tests/phpunit/unit-tests/models/class-llms-test-model-llms-lesson.php @@ -178,6 +178,73 @@ public function test_get_available_date() { } + /** + * Test get available date when the course has "After course starts" delay in days set. + * + * @since [version] + * + * @return void + */ + public function test_get_available_date_with_course_drip_settings() { + + $format = 'Y-m-d'; + + $course_id = $this->generate_mock_courses( 1, 3, 2, 0 )[0]; + + $course = llms_get_post( $course_id ); + $course->set( 'lesson_drip', 'yes' ); + $course->set( 'drip_method', 'start' ); + $course->set( 'days_before_available', '7' ); + $course->set( 'ignore_lessons', '1' ); + + $student = $this->get_mock_student(); + wp_set_current_user( $student->get_id() ); + $student->enroll( $course_id ); + + $now = new DateTimeImmutable(); + + $this->assertEquals( $now->format( $format ), $course->get_lessons()[0]->get_available_date( $format ) ); + $this->assertEquals( $now->add( DateInterval::createFromDateString( '7 days') )->format( $format ), $course->get_lessons()[1]->get_available_date( $format ) ); + $this->assertEquals( $now->add( DateInterval::createFromDateString( '14 days') )->format( $format ), $course->get_lessons()[2]->get_available_date( $format ) ); + $this->assertEquals( $now->add( DateInterval::createFromDateString( '21 days') )->format( $format ), $course->get_lessons()[3]->get_available_date( $format ) ); + + } + + /** + * Test get available date when the course has "After course starts" delay in days set and + * the course has a fixed start date. + * + * @since [version] + * + * @return void + */ + public function test_get_available_date_with_course_drip_settings_with_course_start_date() { + + $format = 'Y-m-d'; + + $course_id = $this->generate_mock_courses( 1, 3, 2, 0 )[0]; + + $course = llms_get_post( $course_id ); + $course->set( 'lesson_drip', 'yes' ); + $course->set( 'drip_method', 'start' ); + $course->set( 'days_before_available', '7' ); + $course->set( 'ignore_lessons', '1' ); + $course_start = new DateTimeImmutable( '-1 week' ); + $course->set( 'start_date', $course_start->format( 'm/d/Y' ) ); + + $student = $this->get_mock_student(); + wp_set_current_user( $student->get_id() ); + $student->enroll( $course_id ); + + $now = new DateTimeImmutable(); + + $this->assertEquals( $now->format( $format ), $course->get_lessons()[0]->get_available_date( $format ) ); + $this->assertEquals( $course_start->add( DateInterval::createFromDateString( '7 days') )->format( $format ), $course->get_lessons()[1]->get_available_date( $format ) ); + $this->assertEquals( $course_start->add( DateInterval::createFromDateString( '14 days') )->format( $format ), $course->get_lessons()[2]->get_available_date( $format ) ); + $this->assertEquals( $course_start->add( DateInterval::createFromDateString( '21 days') )->format( $format ), $course->get_lessons()[3]->get_available_date( $format ) ); + + } + /** * Test get course *