aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2022-01-19 10:25:54 +0000
committerMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2022-01-19 10:25:54 +0000
commitc706fd9205bb45fd3c902b271c626e68a5fe8a10 (patch)
treed7f6647fe667623df6e64d4dbac74104fcc818f4
parentd62f1b7480c51a290c168c79a6f8ea05757a6e77 (diff)
Coursewares Übersichtsseite beschleunigen
-rwxr-xr-xapp/controllers/course/courseware.php237
-rw-r--r--lib/models/Course.class.php1
-rwxr-xr-xlib/models/Courseware/Instance.php60
-rwxr-xr-xresources/assets/stylesheets/scss/courseware.scss37
-rwxr-xr-xresources/vue/components/courseware/CoursewareDashboardProgress.vue71
-rwxr-xr-xresources/vue/components/courseware/CoursewareDashboardProgressItem.vue2
6 files changed, 234 insertions, 174 deletions
diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php
index b22ffb7..c4aae64 100755
--- a/app/controllers/course/courseware.php
+++ b/app/controllers/course/courseware.php
@@ -60,9 +60,9 @@ class Course_CoursewareController extends AuthenticatedController
Navigation::activateItem('course/courseware/content');
$this->setIndexSidebar();
- $this->licenses = array();
- $sorm_licenses = License::findBySQL("1 ORDER BY name ASC");
- foreach($sorm_licenses as $license) {
+ $this->licenses = [];
+ $sorm_licenses = License::findBySQL('1 ORDER BY name ASC');
+ foreach ($sorm_licenses as $license) {
array_push($this->licenses, $license->toArray());
}
$this->licenses = json_encode($this->licenses);
@@ -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 $data;
+ return $childrenOf;
}
- private function getChildren($children): array
- {
- $data = [];
- foreach ($children as $child) {
- $el = [
- 'id' => $child->id,
- 'name' => $child->title,
+ 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,
];
- array_push($data, $el);
+ }
+
+ return $progress;
+ }
+
+ private function getSelfProgresses(
+ array $allBlocks,
+ string $elementId,
+ array $userProgresses,
+ array $courseMemberIds
+ ): array {
+ $blks = $allBlocks[$elementId] ?: [];
+
+ $data = [
+ 'counter' => count($blks),
+ 'progress' => 0,
+ ];
+ $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);
+
+ // compute `cumulative` of each element
+ $visitor = function (&$progress, $element) use (&$childrenOf, &$elements, &$visitor) {
+ $elementId = $element->getId();
+ $numberOfNodes = 0;
+ $cumulative = 0;
+
+ // visit children first
+ if (isset($childrenOf[$elementId])) {
+ foreach ($childrenOf[$elementId] as $childId) {
+ $visitor($progress, $elements[$childId]);
+ $numberOfNodes += $progress[$childId]['numberOfNodes'];
+ $cumulative += $progress[$childId]['cumulative'];
+ }
}
- }
- $own_blocks = $this->getBlocks($element->id, $course_progress, $cw_user_progresses, $course, $course_member_ids);
+ $progress[$elementId]['cumulative'] = $cumulative + $progress[$elementId]['self'];
+ $progress[$elementId]['numberOfNodes'] = $numberOfNodes + 1;
- if ($own_blocks['counter'] > 0) {
- $own_progress = $own_blocks['progress'] / $own_blocks['counter'];
- } else {
- $own_progress = 1;
- }
+ return $progress;
+ };
- $count = count($descendants);
- if ($count > 0) {
- $progress = ($progress + $own_progress) / ($count + 1);
- } else {
- $progress = $own_progress;
- }
+ $visitor($progress, $instance->getRoot());
- return ['total' => round($progress, 2) * 100, 'current' => round($own_progress, 2) * 100];
+ return $progress;
}
- private function getBlocks(string $element_id, bool $course_progress = false, array $cw_user_progresses, Course $course, array $course_member_ids): array
+ private function prepareProgressData(iterable $elements, iterable $progress): iterable
{
- $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;
- }
- });
-
- $users_progress = 0;
- foreach ($progresses as $prog) {
- if (in_array($prog->user_id, $course_member_ids)) {
- $users_progress += $prog->grade;
- }
- }
-
- $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);
- }
- }
- }
- }
+ $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;
}
}
diff --git a/lib/models/Course.class.php b/lib/models/Course.class.php
index f543c88..b078c0c 100644
--- a/lib/models/Course.class.php
+++ b/lib/models/Course.class.php
@@ -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
diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php
index 49b7cae..cc9189c 100755
--- a/lib/models/Courseware/Instance.php
+++ b/lib/models/Courseware/Instance.php
@@ -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;
+ }
}
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index 08016e8..aa31c43 100755
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -104,12 +104,12 @@ c o n t e n t s
background-position-x: 50%;
padding: 24px;
margin-bottom: 10px;
-
+
.cw-contents-overview-teaser-content {
padding-top: 28%;
padding-left: 0;
text-align: justify;
-
+
header{
font-size: 1.5em;
margin: 1em 0 0.5em 0;
@@ -434,10 +434,10 @@ $consum_ribbon_width: calc(100% - 58px);
}
.responsive-display {
- .cw-ribbon-sticky-top,
+ .cw-ribbon-sticky-top,
.cw-ribbon-sticky-bottom,
.cw-ribbon-wrapper-consume,
- .cw-ribbon-consume-bottom {
+ .cw-ribbon-consume-bottom {
width: 100%;
}
.cw-ribbon {
@@ -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;
}
}
@@ -2587,12 +2590,12 @@ a u d i o b l o c k
padding-left: 0;
list-style: none;
cursor: pointer;
-
+
.cw-playlist-item {
@include background-icon(file-audio2, clickable, 24);
background-repeat: no-repeat;
background-position: 1em center;
-
+
margin: 1em;
padding: 1em;
padding-left: 4em;
@@ -3637,7 +3640,7 @@ dialog cards block
right: 0;
}
- &.cw-dialogcards-prev-disabled,
+ &.cw-dialogcards-prev-disabled,
&.cw-dialogcards-next-disabled {
background-color: $light-gray-color-40;
}
@@ -3849,7 +3852,7 @@ headline block
.cw-block-headline {
.cw-block-headline-content {
min-height: 300px;
-
+
&.half {
min-height: 150px;
}
@@ -3865,7 +3868,7 @@ headline block
.icon-layer {
background-position: center calc(50% - 4em);
min-height: 300px;
-
+
&.half {
min-height: 150px;
}
@@ -3890,14 +3893,14 @@ headline block
@include background-icon($icon, status-green, 98);
}
};
-
+
&.half {
background-size: 72px;
background-position: center calc(50% - 2em);
}
}
-
-
+
+
.cw-block-headline-textbox {
.cw-block-headline-title {
h1 {
@@ -3905,7 +3908,7 @@ headline block
font-size: 2em;
}
}
-
+
.cw-block-headline-subtitle {
h2 {
font-size: 12px;
@@ -3942,7 +3945,7 @@ headline block
}
};
}
-
+
.cw-block-headline-textbox {
.cw-block-headline-title {
h1 {
@@ -3952,7 +3955,7 @@ headline block
}
}
-
+
&.ribbon {
.icon-layer {
min-height: 300px;
@@ -3960,14 +3963,14 @@ headline block
&.half {
min-height: 150px;
}
-
+
.cw-block-headline-textbox {
.cw-block-headline-title {
h1 {
font-size: 2.5em;
}
}
-
+
.cw-block-headline-subtitle {
h2 {
font-size: 12px;
diff --git a/resources/vue/components/courseware/CoursewareDashboardProgress.vue b/resources/vue/components/courseware/CoursewareDashboardProgress.vue
index 6f2f4f8..e011c88 100755
--- a/resources/vue/components/courseware/CoursewareDashboardProgress.vue
+++ b/resources/vue/components/courseware/CoursewareDashboardProgress.vue
@@ -1,33 +1,33 @@
<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];
+ },
+ children() {
+ if (!this.selected) {
+ return [];
+ }
+
+ return Object.values(this.progressData).filter(({ parent_id }) => parent_id === this.selected.id);
},
},
methods: {
- getRoot() {
- this.progressData.every((element, index) => {
- if (element.parent_id === null) {
- this.currentProgressData = index;
- return false;
- } else {
- return true;
- }
- });
+ 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>
diff --git a/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue b/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue
index 1d34020..adc26b7 100755
--- a/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue
+++ b/resources/vue/components/courseware/CoursewareDashboardProgressItem.vue
@@ -20,7 +20,7 @@ export default {
props: {
name: String,
value: Number,
- chapterId: String,
+ chapterId: Number,
},
};
</script>