Skip to content

Commit

Permalink
WELLMS-423 Import course statistics (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
daVitekPL committed Nov 22, 2023
1 parent 05b5d5a commit f43d624
Show file tree
Hide file tree
Showing 16 changed files with 329 additions and 10 deletions.
6 changes: 6 additions & 0 deletions src/Http/Controllers/Admin/StatsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use EscolaLms\Reports\Http\Requests\Admin\DateRangeStatsRequest;
use EscolaLms\Reports\Http\Requests\Admin\ExportCourseStatRequest;
use EscolaLms\Reports\Http\Requests\Admin\ExportTopicStatRequest;
use EscolaLms\Reports\Http\Requests\Admin\ImportCoursesStatsRequest;
use EscolaLms\Reports\Http\Requests\Admin\TopicStatsRequest;
use EscolaLms\Reports\Services\Contracts\StatsServiceContract;
use Illuminate\Http\JsonResponse;
Expand Down Expand Up @@ -56,6 +57,11 @@ public function courseExport(ExportCourseStatRequest $request): BinaryFileRespon
return $this->statsService->export($request->getCourse(), $request->getStat());
}

public function courseImport(ImportCoursesStatsRequest $request)
{
$this->statsService->import($request->getCourse(), $request->file('file'));
}

public function topicExport(ExportTopicStatRequest $request): BinaryFileResponse
{
return $this->statsService->export($request->getTopic(), $request->getStat());
Expand Down
41 changes: 41 additions & 0 deletions src/Http/Requests/Admin/ImportCoursesStatsRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace EscolaLms\Reports\Http\Requests\Admin;

use EscolaLms\Courses\Models\Course;
use EscolaLms\Reports\Models\Report;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class ImportCoursesStatsRequest extends FormRequest
{
public function authorize()
{
return $this->user()->can('viewAny', Report::class) || $this->user()->can('update', $this->getCourse());
}

protected function prepareForValidation()
{
$this->merge([
'course_id' => $this->route('course_id')
]);
}

public function rules()
{
return [
'course_id' => ['required', 'integer', Rule::exists((new Course())->getTable(), 'id')],
'file' => ['required', 'file:xlsx'],
];
}

public function getCourseId(): int
{
return $this->validated()['course_id'];
}

public function getCourse(): Course
{
return Course::find($this->getCourseId());
}
}
31 changes: 31 additions & 0 deletions src/Imports/Stats/Course/FinishedTopicsImport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace EscolaLms\Reports\Imports\Stats\Course;

use EscolaLms\Courses\Models\Course;
use EscolaLms\Reports\Imports\Stats\Course\Sheets\FinishedTopicsAttemptsSheet;
use EscolaLms\Reports\Imports\Stats\Course\Sheets\FinishedTopicsSecondsSheet;
use EscolaLms\Reports\Imports\Stats\Course\Sheets\FinishedTopicsStatusesSheet;
use Maatwebsite\Excel\Concerns\Importable;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;

class FinishedTopicsImport implements WithMultipleSheets
{
use Importable;

protected Course $course;

public function __construct(Course $course)
{
$this->course = $course;
}

public function sheets(): array
{
return [
new FinishedTopicsStatusesSheet($this->course),
new FinishedTopicsSecondsSheet($this->course),
new FinishedTopicsAttemptsSheet($this->course),
];
}
}
43 changes: 43 additions & 0 deletions src/Imports/Stats/Course/Sheets/FinishedTopicsAttemptsSheet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace EscolaLms\Reports\Imports\Stats\Course\Sheets;

use EscolaLms\Auth\Models\User;
use EscolaLms\Courses\Models\CourseProgress;
use EscolaLms\Courses\Models\CourseUserAttendance;
use EscolaLms\Courses\Models\Topic;

class FinishedTopicsAttemptsSheet extends FinishedTopicsSheet
{
protected function processRow(User $user, Topic $topic, $value): CourseProgress
{
$courseProgress = parent::processRow($user, $topic, $value);

if ((int) $value === 1) {
CourseUserAttendance::query()->updateOrCreate([
'course_progress_id' => $courseProgress->getKey(),
'attempt' => 1,
'seconds' => 0,
], [
'attendance_date' => now(),
]);
}

CourseUserAttendance::query()->updateOrCreate([
'course_progress_id' => $courseProgress->getKey(),
'attempt' => $value,
], [
'seconds' => $courseProgress->seconds,
'attendance_date' => now(),
]);

return $courseProgress;
}

protected function prepareUpdateData($value, CourseProgress $courseProgress = null): array
{
return [
'attempts' => $value,
];
}
}
15 changes: 15 additions & 0 deletions src/Imports/Stats/Course/Sheets/FinishedTopicsSecondsSheet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace EscolaLms\Reports\Imports\Stats\Course\Sheets;

use EscolaLms\Courses\Models\CourseProgress;

class FinishedTopicsSecondsSheet extends FinishedTopicsSheet
{
protected function prepareUpdateData($value, CourseProgress $courseProgress = null): array
{
return [
'seconds' => $value,
];
}
}
61 changes: 61 additions & 0 deletions src/Imports/Stats/Course/Sheets/FinishedTopicsSheet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace EscolaLms\Reports\Imports\Stats\Course\Sheets;

use EscolaLms\Auth\Models\User;
use EscolaLms\Courses\Models\Course;
use EscolaLms\Courses\Models\CourseProgress;
use EscolaLms\Courses\Models\Topic;
use EscolaLms\Reports\Stats\Course\Strategies\TopicTitleStrategyContext;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Row;

abstract class FinishedTopicsSheet implements OnEachRow, WithHeadingRow
{
protected Course $course;
protected array $topics = [];

public function __construct(Course $course)
{
$this->course = $course;
$this->prepareTopics();
}

public function onRow(Row $row)
{
$user = User::query()->where('email', '=', $row['Email'])->first();
if ($user) {
foreach ($this->topics as $title => $topic) {
$this->processRow($user, $topic, $row[$title]);
}
}
}

protected function processRow(User $user, Topic $topic, $value): CourseProgress
{
$coursesProgress = CourseProgress::query()
->where('user_id', '=', $user->getKey())
->where('topic_id', '=', $topic->getKey())
->first();
if ($coursesProgress) {
$coursesProgress->update($this->prepareUpdateData($value, $coursesProgress));
} else {
$coursesProgress = CourseProgress::create(array_merge([
'user_id' => $user->getKey(),
'topic_id' => $topic->getKey(),
], $this->prepareUpdateData($value)));
}

return $coursesProgress;
}

protected abstract function prepareUpdateData($value, CourseProgress $courseProgress = null): array;

protected function prepareTopics()
{
$this->course->topics->each(function ($topic) {
$this->topics[(new TopicTitleStrategyContext($topic))->getStrategy()->makeTitle()] = $topic;
});
}
}
19 changes: 19 additions & 0 deletions src/Imports/Stats/Course/Sheets/FinishedTopicsStatusesSheet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace EscolaLms\Reports\Imports\Stats\Course\Sheets;

use EscolaLms\Courses\Models\CourseProgress;

class FinishedTopicsStatusesSheet extends FinishedTopicsSheet
{
protected function prepareUpdateData($value, CourseProgress $courseProgress = null): array
{
$value = (int) $value;

return [
'status' => $value,
'started_at' => $value > 0 ? ($this->course->active_from ?? now()->subDay()) : null,
'finished_at' => $value === 2 ? ($this->course->active_to ?? now()->subMinutes(random_int(1, 120))) : null,
];
}
}
2 changes: 2 additions & 0 deletions src/Services/Contracts/StatsServiceContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace EscolaLms\Reports\Services\Contracts;

use EscolaLms\Reports\Exceptions\ExportNotExistsException;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

interface StatsServiceContract
Expand All @@ -14,4 +15,5 @@ public function getAvailableStats($model = null): array;
* @throws ExportNotExistsException
*/
public function export($model, string $stat): BinaryFileResponse;
public function import($model, UploadedFile $file);
}
7 changes: 7 additions & 0 deletions src/Services/StatsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
namespace EscolaLms\Reports\Services;

use EscolaLms\Reports\Exceptions\ExportNotExistsException;
use EscolaLms\Reports\Imports\Stats\Course\FinishedTopicsImport;
use EscolaLms\Reports\Services\Contracts\StatsServiceContract;
use EscolaLms\Reports\Stats\StatsContract;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
Expand Down Expand Up @@ -65,4 +67,9 @@ public function export($model, string $stat): BinaryFileResponse
\Maatwebsite\Excel\Excel::XLSX
);
}

public function import($model, UploadedFile $file)
{
Excel::import(new FinishedTopicsImport($model), $file);
}
}
91 changes: 91 additions & 0 deletions src/Stats/Course/CourseUsersAndGroupsStat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace EscolaLms\Reports\Stats\Course;

use EscolaLms\Auth\Models\GroupUser;
use EscolaLms\Core\Models\User;
use EscolaLms\Courses\Models\Course;
use EscolaLms\Courses\Models\CourseGroupPivot;
use EscolaLms\Courses\Models\CourseProgress;
use EscolaLms\Courses\Models\CourseUserPivot;
use EscolaLms\Courses\Models\Group;
use EscolaLms\Courses\Models\Lesson;
use EscolaLms\Courses\Models\Topic;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Query\JoinClause;

abstract class CourseUsersAndGroupsStat extends AbstractCourseStat
{
protected string $topicTable;
protected string $lessonTable;
protected string $courseTable;
protected string $courseUserTable;
protected string $userTable;
protected string $courseProgressTable;
protected string $groupTable;
protected string $courseGroupTable;
protected string $userGroupTable;

public function __construct(Course $course)
{
parent::__construct($course);

$this->topicTable = (new Topic())->getTable();
$this->lessonTable = (new Lesson())->getTable();
$this->courseTable = (new Course())->getTable();
$this->courseUserTable = (new CourseUserPivot())->getTable();
$this->userTable = (new User())->getTable();
$this->courseProgressTable = (new CourseProgress())->getTable();
$this->groupTable = (new Group())->getTable();
$this->courseGroupTable = (new CourseGroupPivot())->getTable();
$this->userGroupTable = (new GroupUser())->getTable();
}

protected function getGroupUsers(): \Illuminate\Support\Collection
{
return $this->formatGroupResult(Topic::query()
->select(
$this->topicTable . '.id as topic_id',
$this->topicTable . '.title as topic_title',
$this->topicTable . '.topicable_id',
$this->topicTable . '.topicable_type',
$this->userTable . '.id as user_id',
$this->userTable . '.email as user_email',
$this->userTable . '.first_name as user_first_name',
$this->userTable . '.last_name as user_last_name',
$this->courseProgressTable . '.finished_at',
$this->courseProgressTable . '.seconds',
$this->courseProgressTable . '.started_at',
$this->courseProgressTable . '.attempt',
)
->with('topicable')
->join($this->lessonTable, $this->topicTable . '.lesson_id', '=', $this->lessonTable . '.id')
->join($this->courseTable, $this->lessonTable . '.course_id', '=', $this->courseTable . '.id')
->where($this->courseTable . '.id', '=', $this->course->getKey())
->join($this->courseGroupTable, $this->courseTable . '.id', '=', $this->courseGroupTable . '.course_id')
->join($this->groupTable, $this->courseGroupTable . '.group_id', '=', $this->groupTable . '.id')
->join($this->userGroupTable, $this->groupTable . '.id', '=', $this->userGroupTable . '.group_id')
->join($this->userTable, $this->userGroupTable . '.user_id', '=', $this->userTable . '.id')
->leftJoin($this->courseProgressTable, fn(JoinClause $join) => $join
->on($this->courseProgressTable . '.user_id', '=', $this->userTable . '.id')
->on($this->courseProgressTable . '.topic_id', '=', $this->topicTable . '.id')
)
->get());
}

private function formatGroupResult(Collection $result): \Illuminate\Support\Collection
{
return $result
->groupBy('user_email')
->map(function ($topics, $userEmail) {
$finished = collect($topics)->first(fn ($topic) => $topic->finished_at === null) === null;
return [
'id' => $topics[0]->user_id,
'name' => $topics[0]->user_first_name . ' ' . $topics[0]->user_last_name,
'email' => $userEmail,
'finished_at' => $finished ? collect($topics)->max('finished_at') : null,
'finished' => $finished,
];
});
}
}
6 changes: 3 additions & 3 deletions src/Stats/Course/FinishedCourse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace EscolaLms\Reports\Stats\Course;

class FinishedCourse extends AbstractCourseStat
class FinishedCourse extends CourseUsersAndGroupsStat
{
public function calculate()
{
$users = $this->course->users()->withPivot('updated_at', 'finished');
return $users
return array_merge($users
->get()
->map(fn($user) => [
'id' => $user->id,
Expand All @@ -17,6 +17,6 @@ public function calculate()
'finished' => $user->pivot->finished,
])
->values()
->toArray();
->toArray(), $this->getGroupUsers()->values()->toArray());
}
}
5 changes: 3 additions & 2 deletions src/Stats/Course/PeopleFinished.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace EscolaLms\Reports\Stats\Course;

class PeopleFinished extends AbstractCourseStat
class PeopleFinished extends CourseUsersAndGroupsStat
{
public function calculate(): int
{
return $this->course->users()->wherePivot('finished', '=', true)->count();
return $this->course->users()->wherePivot('finished', '=', true)->count()
+ $this->getGroupUsers()->countBy(fn ($progress) => $progress['finished'])->get(1);
}
}

0 comments on commit f43d624

Please sign in to comment.