diff --git a/common/helpers/UserQuestionnaireStatusHelper.php b/common/helpers/UserQuestionnaireStatusHelper.php index af45b75..3512ef3 100644 --- a/common/helpers/UserQuestionnaireStatusHelper.php +++ b/common/helpers/UserQuestionnaireStatusHelper.php @@ -23,6 +23,15 @@ class UserQuestionnaireStatusHelper ]; } + public static function listCompleteStatuses(): array + { + return [ + self::STATUS_COMPLETED, + self::STATUS_ON_INSPECTION + ]; + + } + /** * @throws Exception */ diff --git a/common/models/Answer.php b/common/models/Answer.php index d018ad5..25f3a85 100644 --- a/common/models/Answer.php +++ b/common/models/Answer.php @@ -89,11 +89,6 @@ class Answer extends \yii\db\ActiveRecord ->viaTable('question', ['id' => 'question_id']); } -// public function getUserQuestionnaire() -// { -// return $this->hasOne(\backend\modules\questionnaire\models\UserQuestionnaire::className(), ['id']) -// } - static function numCorrectAnswers($question_id) { return Answer::find() diff --git a/common/models/Question.php b/common/models/Question.php index 568db21..4048a50 100644 --- a/common/models/Question.php +++ b/common/models/Question.php @@ -133,7 +133,6 @@ class Question extends \yii\db\ActiveRecord { return self::find()->where(['questionnaire_id' => $questionnaire_id]) ->andWhere(['status' => '1']) - ->AsArray() ->all(); } } diff --git a/common/models/UserQuestionnaire.php b/common/models/UserQuestionnaire.php index 7927db6..7a9af0d 100644 --- a/common/models/UserQuestionnaire.php +++ b/common/models/UserQuestionnaire.php @@ -26,6 +26,8 @@ use \backend\modules\questionnaire\models\Answer; * @property int $score * @property int $status * @property double $percent_correct_answers + * @property string $testing_date + * @property string $start_testing * * @property Questionnaire $questionnaire * @property User $user @@ -63,7 +65,7 @@ class UserQuestionnaire extends ActiveRecord [['questionnaire_id', 'user_id', 'status'], 'required'], [['questionnaire_id', 'user_id', 'score', 'status'], 'integer'], [['percent_correct_answers'], 'number'], - [['created_at', 'updated_at', 'testing_date'], 'safe'], + [['created_at', 'updated_at', 'testing_date', 'start_testing'], 'safe'], [['uuid'], 'string', 'max' => 36], [['uuid'], 'unique'], [['questionnaire_id'], 'exist', 'skipOnError' => true, 'targetClass' => Questionnaire::className(), 'targetAttribute' => ['questionnaire_id' => 'id']], @@ -98,6 +100,7 @@ class UserQuestionnaire extends ActiveRecord 'created_at' => 'Дата создания', 'updated_at' => 'Дата обновления', 'testing_date' => 'Дата тестирования', + 'start_testing' => 'Дата начала тестирования', 'percent_correct_answers' => 'Процент правильных ответов', ]; } diff --git a/console/migrations/m231114_072752_add_column_start_testing_to_user_questionnaire.php b/console/migrations/m231114_072752_add_column_start_testing_to_user_questionnaire.php new file mode 100644 index 0000000..27fb155 --- /dev/null +++ b/console/migrations/m231114_072752_add_column_start_testing_to_user_questionnaire.php @@ -0,0 +1,25 @@ +addColumn('user_questionnaire', 'start_testing', $this->dateTime()); + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $this->dropColumn('user_questionnaire', 'start_testing'); + } +} diff --git a/frontend/modules/api/controllers/AnswerController.php b/frontend/modules/api/controllers/AnswerController.php deleted file mode 100644 index 57673a2..0000000 --- a/frontend/modules/api/controllers/AnswerController.php +++ /dev/null @@ -1,85 +0,0 @@ - ['get'], - ]; - } - - /** - * @OA\Get(path="/answer/get-answers", - * summary="Список ответов на вопрос", - * description="Получение списка ответов", - * security={ - * {"bearerAuth": {}} - * }, - * tags={"Tests"}, - * @OA\Parameter( - * name="question_id", - * in="query", - * required=true, - * description="id вопроса", - * @OA\Schema( - * type="integer", - * ) - * ), - * @OA\Response( - * response=200, - * description="Возвращает масив вопросов", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema(ref="#/components/schemas/AnswerExampleArr"), - * ), - * - * ), - * ) - * - * @throws NotFoundHttpException - */ - public function actionGetAnswers(): array - { - $question_id = Yii::$app->request->get('question_id'); - if(empty($question_id) or !is_numeric($question_id)) - { - throw new NotFoundHttpException('Incorrect question ID'); - } - - $answers = Answer::activeAnswers($question_id); - if(empty($answers)) { - throw new NotFoundHttpException('Answers not found or question inactive'); - } - - array_walk( $answers, function(&$arr){ - unset( - $arr['created_at'], - $arr['updated_at'], - $arr['answer_flag'], - $arr['status'] - ); - }); - - return $answers; - } -} diff --git a/frontend/modules/api/controllers/QuestionController.php b/frontend/modules/api/controllers/QuestionController.php index 4881fc0..e8f562e 100644 --- a/frontend/modules/api/controllers/QuestionController.php +++ b/frontend/modules/api/controllers/QuestionController.php @@ -2,36 +2,35 @@ namespace frontend\modules\api\controllers; -use common\helpers\UUIDHelper; -use common\models\Question; -use common\models\UserQuestionnaire; +use Exception; +use frontend\modules\api\models\questionnaire\forms\QuestionnaireUuidForm; +use frontend\modules\api\models\questionnaire\Question; +use frontend\modules\api\models\questionnaire\UserQuestionnaire; +use frontend\modules\api\services\UserQuestionnaireService; use Yii; -use yii\filters\auth\HttpBearerAuth; -use yii\rest\Controller; -use yii\web\NotFoundHttpException; +use yii\web\BadRequestHttpException; class QuestionController extends ApiController { - public function behaviors() - { - $behaviors = parent::behaviors(); - $behaviors['authenticator']['authMethods'] = [ - HttpBearerAuth::className(), - ]; - return $behaviors; - } + private UserQuestionnaireService $userQuestionnaireService; - public function verbs() + public function __construct( + $id, + $module, + UserQuestionnaireService $userQuestionnaireService, + $config = [] + ) { - return [ - 'get-questions' => ['get'], - ]; + $this->userQuestionnaireService = $userQuestionnaireService; + parent::__construct($id, $module, $config); } /** * @OA\Get(path="/question/get-questions", * summary="Список вопросов", - * description="Получение списка вопросов", + * description="Получение списка вопросов и возможные варианты ответа. Сохраняет временную метку начала тестирования, + от которой будет отсчитываться временной интервал на выполнение теста. При наличии лимита времени на выполнение теста. + При превышении лимита времени на выполнение будет возвращена ошибка: Time's up!", * security={ * {"bearerAuth": {}} * }, @@ -53,40 +52,28 @@ class QuestionController extends ApiController * @OA\Schema(ref="#/components/schemas/QuestionExampleArr"), * ), * - * * ), * ) * - * @throws NotFoundHttpException - * @throws \Exception + * @throws BadRequestHttpException + * @throws Exception */ public function actionGetQuestions(): array { - $uuid = Yii::$app->request->get('uuid'); + $form = new QuestionnaireUuidForm(); - if(empty($uuid) or !UUIDHelper::is_valid($uuid)) - { - throw new NotFoundHttpException('Incorrect questionnaire UUID'); + if ($form->load(Yii::$app->request->get()) && !$form->validate()) { + $errors = $form->errors; + throw new BadRequestHttpException(array_shift($errors)[0]); } - $questionnaire_id = UserQuestionnaire::getQuestionnaireId($uuid); + $userQuestionnaire = UserQuestionnaire::findOne(['uuid' => $form->uuid]); - $questions = Question::activeQuestions($questionnaire_id); - if(empty($questions)) { - throw new NotFoundHttpException('Questions not found'); + if (!$this->userQuestionnaireService->checkTimeLimit($userQuestionnaire)) { + UserQuestionnaireService::calculateScore($userQuestionnaire->uuid); + throw new BadRequestHttpException("Time's up!"); } - array_walk( $questions, function(&$arr){ - unset( - $arr['score'], - $arr['created_at'], - $arr['updated_at'], - $arr['status'], - $arr['questionnaire_id'] - ); - }); - - return $questions; + return Question::activeQuestions($userQuestionnaire->questionnaire_id); } - } diff --git a/frontend/modules/api/controllers/UserQuestionnaireController.php b/frontend/modules/api/controllers/UserQuestionnaireController.php index c769572..50e3c6c 100644 --- a/frontend/modules/api/controllers/UserQuestionnaireController.php +++ b/frontend/modules/api/controllers/UserQuestionnaireController.php @@ -2,8 +2,9 @@ namespace frontend\modules\api\controllers; -use frontend\modules\api\models\UserQuestionnaire; +use frontend\modules\api\models\questionnaire\UserQuestionnaire; use frontend\modules\api\services\UserQuestionnaireService; +use yii\base\InvalidConfigException; use yii\helpers\ArrayHelper; use yii\web\NotFoundHttpException; use yii\web\ServerErrorHttpException; @@ -14,14 +15,11 @@ class UserQuestionnaireController extends ApiController public function behaviors(): array { return ArrayHelper::merge(parent::behaviors(), [ - 'verbs' => [ 'class' => \yii\filters\VerbFilter::class, 'actions' => [ 'questionnaires-list' => ['get'], 'questionnaire-completed' => ['get'], - 'get-points-number' => ['get'], - 'get-question-number' => ['get'], ], ] ]); @@ -73,7 +71,7 @@ class UserQuestionnaireController extends ApiController /** * @OA\Get(path="/user-questionnaire/questionnaire-completed", * summary="Проверка теста", - * description="Выполнения проверки теста", + * description="Выполнение проверки теста", * security={ * {"bearerAuth": {}} * }, @@ -98,7 +96,7 @@ class UserQuestionnaireController extends ApiController * ) * * @throws NotFoundHttpException - * @throws ServerErrorHttpException + * @throws ServerErrorHttpException|InvalidConfigException */ public function actionQuestionnaireCompleted($user_questionnaire_uuid): UserQuestionnaire { @@ -108,90 +106,4 @@ class UserQuestionnaireController extends ApiController } return $userQuestionnaireModel; } - - /** - * @OA\Get(path="/user-questionnaire/get-points-number", - * summary="Количество балов в тесте", - * description="Возвращает максимальное количество балов за тест", - * security={ - * {"bearerAuth": {}} - * }, - * tags={"Tests"}, - * @OA\Parameter( - * name="user_questionnaire_uuid", - * in="query", - * required=true, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Response( - * response=200, - * description="Возвращает максимально возможное количество балов за тест", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * @OA\Property( - * property="sum_point", - * type="integer", - * example="61", - * ), - * ), - * ), - * - * ), - * ) - * @throws ServerErrorHttpException - */ - public function actionGetPointsNumber($user_questionnaire_uuid): array - { - $questionPointsNumber = UserQuestionnaireService::getPointsNumber($user_questionnaire_uuid); - if (empty($questionPointsNumber)) { - throw new ServerErrorHttpException('Question points not found!'); - } - return $questionPointsNumber; - } - - /** - * @OA\Get(path="/user-questionnaire/get-question-number", - * summary="Число вопросов в тесте", - * description="Возвращает число вопросов в тесте", - * security={ - * {"bearerAuth": {}} - * }, - * tags={"Tests"}, - * @OA\Parameter( - * name="user_questionnaire_uuid", - * in="query", - * required=true, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Response( - * response=200, - * description="Возвращает число вопросов в тесте", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * @OA\Property( - * property="question_number", - * type="integer", - * example="61", - * ), - * ), - * ), - * - * ), - * ) - * @throws ServerErrorHttpException - */ - public function actionGetQuestionNumber($user_questionnaire_uuid): array - { - $questionNumber = UserQuestionnaireService::getQuestionNumber($user_questionnaire_uuid); - if (empty($questionNumber)) { - throw new ServerErrorHttpException('Question number not found!'); - } - return $questionNumber; - } } diff --git a/frontend/modules/api/controllers/UserResponseController.php b/frontend/modules/api/controllers/UserResponseController.php index d9ffec5..251c378 100644 --- a/frontend/modules/api/controllers/UserResponseController.php +++ b/frontend/modules/api/controllers/UserResponseController.php @@ -2,6 +2,8 @@ namespace frontend\modules\api\controllers; +use frontend\modules\api\models\questionnaire\UserQuestionnaire; +use frontend\modules\api\services\UserQuestionnaireService; use frontend\modules\api\services\UserResponseService; use Yii; use yii\base\InvalidConfigException; @@ -10,18 +12,24 @@ use yii\web\ServerErrorHttpException; class UserResponseController extends ApiController { - public function verbs(): array + private UserQuestionnaireService $userQuestionnaireService; + + public function __construct( + $id, + $module, + UserQuestionnaireService $userQuestionnaireService, + $config = [] + ) { - return [ - 'set-response' => ['post'], - 'set-responses' => ['post'], - ]; + $this->userQuestionnaireService = $userQuestionnaireService; + parent::__construct($id, $module, $config); } /** * @OA\Post(path="/user-response/set-responses", * summary="Добавить массив ответов пользователя", - * description="Добавление массива ответов на вопросы от пользователя", + * description="Добавление массива ответов на вопросы от пользователя. При наличии лимита времени на выполнение теста, + будет проведена проверка. При превышении лимита времени на выполнение будет возвращена ошибка: Time's up!", * security={ * {"bearerAuth": {}} * }, @@ -58,7 +66,7 @@ class UserResponseController extends ApiController * * @OA\Response( * response=200, - * description="Возвращает объект Запроса", + * description="Возвращает масив ответов", * @OA\MediaType( * mediaType="application/json", * @OA\Schema(ref="#/components/schemas/UserResponseExampleArr"), @@ -66,12 +74,25 @@ class UserResponseController extends ApiController * ), * ) * + * @return array + * @throws BadRequestHttpException * @throws InvalidConfigException - * @throws ServerErrorHttpException|BadRequestHttpException + * @throws ServerErrorHttpException + * @throws \yii\web\NotFoundHttpException */ public function actionSetResponses(): array { - $userResponseModels = UserResponseService::createUserResponses(Yii::$app->getRequest()->getBodyParams()); + $uuid = Yii::$app->request->post('user_questionnaire_uuid'); + $userResponses = Yii::$app->request->post('userResponses'); + + $userQuestionnaire = UserQuestionnaire::findOne(['uuid' => $uuid]); + + if (!$this->userQuestionnaireService->checkTimeLimit($userQuestionnaire)) { + UserQuestionnaireService::calculateScore($userQuestionnaire->uuid); + throw new BadRequestHttpException("Time's up!"); + } + + $userResponseModels = UserResponseService::createUserResponses($userResponses, $uuid); foreach ($userResponseModels as $model) { if ($model->errors) { throw new ServerErrorHttpException(json_encode($model->errors)); diff --git a/frontend/modules/api/models/Answer.php b/frontend/modules/api/models/questionnaire/Answer.php similarity index 62% rename from frontend/modules/api/models/Answer.php rename to frontend/modules/api/models/questionnaire/Answer.php index a22fbc7..abc87a1 100644 --- a/frontend/modules/api/models/Answer.php +++ b/frontend/modules/api/models/questionnaire/Answer.php @@ -1,6 +1,6 @@ function () { + return $this->answers; + } + ]; + } + /** + * @return string[] + */ + public function extraFields(): array + { + return []; + } + + /** + * @return ActiveQuery + */ + public function getAnswers(): ActiveQuery + { + return $this->hasMany(Answer::class, ['question_id' => 'id']); + } } \ No newline at end of file diff --git a/frontend/modules/api/models/UserQuestionnaire.php b/frontend/modules/api/models/questionnaire/UserQuestionnaire.php similarity index 57% rename from frontend/modules/api/models/UserQuestionnaire.php rename to frontend/modules/api/models/questionnaire/UserQuestionnaire.php index 5ea7c37..21ed16b 100644 --- a/frontend/modules/api/models/UserQuestionnaire.php +++ b/frontend/modules/api/models/questionnaire/UserQuestionnaire.php @@ -1,6 +1,6 @@ function() { + 'questionnaire_title' => function () { return $this->questionnaire->title; }, - 'description' => function() { + 'description' => function () { return $this->questionnaire->description; }, + 'points_number' => function () { + return Question::find() + ->where(['questionnaire_id' => $this->questionnaire_id]) + ->andWhere(['status' => 1]) + ->sum('score'); + }, + 'number_questions' => function () { + return Question::find() + ->where(['questionnaire_id' => $this->questionnaire_id]) + ->andWhere(['status' => 1]) + ->count(); + }, + 'time_limit' => function () { + return $this->questionnaire->time_limit; + } ]; } diff --git a/frontend/modules/api/models/questionnaire/forms/QuestionnaireUuidForm.php b/frontend/modules/api/models/questionnaire/forms/QuestionnaireUuidForm.php new file mode 100644 index 0000000..ce1ab4f --- /dev/null +++ b/frontend/modules/api/models/questionnaire/forms/QuestionnaireUuidForm.php @@ -0,0 +1,32 @@ + false, 'targetClass' => UserQuestionnaire::class, 'targetAttribute' => ['uuid' => 'uuid']], + ]; + } + + /** + * @return string + */ + public function formName(): string + { + return ''; + } +} diff --git a/frontend/modules/api/services/UserQuestionnaireService.php b/frontend/modules/api/services/UserQuestionnaireService.php index 9f1340f..3062cc8 100644 --- a/frontend/modules/api/services/UserQuestionnaireService.php +++ b/frontend/modules/api/services/UserQuestionnaireService.php @@ -2,12 +2,12 @@ namespace frontend\modules\api\services; -use common\models\Question; +use common\helpers\UserQuestionnaireStatusHelper; use common\services\ScoreCalculatorService; -use frontend\modules\api\models\UserQuestionnaire; +use frontend\modules\api\models\questionnaire\UserQuestionnaire; use yii\base\InvalidConfigException; +use yii\web\BadRequestHttpException; use yii\web\NotFoundHttpException; -use yii\web\ServerErrorHttpException; class UserQuestionnaireService { @@ -26,45 +26,33 @@ class UserQuestionnaireService if (empty($userQuestionnaireModel)) { throw new NotFoundHttpException('The questionnaire with this uuid does not exist'); } - ScoreCalculatorService::rateResponses($userQuestionnaireModel); - if (ScoreCalculatorService::checkAnswerFlagsForNull($userQuestionnaireModel)) { - ScoreCalculatorService::calculateScore($userQuestionnaireModel); - } else { - $userQuestionnaireModel->status = 3; - $userQuestionnaireModel->save(); - } + + if (!in_array($userQuestionnaireModel->status, UserQuestionnaireStatusHelper::listCompleteStatuses() )) { + ScoreCalculatorService::rateResponses($userQuestionnaireModel); + if (ScoreCalculatorService::checkAnswerFlagsForNull($userQuestionnaireModel)) { + ScoreCalculatorService::calculateScore($userQuestionnaireModel); + } else { + $userQuestionnaireModel->status = 3; + $userQuestionnaireModel->save(); + } + } return $userQuestionnaireModel; } - /** - * @throws ServerErrorHttpException - */ - public static function getQuestionNumber($user_questionnaire_uuid): array + public function checkTimeLimit(UserQuestionnaire $userQuestionnaire): bool { - $userQuestionnaireModel = UserQuestionnaire::findOne(['uuid' => $user_questionnaire_uuid]); - if (empty($userQuestionnaireModel)) { - throw new ServerErrorHttpException('Not found UserQuestionnaire'); - } - $count = Question::find() - ->where(['questionnaire_id' => $userQuestionnaireModel->questionnaire_id]) - ->andWhere(['status' => 1]) - ->count(); - return array('question_number' => $count); - } + if (!$userQuestionnaire->start_testing) { + $userQuestionnaire->start_testing = date('Y:m:d H:i:s'); + $userQuestionnaire->save(); + } elseif ($userQuestionnaire->questionnaire->time_limit) { + $limitTime = strtotime($userQuestionnaire->questionnaire->time_limit) - strtotime("00:00:00"); + $currentTime = strtotime(date('Y-m-d H:i:s')); + $startTesting = strtotime($userQuestionnaire->start_testing); - /** - * @throws ServerErrorHttpException - */ - public static function getPointsNumber($user_questionnaire_uuid) - { - $userQuestionnaireModel = UserQuestionnaire::findOne(['uuid' => $user_questionnaire_uuid]); - if (empty($userQuestionnaireModel)) { - throw new ServerErrorHttpException('Not found UserQuestionnaire'); + if ($currentTime - $startTesting > $limitTime) { + return false; + } } - $pointSum = Question::find() - ->where(['questionnaire_id' => $userQuestionnaireModel->questionnaire_id]) - ->andWhere(['status' => 1]) - ->sum('score'); - return array('sum_point' => $pointSum); + return true; } } \ No newline at end of file diff --git a/frontend/modules/api/services/UserResponseService.php b/frontend/modules/api/services/UserResponseService.php index 7b41bdb..1d3abfe 100644 --- a/frontend/modules/api/services/UserResponseService.php +++ b/frontend/modules/api/services/UserResponseService.php @@ -14,17 +14,19 @@ class UserResponseService * @throws BadRequestHttpException * @throws ServerErrorHttpException */ - public static function createUserResponses($userResponsesParams): array + public static function createUserResponses($userResponsesParams, $uuid): array { $userResponseModels = array(); foreach ($userResponsesParams as $userResponse) { + $model = new UserResponse(); $model->load($userResponse, ''); + $model->user_questionnaire_uuid = $uuid; try { self::validateResponseModel($model); } catch (\Exception $ex) { - throw new BadRequestHttpException(json_encode('One of the parameters is empty!')); + throw new BadRequestHttpException($ex->getMessage()); } @@ -48,7 +50,7 @@ class UserResponseService } if (empty($model->user_id) or empty($model->question_id) or empty($model->user_questionnaire_uuid)) { - throw new BadRequestHttpException(json_encode('One of the parameters is empty!')); + throw new BadRequestHttpException(json_encode('One of t222he parameters is empty!')); } }