From c452b4c454c7f8a1758492ac72e1f2569750fc1f Mon Sep 17 00:00:00 2001 From: Jonathan Foucher <jfoucher@gmail.com> Date: Tue, 9 Aug 2022 11:32:23 +0200 Subject: [PATCH] Add maturity graph data + tests for fundamental level calculator --- .../MaturityAnswerLevelCalculator.php | 6 +- .../Controllers/Api/GraphDataController.php | 10 ++ app/Models/EvaluationMaturityAnswer.php | 4 +- app/Models/Measure.php | 1 + app/Repository/EvaluationRepository.php | 108 +++++++++--------- app/Repository/GraphDataRepository.php | 17 ++- .../EvaluationMaturityAnswerFactory.php | 3 +- database/seeders/OrganizationSeeder.php | 19 ++- routes/api.php | 1 + tests/Feature/GraphDataControllerTest.php | 60 ++++++++++ .../MaturityAnswerLevelCalculatorTest.php | 96 ++++++++++++++++ 11 files changed, 262 insertions(+), 63 deletions(-) create mode 100644 tests/Unit/MaturityAnswerLevelCalculatorTest.php diff --git a/app/Calculator/MaturityAnswerLevelCalculator.php b/app/Calculator/MaturityAnswerLevelCalculator.php index aa0657e2..177b2787 100644 --- a/app/Calculator/MaturityAnswerLevelCalculator.php +++ b/app/Calculator/MaturityAnswerLevelCalculator.php @@ -9,9 +9,11 @@ class MaturityAnswerLevelCalculator { public function calculateFundamentalLevel(Evaluation $evaluation): int { - $level = $evaluation->measureLevels->filter(function (MeasureLevel $ml) { + $fundamentalMeasures = $evaluation->measureLevels->filter(function (MeasureLevel $ml) { return true === $ml->measure->fundamental; - })->average(function (MeasureLevel $ml) { + }); + + $level = $fundamentalMeasures->average(function (MeasureLevel $ml) { return $ml->actual_level; }); diff --git a/app/Http/Controllers/Api/GraphDataController.php b/app/Http/Controllers/Api/GraphDataController.php index e488dc23..b2a856be 100644 --- a/app/Http/Controllers/Api/GraphDataController.php +++ b/app/Http/Controllers/Api/GraphDataController.php @@ -45,6 +45,16 @@ public function risks(int $id): Collection|array|\Illuminate\Support\Collection return $this->repository->getRisksDataForEvaluation($evaluation); } + /** + * @param int $id Evaluation id + */ + public function maturity(int $id): Collection|array|\Illuminate\Support\Collection + { + $evaluation = $this->getEvaluation($id); + + return $this->repository->getMaturityDataForEvaluation($evaluation); + } + /** * @param int $id Evaluation id */ diff --git a/app/Models/EvaluationMaturityAnswer.php b/app/Models/EvaluationMaturityAnswer.php index 6f10d976..3681ad62 100644 --- a/app/Models/EvaluationMaturityAnswer.php +++ b/app/Models/EvaluationMaturityAnswer.php @@ -33,11 +33,11 @@ public function evaluation(): BelongsTo public function question(): BelongsTo { - return $this->belongsTo(MaturityQuestion::class); + return $this->belongsTo(MaturityQuestion::class, 'maturity_question_id'); } public function answer(): BelongsTo { - return $this->belongsTo(MaturityAnswer::class); + return $this->belongsTo(MaturityAnswer::class, 'maturity_answer_id'); } } diff --git a/app/Models/Measure.php b/app/Models/Measure.php index db55df09..5d61f7f7 100644 --- a/app/Models/Measure.php +++ b/app/Models/Measure.php @@ -66,6 +66,7 @@ class Measure extends Model 'level3_cost' => 'string', 'level3_duration' => 'string', 'level3_assistance' => 'int', + 'fundamental' => 'bool', ]; public function scenarios(): BelongsToMany diff --git a/app/Repository/EvaluationRepository.php b/app/Repository/EvaluationRepository.php index f915af4f..693f6bb7 100644 --- a/app/Repository/EvaluationRepository.php +++ b/app/Repository/EvaluationRepository.php @@ -19,67 +19,73 @@ public function __construct(MaturityAnswerLevelCalculator $calculator) $this->calculator = $calculator; } - /** - * Save maturity answers - * @throws MissingMaturityAnswerException - */ - public function saveMaturity(Evaluation $evaluation, Collection $answers, int $draft = 0) + public function saveAutomaticAnswers($evaluation) { - $questions = MaturityQuestion::all(); + $questions = MaturityQuestion::where('auto', true)->get(); foreach ($questions as $question) { - if ($question->auto) { - // Calculate answer based on data - // For "execution" - // Get completed evaluation before this one - // If there are none then all calculated levels are 0 - // (nombre de mesure basculée en mesure en place dans l'évaluation en cours/nombre de mesure prévue dans évaluation précédente) *100 - // 0 => level 0 - // 0 < calculated_level < 20 => level 1 - // 20 < calculated_level < 80 => level 2 - // calculated_level >= 80 => level 3 - // For "fondamentaux" - // MOYENNE.INF( niveau de la mesure en place sauvegarde, niveau de la mesure en place mot de passe, niveau de la mesure en place mise à jour , niveau de la mesure en place sensibilisation) + // Calculate answer based on data + // For "execution" + // Get completed evaluation before this one + // If there are none then all calculated levels are 0 + // (nombre de mesure basculée en mesure en place dans l'évaluation en cours/nombre de mesure prévue dans évaluation précédente) *100 + // 0 => level 0 + // 0 < calculated_level < 20 => level 1 + // 20 < calculated_level < 80 => level 2 + // calculated_level >= 80 => level 3 + // For "fondamentaux" + // MOYENNE.INF( niveau de la mesure en place sauvegarde, niveau de la mesure en place mot de passe, niveau de la mesure en place mise à jour , niveau de la mesure en place sensibilisation) - $calculated_level = 0; - if ('Fondamentaux' === $question->title) { - $calculated_level = $this->calculator->calculateFundamentalLevel($evaluation); - } + $calculated_level = 0; + if ('Fondamentaux' === $question->title) { + $calculated_level = $this->calculator->calculateFundamentalLevel($evaluation); + } - if ('Exécution' === $question->title) { - $calculated_level = $this->calculator->calculateExecutionLevel($evaluation); - } + if ('Exécution' === $question->title) { + $calculated_level = $this->calculator->calculateExecutionLevel($evaluation); + } - $answer = MaturityAnswer::where('maturity_question_id', $question->id) - ->where('level', $calculated_level) - ->first(); + $answer = MaturityAnswer::where('maturity_question_id', $question->id) + ->where('level', $calculated_level) + ->first(); - $evalAnswer = EvaluationMaturityAnswer::where('evaluation_id', $evaluation->id) - ->where('maturity_question_id', $question->id) - ->first(); - if (!$evalAnswer) { - $evalAnswer = new EvaluationMaturityAnswer(); - } + $evalAnswer = EvaluationMaturityAnswer::where('evaluation_id', $evaluation->id) + ->where('maturity_question_id', $question->id) + ->first(); + if (!$evalAnswer) { + $evalAnswer = new EvaluationMaturityAnswer(); + } - $evalAnswer->maturity_question_id = $question->id; - $evalAnswer->maturity_answer_id = $answer->id; - $evalAnswer->evaluation_id = $evaluation->id; - $evalAnswer->save(); - } else { - // Get answer from sent values - $answer = $answers->filter(function ($a) use ($question) { - return $a['maturity_question_id'] === $question->id; - })->first(); - if ((!isset($answer) || !isset($answer['maturity_answer_id'])) && 0 === $draft) { - throw new MissingMaturityAnswerException(); - } + $evalAnswer->maturity_question_id = $question->id; + $evalAnswer->maturity_answer_id = $answer->id; + $evalAnswer->evaluation_id = $evaluation->id; + $evalAnswer->save(); + } + } - $maturity = isset($answer['id']) ? EvaluationMaturityAnswer::find($answer['id']) : new EvaluationMaturityAnswer(); - $maturity->maturity_question_id = $answer['maturity_question_id']; - $maturity->maturity_answer_id = $answer['maturity_answer_id']; - $maturity->evaluation_id = $evaluation->id; - $maturity->save(); + /** + * Save maturity answers + * @throws MissingMaturityAnswerException + */ + public function saveMaturity(Evaluation $evaluation, Collection $answers, int $draft = 0) + { + $questions = MaturityQuestion::where('auto', false)->get(); + foreach ($questions as $question) { + // Get answer from sent values + $answer = $answers->filter(function ($a) use ($question) { + return $a['maturity_question_id'] === $question->id; + })->first(); + if ((!isset($answer) || !isset($answer['maturity_answer_id'])) && 0 === $draft) { + throw new MissingMaturityAnswerException(); } + + $maturity = isset($answer['id']) ? EvaluationMaturityAnswer::find($answer['id']) : new EvaluationMaturityAnswer(); + $maturity->maturity_question_id = $answer['maturity_question_id']; + $maturity->maturity_answer_id = $answer['maturity_answer_id']; + $maturity->evaluation_id = $evaluation->id; + $maturity->save(); } + + $this->saveAutomaticAnswers($evaluation); } } diff --git a/app/Repository/GraphDataRepository.php b/app/Repository/GraphDataRepository.php index 63826056..efd0f599 100644 --- a/app/Repository/GraphDataRepository.php +++ b/app/Repository/GraphDataRepository.php @@ -2,6 +2,7 @@ namespace App\Repository; +use App\Calculator\MaturityAnswerLevelCalculator; use App\Models\Danger; use App\Models\DangerLevelEvaluation; use App\Models\Evaluation; @@ -12,6 +13,13 @@ class GraphDataRepository { + private EvaluationRepository $repository; + + public function __construct(EvaluationRepository $repository) + { + $this->repository = $repository; + } + public function getMeasuresDataForEvaluation(Evaluation $evaluation): array { $measureLevels = collect($evaluation->measureLevels)->sortBy(function (MeasureLevel $ml) { @@ -164,6 +172,11 @@ public function getAttackDataForEvaluation(Evaluation $evaluation): array public function getMaturityDataForEvaluation(Evaluation $evaluation): array { + // Calculate automatic answer results + $this->repository->saveAutomaticAnswers($evaluation); + + $evaluation = Evaluation::find($evaluation->id); + $maturityLevels = $evaluation->maturityLevels; $values = $maturityLevels->map(function (EvaluationMaturityAnswer $ml) { @@ -171,8 +184,8 @@ public function getMaturityDataForEvaluation(Evaluation $evaluation): array }); return [ - 'labels' => $scenarios->pluck('name'), - 'data' => $values, + 'labels' => $values->pluck('label'), + 'data' => $values->pluck('level'), ]; } diff --git a/database/factories/EvaluationMaturityAnswerFactory.php b/database/factories/EvaluationMaturityAnswerFactory.php index eb2bea34..c41ce709 100644 --- a/database/factories/EvaluationMaturityAnswerFactory.php +++ b/database/factories/EvaluationMaturityAnswerFactory.php @@ -19,9 +19,10 @@ class EvaluationMaturityAnswerFactory extends Factory */ public function definition() { + $mqid = MaturityQuestion::inRandomOrder()->first()->id; return [ 'maturity_question_id' => MaturityQuestion::inRandomOrder()->first()->id, - 'maturity_answer_id' => MaturityAnswer::inRandomOrder()->first()->id, + 'maturity_answer_id' => MaturityAnswer::where('maturity_question_id', $mqid)->inRandomOrder()->first()->id, 'evaluation_id' => Evaluation::inRandomOrder()->first()->id, ]; } diff --git a/database/seeders/OrganizationSeeder.php b/database/seeders/OrganizationSeeder.php index 592d181a..1289203a 100644 --- a/database/seeders/OrganizationSeeder.php +++ b/database/seeders/OrganizationSeeder.php @@ -32,11 +32,16 @@ public function run() $org->evaluations()->saveMany(Evaluation::factory(2)->make(['current_step' => 4])); $org->evaluations()->saveMany(Evaluation::factory(2)->make(['current_step' => 5])); $org->evaluations()->saveMany(Evaluation::factory(2)->make([ - 'current_step' => 5, + 'current_step' => Evaluation::STEP_MATURITY, 'reference' => env('REFERENTIEL_VERSION'), ])); $org->evaluations()->saveMany(Evaluation::factory(2)->make([ - 'current_step' => 2, + 'current_step' => Evaluation::STEP_RESULTS, + 'status' => Evaluation::STATUS_DONE, + 'reference' => env('REFERENTIEL_VERSION'), + ])); + $org->evaluations()->saveMany(Evaluation::factory(2)->make([ + 'current_step' => Evaluation::STEP_MEASURES, 'reference' => env('REFERENTIEL_VERSION'), ])); $org->evaluations()->saveMany(Evaluation::factory(2)->make(['current_step' => 1])); @@ -66,8 +71,10 @@ public function run() if ($eval->current_step >= Evaluation::STEP_MEASURES) { $measures = Measure::all(); - $evalMeasures = $measures->map(function (Measure $m) { - return MeasureLevel::factory()->make(['measure_id' => $m->id]); + $evalMeasures = $measures->map(function (Measure $m) use ($eval) { + return MeasureLevel::factory()->make([ + 'measure_id' => $m->id, + ]); }); $eval->measureLevels()->saveMany($evalMeasures); } @@ -77,7 +84,9 @@ public function run() $answers = $questions->map(function (MaturityQuestion $m) { $a = new EvaluationMaturityAnswer(); $a->maturity_question_id = $m->id; - $a->maturity_answer_id = MaturityAnswer::inRandomOrder()->first()->id; + $a->maturity_answer_id = MaturityAnswer::where('maturity_question_id', $m->id) + ->inRandomOrder() + ->first()->id; return $a; }); diff --git a/routes/api.php b/routes/api.php index d9248f48..7e9b2df9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -57,6 +57,7 @@ Route::get('/{id}/graphs/futurerisks', [GraphDataController::class, 'futurerisks'])->name('api.evaluations.graph.futurerisks'); Route::get('/{id}/graphs/exposition', [GraphDataController::class, 'exposition'])->name('api.evaluations.graph.exposition'); Route::get('/{id}/graphs/attack', [GraphDataController::class, 'attack'])->name('api.evaluations.graph.attack'); + Route::get('/{id}/graphs/maturity', [GraphDataController::class, 'maturity'])->name('api.evaluations.graph.maturity'); Route::delete('/{id}', [EvaluationsController::class, 'delete'])->name('api.evaluations.delete'); }); Route::prefix('/dangers')->middleware('auth:sanctum')->group(function () { diff --git a/tests/Feature/GraphDataControllerTest.php b/tests/Feature/GraphDataControllerTest.php index 0a4b1666..663d50d5 100644 --- a/tests/Feature/GraphDataControllerTest.php +++ b/tests/Feature/GraphDataControllerTest.php @@ -4,7 +4,10 @@ use App\Models\DangerLevel; use App\Models\Evaluation; +use App\Models\EvaluationMaturityAnswer; +use App\Models\MaturityAnswer; use App\Models\User; +use App\Repository\EvaluationRepository; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -207,4 +210,61 @@ public function testCheckEvaluationExpositionGraphData() $this->assertEquals($expected, $data); } + + + public function testCheckEvaluationMaturityGraphData() + { + /** + * @var Evaluation $evaluation + */ + $evaluation = Evaluation::with('dangerLevels') + ->with('maturityLevels') + ->with('measureLevels') + ->where('current_step', '>=', 5) + ->where('status', Evaluation::STATUS_DONE) + ->first(); + + // Set all levels to know quantity + foreach ($evaluation->measureLevels as $k => $measureLevel) { + $measureLevel->actual_level = 3; + $measureLevel->save(); + } + + foreach ($evaluation->maturityLevels as $k => $maturityLevel) { + $maturityLevel->maturity_answer_id = MaturityAnswer::where('level', $k % 3) + ->where('maturity_question_id', $maturityLevel->maturity_question_id) + ->first()->id; + + $maturityLevel->save(); + } + + $admin = User::where('role', User::ROLE_ADMIN)->first(); + + $response = $this->actingAs($admin)->getJson(route('api.evaluations.graph.maturity', ['id' => $evaluation->id])); + + $response->assertOk(); + + $data = $response->json(); + + $expected = array ( + 'labels' => + array ( + 0 => 'Connaissance', + 1 => 'Organisation', + 2 => 'Motivation', + 3 => 'Exécution', + 4 => 'Fondamentaux', + ), + 'data' => + array ( + 0 => 0, + 1 => 1, + 2 => 2, + 3 => 0, + 4 => 3, + ), + ); + + $this->assertEquals($expected, $data); + } } diff --git a/tests/Unit/MaturityAnswerLevelCalculatorTest.php b/tests/Unit/MaturityAnswerLevelCalculatorTest.php new file mode 100644 index 00000000..91dda54c --- /dev/null +++ b/tests/Unit/MaturityAnswerLevelCalculatorTest.php @@ -0,0 +1,96 @@ +<?php + +namespace Tests\Unit; + +use App\Calculator\MaturityAnswerLevelCalculator; +use App\Models\Evaluation; +use App\Models\Measure; +use App\Models\MeasureLevel; +use Tests\TestCase; + +class MaturityAnswerLevelCalculatorTest extends TestCase +{ + public function testCalculateFundamentalLevelWithOneLevel() + { + $evaluation = new Evaluation(); + + $measure = new Measure(); + $measure->fundamental = true; + + $measureLevel = new MeasureLevel(); + $measureLevel->measure = $measure; + $measureLevel->evaluation = $evaluation; + $measureLevel->actual_level = 3; + + $evaluation->measureLevels->add($measureLevel); + + $calculator = new MaturityAnswerLevelCalculator(); + + $res = $calculator->calculateFundamentalLevel($evaluation); + + $this->assertEquals(3, $res); + } + + public function testCalculateFundamentalLevelWithSeveralLevels() + { + $evaluation = new Evaluation(); + + $measure = new Measure(); + $measure->fundamental = true; + + $measureLevel = new MeasureLevel(); + $measureLevel->measure = $measure; + $measureLevel->evaluation = $evaluation; + $measureLevel->actual_level = 3; + + $evaluation->measureLevels->add($measureLevel); + + $measure = new Measure(); + $measure->fundamental = true; + + $measureLevel = new MeasureLevel(); + $measureLevel->measure = $measure; + $measureLevel->evaluation = $evaluation; + $measureLevel->actual_level = 1; + + $evaluation->measureLevels->add($measureLevel); + + $calculator = new MaturityAnswerLevelCalculator(); + + $res = $calculator->calculateFundamentalLevel($evaluation); + + $this->assertEquals(2, $res); + } + + + public function testCalculateFundamentalLevelWithNonFundamentalLevels() + { + $evaluation = new Evaluation(); + + $measure = new Measure(); + $measure->fundamental = true; + + $measureLevel = new MeasureLevel(); + $measureLevel->measure = $measure; + $measureLevel->evaluation = $evaluation; + $measureLevel->actual_level = 3; + + $evaluation->measureLevels->add($measureLevel); + + $measure = new Measure(); + $measure->fundamental = false; + + $measureLevel = new MeasureLevel(); + $measureLevel->measure = $measure; + $measureLevel->evaluation = $evaluation; + $measureLevel->actual_level = 1; + + $evaluation->measureLevels->add($measureLevel); + + $calculator = new MaturityAnswerLevelCalculator(); + + $res = $calculator->calculateFundamentalLevel($evaluation); + + $this->assertEquals(3, $res); + } +} -- GitLab