Skip to content
Snippets Groups Projects
Commit c706fd92 authored by Marcus Eibrink-Lunzenauer's avatar Marcus Eibrink-Lunzenauer
Browse files

Coursewares Übersichtsseite beschleunigen

parent d62f1b74
No related branches found
No related tags found
No related merge requests found
......@@ -60,8 +60,8 @@ class Course_CoursewareController extends AuthenticatedController
Navigation::activateItem('course/courseware/content');
$this->setIndexSidebar();
$this->licenses = array();
$sorm_licenses = License::findBySQL("1 ORDER BY name ASC");
$this->licenses = [];
$sorm_licenses = License::findBySQL('1 ORDER BY name ASC');
foreach ($sorm_licenses as $license) {
array_push($this->licenses, $license->toArray());
}
......@@ -87,7 +87,6 @@ class Course_CoursewareController extends AuthenticatedController
} else {
Navigation::activateItem('course/courseware/manager');
}
}
private function setIndexSidebar(): void
......@@ -106,151 +105,145 @@ class Course_CoursewareController extends AuthenticatedController
$sidebar->addWidget($views)->addLayoutCSSClass('courseware-view-widget');
}
private function getProgressData(bool $course_progress = false): array
private function getProgressData(bool $showProgressForAllParticipants = false): iterable
{
$data = [];
/** @var ?\Course $course */
$course = Context::get();
if (!$course || !$course->courseware) {
return [];
}
$cid = Context::getId();
$course = Course::find($cid);
$course_members = $course->getMembersWithStatus('autor');
$course_member_ids = array_column($course_members, 'user_id');
$instance = new Instance($course->courseware);
$user = \User::findCurrent();
$elements = StructuralElement::findBySQL('range_id = ?', [$cid]);
$elements = $this->findElements($instance, $user);
$progress = $this->computeSelfProgresses($instance, $user, $elements, $showProgressForAllParticipants);
$progress = $this->computeCumulativeProgresses($instance, $elements, $progress);
if ($course_progress) {
$cw_user_progresses = UserProgress::findBySQL('user_id IN (?)', [$course_member_ids]);
} else {
$cw_user_progresses = UserProgress::findBySQL('user_id = ?', [
$GLOBALS['user']->id,
]);
return $this->prepareProgressData($elements, $progress);
}
foreach ($elements as $element) {
$el = [
'id' => $element->id,
'name' => $element->title,
'parent_id' => $element->parent->id,
'parent_name' => $element->parent->title,
'children' => $this->getChildren($element->children),
];
$el['progress'] = $this->getProgress($course, $element, $course_progress, $cw_user_progresses, $course_member_ids);
private function findElements(Instance $instance, User $user): iterable
{
$elements = $instance->getRoot()->findDescendants($user);
$elements[] = $instance->getRoot();
array_push($data, $el);
return array_combine(array_column($elements, 'id'), $elements);
}
//update children progress
foreach ($data as &$element) {
if (count($element['children'])) {
foreach ($element['children'] as &$child) {
foreach ($data as $el) {
if ($el['id'] == $child['id']) {
$child['progress'] = $el['progress'];
}
}
}
}
private function computeChildrenOf(iterable $elements): iterable
{
$childrenOf = [];
foreach ($elements as $elementId => $element) {
if ($element['parent_id']) {
if (!isset($childrenOf[$element['parent_id']])) {
$childrenOf[$element['parent_id']] = [];
}
$childrenOf[$element['parent_id']][] = $elementId;
}
}
return $childrenOf;
}
private function computeSelfProgresses(
Instance $instance,
User $user,
iterable $elements,
bool $showProgressForAllParticipants
): iterable {
$progress = [];
/** @var \Course $course */
$course = $instance->getRange();
$allBlocks = $instance->findAllBlocksGroupedByStructuralElementId();
$courseMemberIds = $showProgressForAllParticipants
? array_column($course->getMembersWithStatus('autor'), 'user_id')
: [$user->getId()];
$userProgresses = UserProgress::findBySQL('user_id IN (?)', [$courseMemberIds]);
foreach ($elements as $elementId => $element) {
$selfProgress = $this->getSelfProgresses($allBlocks, $elementId, $userProgresses, $courseMemberIds);
$progress[$elementId] = [
'self' => $selfProgress['counter'] ? $selfProgress['progress'] / $selfProgress['counter'] : 1,
];
}
return $data;
return $progress;
}
private function getChildren($children): array
{
$data = [];
foreach ($children as $child) {
$el = [
'id' => $child->id,
'name' => $child->title,
private function getSelfProgresses(
array $allBlocks,
string $elementId,
array $userProgresses,
array $courseMemberIds
): array {
$blks = $allBlocks[$elementId] ?: [];
$data = [
'counter' => count($blks),
'progress' => 0,
];
array_push($data, $el);
$usersCounter = count($courseMemberIds);
foreach ($blks as $blk) {
$progresses = array_filter($userProgresses, function ($progress) use ($blk, $courseMemberIds) {
return $progress->block_id === $blk->getId() && in_array($progress->user_id, $courseMemberIds);
});
$usersProgress = count($progresses) ? array_sum(array_column($progresses, 'grade')) : 0;
$data['progress'] += $usersProgress / $usersCounter;
}
return $data;
}
private function getProgress(Course $course, StructuralElement $element, bool $course_progress = false, array $cw_user_progresses, array $course_member_ids): array
private function computeCumulativeProgresses(Instance $instance, iterable $elements, iterable $progress): iterable
{
$descendants = $element->findDescendants(\User::findCurrent());
$count = count($descendants);
$progress = 0;
$own_progress = 0;
foreach ($descendants as $el) {
$block = $this->getBlocks($el->id, $course_progress, $cw_user_progresses, $course, $course_member_ids);
if ($block['counter'] > 0) {
$progress += $block['progress'] / $block['counter'];
} else {
$progress += 1;
}
}
$childrenOf = $this->computeChildrenOf($elements);
$own_blocks = $this->getBlocks($element->id, $course_progress, $cw_user_progresses, $course, $course_member_ids);
// compute `cumulative` of each element
$visitor = function (&$progress, $element) use (&$childrenOf, &$elements, &$visitor) {
$elementId = $element->getId();
$numberOfNodes = 0;
$cumulative = 0;
if ($own_blocks['counter'] > 0) {
$own_progress = $own_blocks['progress'] / $own_blocks['counter'];
} else {
$own_progress = 1;
// visit children first
if (isset($childrenOf[$elementId])) {
foreach ($childrenOf[$elementId] as $childId) {
$visitor($progress, $elements[$childId]);
$numberOfNodes += $progress[$childId]['numberOfNodes'];
$cumulative += $progress[$childId]['cumulative'];
}
$count = count($descendants);
if ($count > 0) {
$progress = ($progress + $own_progress) / ($count + 1);
} else {
$progress = $own_progress;
}
return ['total' => round($progress, 2) * 100, 'current' => round($own_progress, 2) * 100];
}
$progress[$elementId]['cumulative'] = $cumulative + $progress[$elementId]['self'];
$progress[$elementId]['numberOfNodes'] = $numberOfNodes + 1;
private function getBlocks(string $element_id, bool $course_progress = false, array $cw_user_progresses, Course $course, array $course_member_ids): array
{
$containers = Courseware\Container::findBySQL('structural_element_id = ?', [intval($element_id)]);
$blocks = [];
$blocks['counter'] = 0;
$blocks['progress'] = 0;
$users_counter = count($course->getMembersWithStatus('autor'));
foreach ($containers as $container) {
$counter = $container->countBlocks();
$blocks['counter'] += $counter;
if ($counter > 0) {
$blks = Courseware\Block::findBySQL('container_id = ?', [$container->id]);
foreach ($blks as $item) {
if ($course_progress) {
if ($users_counter > 0) {
$progresses = array_filter($cw_user_progresses, function($progress) use ($item) {
if ($progress->block_id === $item->id) {
return true;
}
});
return $progress;
};
$users_progress = 0;
foreach ($progresses as $prog) {
if (in_array($prog->user_id, $course_member_ids)) {
$users_progress += $prog->grade;
}
}
$visitor($progress, $instance->getRoot());
$blocks['progress'] += $users_progress / $users_counter;
}
} else {
$uid = $GLOBALS['user']->id;
$progresses = array_filter($cw_user_progresses, function($progress) use ($item, $uid) {
if ($progress->block_id === $item->id && $progress->user_id === $uid) {
return true;
}
});
$progress = reset($progresses);
if ($progress !== null) {
$blocks['progress'] += intval($progress->grade);
}
}
}
return $progress;
}
private function prepareProgressData(iterable $elements, iterable $progress): iterable
{
$data = [];
foreach ($elements as $elementId => $element) {
$elementProgress = $progress[$elementId];
$cumulative = $elementProgress['cumulative'] / $elementProgress['numberOfNodes'];
$data[$elementId] = [
'id' => (int) $elementId,
'parent_id' => (int) $element['parent_id'],
'name' => $element['title'],
'progress' => [
'cumulative' => round($cumulative, 2) * 100,
'self' => round($elementProgress['self'], 2) * 100,
],
];
}
return $blocks;
return $data;
}
private function getChapterCounter(array $chapters): array
......@@ -261,13 +254,13 @@ class Course_CoursewareController extends AuthenticatedController
foreach ($chapters as $chapter) {
if ($chapter['parent_id'] != null) {
if ($chapter['progress']['current'] == 0) {
if ($chapter['progress']['self'] == 0) {
$ahead += 1;
}
if ($chapter['progress']['current'] > 0 && $chapter['progress']['current'] < 100) {
if ($chapter['progress']['self'] > 0 && $chapter['progress']['self'] < 100) {
$started += 1;
}
if ($chapter['progress']['current'] == 100) {
if ($chapter['progress']['self'] == 100) {
$finished += 1;
}
}
......
......@@ -69,6 +69,7 @@
* @property Course parent belongs_to Course
* @property SimpleORMapCollection children has_many Course
* @property CourseConfig config additional field
* @property ?\Courseware\StructuralElement $courseware has_one
*/
class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, FeedbackRange
......
......@@ -235,4 +235,64 @@ class Instance
{
return StructuralElement::findUsersBookmarksByRange($user, $this->getRange());
}
public function findAllStructuralElements(): iterable
{
$sql = 'SELECT se.*
FROM cw_structural_elements se
WHERE se.range_id = ? AND se.range_type = ?';
$statement = \DBManager::get()->prepare($sql);
$statement->execute([$this->root['range_id'], $this->root['range_type']]);
$data = [];
foreach ($statement as $key => $row) {
$data[] = \Courseware\StructuralElement::build($row, false);
}
return $data;
}
public function findAllBlocks(): iterable
{
$sql = 'SELECT b.*
FROM cw_structural_elements se
JOIN cw_containers c ON se.id = c.structural_element_id
JOIN cw_blocks b ON c.id = b.container_id
WHERE se.range_id = ? AND se.range_type = ?';
$statement = \DBManager::get()->prepare($sql);
$statement->execute([$this->root['range_id'], $this->root['range_type']]);
$data = [];
foreach ($statement as $key => $row) {
$data[] = \Courseware\Block::build($row, false);
}
return $data;
}
public function findAllBlocksGroupedByStructuralElementId(): iterable
{
$sql = 'SELECT se.id AS structural_element_id, b.*
FROM cw_structural_elements se
JOIN cw_containers c ON se.id = c.structural_element_id
JOIN cw_blocks b ON c.id = b.container_id
WHERE se.range_id = ? AND se.range_type = ?';
$statement = \DBManager::get()->prepare($sql);
$statement->execute([$this->root['range_id'], $this->root['range_type']]);
$data = [];
foreach ($statement as $row) {
$structuralElementId = $row['structural_element_id'];
unset($row['structural_element_id']);
$block = \Courseware\Block::build($row, false);
if (!isset($data[$structuralElementId])) {
$data[$structuralElementId] = [];
}
$data[$structuralElementId][] = $block;
}
return $data;
}
}
......@@ -1994,6 +1994,9 @@ d a s h b o a r d
width: 404px;
color: $base-color;
padding-left: 14px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
......
<template>
<div class="cw-dashboard-progress">
<div class="cw-dashboard-progress-breadcrumb">
<span v-if="currentChapter.parent_id !== null" @click="getRoot"><studip-icon shape="home" /></span>
<span v-if="currentChapter.parent_id !== null" @click="selectChapter(currentChapter.parent_id)">
/ {{ currentChapter.parent_name }}</span
>
<span v-if="parent" @click="visitRoot"><studip-icon shape="home" /></span>
<span v-if="parent" @click="selectChapter(parent.id)"> / {{ parent.name }}</span>
</div>
<div class="cw-dashboard-progress-chapter">
<h1><a :href="chapterUrl">{{ currentChapter.name }}</a></h1>
<div class="cw-dashboard-progress-chapter" v-if="selected">
<h1>
<a :href="chapterUrl">{{ selected.name }}</a>
</h1>
<courseware-progress-circle
:title="$gettext('diese Seite inkl. darunter liegende Seiten')"
:value="parseInt(currentChapter.progress.total)"
:value="parseInt(selected.progress.cumulative)"
/>
<courseware-progress-circle
:title="$gettext('diese Seite')"
class="cw-dashboard-progress-current"
:value="parseInt(currentChapter.progress.current)"
:value="parseInt(selected.progress.self)"
/>
</div>
<div class="cw-dashboard-progress-subchapter-list">
<courseware-dashboard-progress-item
v-for="chapter in currentChapter.children"
v-for="chapter in children"
:key="chapter.id"
:name="chapter.name"
:value="chapter.progress.total"
:value="chapter.progress.cumulative"
:chapterId="chapter.id"
@selectChapter="selectChapter"
/>
<div v-if="currentChapter.children.length === 0">
<div v-if="!children.length">
<translate>Dieses Seite enthält keine darunter liegenden Seiten</translate>
</div>
</div>
......@@ -48,44 +48,47 @@ export default {
},
data() {
return {
currentProgressData: 0,
selected: null,
};
},
computed: {
progressData() {
return STUDIP.courseware_progress_data;
},
currentChapter() {
return this.progressData[this.currentProgressData];
},
chapterUrl() {
return STUDIP.URLHelper.base_url + 'dispatch.php/course/courseware/?cid=' + STUDIP.URLHelper.parameters.cid + '#/structural_element/' + this.currentChapter.id;
return (
STUDIP.URLHelper.base_url +
'dispatch.php/course/courseware/?cid=' +
STUDIP.URLHelper.parameters.cid +
'#/structural_element/' +
this.selected.id
);
},
parent() {
if (!this.selected?.parent_id) {
return null;
}
return this.progressData[this.selected.parent_id];
},
methods: {
getRoot() {
this.progressData.every((element, index) => {
if (element.parent_id === null) {
this.currentProgressData = index;
return false;
} else {
return true;
children() {
if (!this.selected) {
return [];
}
});
return Object.values(this.progressData).filter(({ parent_id }) => parent_id === this.selected.id);
},
},
methods: {
visitRoot() {
this.selected = Object.values(this.progressData).find(({ parent_id }) => !!parent_id) ?? null;
},
selectChapter(id) {
this.progressData.every((element, index) => {
if (element.id === id) {
this.currentProgressData = index;
return false;
} else {
return true;
}
});
this.selected = this.progressData[id] ?? null;
},
},
mounted() {
this.getRoot();
this.visitRoot();
},
};
</script>
......@@ -20,7 +20,7 @@ export default {
props: {
name: String,
value: Number,
chapterId: String,
chapterId: Number,
},
};
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment