<?php
/**
* File containing the Comments_Based_Quiz_Progress_Repository class.
*
* @package sensei
*/
namespace Sensei\Internal\Student_Progress\Quiz_Progress\Repositories;
use DateTime;
use Sensei\Internal\Student_Progress\Quiz_Progress\Models\Comments_Based_Quiz_Progress;
use Sensei\Internal\Student_Progress\Quiz_Progress\Models\Quiz_Progress_Interface;
use Sensei_Utils;
use WP_Comment;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Comments_Based_Quiz_Progress_Repository.
*
* @internal
*
* @since 4.7.2
*/
class Comments_Based_Quiz_Progress_Repository implements Quiz_Progress_Repository_Interface {
/**
* Create a new quiz progress.
*
* @internal
*
* @param int $quiz_id Quiz identifier.
* @param int $user_id User identifier.
* @return Quiz_Progress_Interface
* @throws \RuntimeException When the quiz progress doesn't exist. In this implementation we re-use lesson progress.
*/
public function create( int $quiz_id, int $user_id ): Quiz_Progress_Interface {
/**
* Filter quiz id for quiz progress creation.
*
* @hook sensei_quiz_progress_create_quiz_id
*
* @since 4.23.1
*
* @param {int} $quiz_id Quiz ID.
* @return {int} Filtered quiz ID.
*/
$quiz_id = (int) apply_filters( 'sensei_quiz_progress_create_quiz_id', $quiz_id );
$progress = $this->get( $quiz_id, $user_id );
if ( ! $progress ) {
/**
* In comments-based implementation we don't have a separate quiz progress.
* It depends on the lesson progress. If it doesn't exist yet, throw an exception.
*/
throw new \RuntimeException( 'Cannot create quiz progress' );
}
return $progress;
}
/**
* Find a quiz progress by quiz and user identifiers.
*
* @internal
*
* @param int $quiz_id Quiz identifier.
* @param int $user_id User identifier.
* @return Quiz_Progress_Interface
*/
public function get( int $quiz_id, int $user_id ): ?Quiz_Progress_Interface {
if ( ! $user_id ) {
return null;
}
/**
* Filter quiz id for quiz progress retrieval.
*
* @hook sensei_quiz_progress_get_quiz_id
*
* @since 4.23.1
*
* @param {int} $quiz_id Quiz ID.
* @return {int} Filtered quiz ID.
*/
$quiz_id = (int) apply_filters( 'sensei_quiz_progress_get_quiz_id', $quiz_id );
$lesson_id = (int) Sensei()->quiz->get_lesson_id( $quiz_id );
if ( ! $lesson_id ) {
return null;
}
$activity_args = [
'post_id' => $lesson_id,
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
];
$comment = Sensei_Utils::sensei_check_for_activity( $activity_args, true );
if ( ! $comment instanceof WP_Comment ) {
return null;
}
return $this->create_progress_from_comment( $comment, $quiz_id );
}
/**
* Check if a quiz progress exists.
*
* @internal
*
* @param int $quiz_id Quiz identifier.
* @param int $user_id User identifier.
* @return bool
*/
public function has( int $quiz_id, int $user_id ): bool {
if ( ! $user_id ) {
return false;
}
/**
* Filter quiz id for quiz progress existence check.
*
* @hook sensei_quiz_progress_has_quiz_id
*
* @since 4.23.1
*
* @param {int} $quiz_id Quiz ID.
* @return {int} Filtered quiz ID.
*/
$quiz_id = (int) apply_filters( 'sensei_quiz_progress_has_quiz_id', $quiz_id );
$lesson_id = (int) Sensei()->quiz->get_lesson_id( $quiz_id );
if ( ! $lesson_id ) {
return false;
}
$activity_args = [
'post_id' => $lesson_id,
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
];
$count = (int) Sensei_Utils::sensei_check_for_activity( $activity_args );
return $count > 0;
}
/**
* Save the quiz progress.
*
* @internal
*
* @param Quiz_Progress_Interface $quiz_progress Quiz progress.
*/
public function save( Quiz_Progress_Interface $quiz_progress ): void {
$this->assert_comments_based_quiz_progress( $quiz_progress );
$lesson_id = (int) Sensei()->quiz->get_lesson_id( $quiz_progress->get_quiz_id() );
$metadata = [];
if ( $quiz_progress->get_started_at() ) {
$metadata['start'] = $quiz_progress->get_started_at()->format( 'Y-m-d H:i:s' );
}
// We need to use internal value for status, not the one returned by the getter.
// Commets-based `get_status` method excludes lesson-progress `complete` status, that we still need while saving.
$reflection_class = new \ReflectionClass( Comments_Based_Quiz_Progress::class );
$status_property = $reflection_class->getProperty( 'status' );
$status_property->setAccessible( true );
$status = (string) $status_property->getValue( $quiz_progress );
Sensei_Utils::update_lesson_status( $quiz_progress->get_user_id(), $lesson_id, $status, $metadata );
}
/**
* Delete the quiz progress.
*
* @internal
*
* @param Quiz_Progress_Interface $quiz_progress Quiz progress.
*/
public function delete( Quiz_Progress_Interface $quiz_progress ): void {
Sensei_Utils::sensei_delete_quiz_answers( $quiz_progress->get_quiz_id(), $quiz_progress->get_user_id() );
}
/**
* Delete all quiz progress for a given quiz.
*
* @internal
*
* @param int $quiz_id Quiz identifier.
*/
public function delete_for_quiz( int $quiz_id ): void {
/**
* Filter quiz id for quiz progress deletion.
*
* @hook sensei_quiz_progress_delete_for_quiz_quiz_id
*
* @since 4.23.1
*
* @param {int} $quiz_id Quiz ID.
* @return {int} Filtered quiz ID.
*/
$quiz_id = (int) apply_filters( 'sensei_quiz_progress_delete_for_quiz_quiz_id', $quiz_id );
$lesson_id = (int) Sensei()->quiz->get_lesson_id( $quiz_id );
if ( ! $lesson_id ) {
return;
}
$activity_args = [
'post_id' => $lesson_id,
'type' => 'sensei_lesson_status',
];
$comments = Sensei_Utils::sensei_check_for_activity( $activity_args, true );
foreach ( $comments as $comment ) {
$this->delete_grade_and_answers( (int) $comment->comment_ID );
Sensei_Utils::sensei_delete_quiz_answers( $quiz_id, (int) $comment->user_id );
}
}
/**
* Delete all quiz grades and answers for a user.
*
* @internal
*
* @param int $user_id User identifier.
*/
public function delete_for_user( int $user_id ): void {
if ( ! $user_id ) {
return;
}
$activity_args = [
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
];
$comments = Sensei_Utils::sensei_check_for_activity( $activity_args, true );
foreach ( $comments as $comment ) {
$this->delete_grade_and_answers( $comment->comment_ID );
}
}
/**
* Delete the quiz grade and answers.
*
* @param int $comment_id Comment identifier.
*/
private function delete_grade_and_answers( $comment_id ): void {
delete_comment_meta( $comment_id, 'quiz_answers' );
delete_comment_meta( $comment_id, 'grade' );
}
/**
* Assert that the quiz progress is a Comments_Based_Quiz_Progress.
*
* @param Quiz_Progress_Interface $quiz_progress Quiz progress.
* @throws \InvalidArgumentException When the quiz progress is not a Comments_Based_Quiz_Progress.
*/
private function assert_comments_based_quiz_progress( Quiz_Progress_Interface $quiz_progress ): void {
if ( ! $quiz_progress instanceof Comments_Based_Quiz_Progress ) {
$actual_type = get_class( $quiz_progress );
throw new \InvalidArgumentException( esc_html( "Expected Comments_Based_Quiz_Progress, got {$actual_type}." ) );
}
}
/**
* Find quiz progress.
*
* @internal
*
* @param array $args The arguments.
* @return Quiz_Progress_Interface[] The quiz progress.
* @throws \InvalidArgumentException When ordering is not supported.
*/
public function find( array $args ): array {
$comments_args = array(
'type' => 'sensei_lesson_status',
'order' => 'ASC',
'orderby' => 'comment_ID',
);
$quiz_id = $args['quiz_id'] ?? null;
if ( ! empty( $quiz_id ) ) {
$quiz_id = (array) $quiz_id;
$quiz_id = array_map( 'intval', $quiz_id );
$quiz_id = array_map(
function ( $id ) {
/**
* Filter quiz id for quiz progress retrieval.
*
* @hook sensei_quiz_progress_find_quiz_id
*
* @since 4.23.1
*
* @param {int} $quiz_id Quiz ID.
* @return {int} Filtered quiz ID.
*/
return (int) apply_filters( 'sensei_quiz_progress_find_quiz_id', $id );
},
$quiz_id
);
$lesson_ids = Sensei()->quiz->get_lesson_ids( $quiz_id );
if ( ! empty( $lesson_ids ) ) {
$comments_args['post__in'] = $lesson_ids;
} else {
return array();
}
}
if ( isset( $args['user_id'] ) ) {
$comments_args['user_id'] = $args['user_id'];
}
if ( isset( $args['status'] ) ) {
$comments_args['status'] = $args['status'];
}
if ( isset( $args['order'] ) ) {
$comments_args['order'] = $args['order'];
}
if ( isset( $args['orderby'] ) ) {
switch ( $args['orderby'] ) {
case 'started_at':
throw new \InvalidArgumentException( 'Ordering by started_at is not supported in comments-based version.' );
case 'completed_at':
case 'created_at':
case 'updated_at':
$comments_args['orderby'] = 'comment_date';
break;
case 'quiz_id':
// We need to order by lesson ID, not quiz ID, as the lesson ID is not reachable from the comment.
$comments_args['orderby'] = 'comment_post_ID';
break;
case 'id':
$comments_args['orderby'] = 'comment_ID';
break;
case 'status':
$comments_args['orderby'] = 'comment_approved';
break;
default:
$comments_args['orderby'] = $args['orderby'];
break;
}
}
if ( isset( $args['order'] ) ) {
$comments_args['order'] = $args['order'];
}
if ( isset( $args['offset'] ) ) {
$comments_args['offset'] = $args['offset'];
}
if ( isset( $args['number'] ) ) {
$comments_args['number'] = $args['number'];
}
$comments = \Sensei_Utils::sensei_check_for_activity( $comments_args, true );
if ( empty( $comments ) ) {
return array();
}
$comments = is_array( $comments ) ? $comments : array( $comments );
$quiz_progresses = [];
foreach ( $comments as $comment ) {
$quiz_progresses[] = $this->create_progress_from_comment( $comment );
}
return $quiz_progresses;
}
/**
* Create a lesson progress from a comment.
*
* @param WP_Comment $comment The comment.
* @param int|null $quiz_id The quiz ID that is associated with the status comment.
* @return Comments_Based_Quiz_Progress The course progress.
*/
private function create_progress_from_comment( WP_Comment $comment, ?int $quiz_id = null ): Comments_Based_Quiz_Progress {
$comment_date = new DateTime( $comment->comment_date, wp_timezone() );
$meta_start = get_comment_meta( (int) $comment->comment_ID, 'start', true );
$started_at = ! empty( $meta_start ) ? new DateTime( $meta_start, wp_timezone() ) : current_datetime();
if ( in_array( $comment->comment_approved, [ 'complete', 'passed', 'graded' ], true ) ) {
$completed_at = $comment_date;
} else {
$completed_at = null;
}
if ( is_null( $quiz_id ) ) {
$quiz_id = Sensei()->lesson->lesson_quizzes( $comment->comment_post_ID );
}
return new Comments_Based_Quiz_Progress(
(int) $comment->comment_ID,
(int) $quiz_id,
(int) $comment->user_id,
$comment->comment_approved,
$started_at,
$completed_at,
$comment_date,
$comment_date
);
}
}