Skip to content

Commit 393db00

Browse files
authored
Course Default Visibility feature, (elan-ev#1324)
This PR fixes elan-ev#1316. It introduces a new course config via migration 109 called `OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY`. Additionally, a new sidebar action is added which opens a dialog to change the default visibility of episodes in a course. It would apply to the episodes that have "default" as for their visibility! It works in a way, that it could be set to follow the default "global" config `OPENCAST_HIDE_EPISODES`, visible or hidden!
1 parent 747f92f commit 393db00

File tree

11 files changed

+284
-23
lines changed

11 files changed

+284
-23
lines changed

lib/Models/Videos.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static function getPlaylistVideos($playlist_id, $filters)
109109
$sql .= ' INNER JOIN oc_playlist_seminar AS ops ON (ops.seminar_id = :cid AND ops.playlist_id = opv.playlist_id)'.
110110
' LEFT JOIN oc_playlist_seminar_video AS opsv ON (opsv.playlist_seminar_id = ops.id AND opsv.video_id = opv.video_id)';
111111

112-
$where = ' WHERE '. self::getVisibilitySql();
112+
$where = ' WHERE '. self::getVisibilitySql($cid);
113113

114114
$params[':cid'] = $cid;
115115
}
@@ -150,7 +150,7 @@ public static function getCourseVideos($course_id, $filters)
150150
if (!$perm->have_studip_perm($required_course_perm, $course_id)) {
151151
$sql .= ' LEFT JOIN oc_playlist_seminar_video AS opsv ON (opsv.playlist_seminar_id = ops.id AND opsv.video_id = opv.video_id)';
152152

153-
$where = ' WHERE '. self::getVisibilitySql();
153+
$where = ' WHERE '. self::getVisibilitySql($course_id);
154154
}
155155

156156
$query = [
@@ -162,10 +162,15 @@ public static function getCourseVideos($course_id, $filters)
162162
return self::getFilteredVideos($query, $filters);
163163
}
164164

165-
private static function getVisibilitySql()
165+
private static function getVisibilitySql($course_id)
166166
{
167167
// if each video has to explicitly set to visible, filter out everything else
168-
if (\Config::get()->OPENCAST_HIDE_EPISODES) {
168+
$course_hide_episodes = \Config::get()->OPENCAST_HIDE_EPISODES;
169+
$course_default_episodes_visibility = \CourseConfig::get($course_id)->OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY ?? 'default';
170+
if ($course_default_episodes_visibility !== 'default') {
171+
$course_hide_episodes = $course_default_episodes_visibility === 'hidden' ? true : false;
172+
}
173+
if ($course_hide_episodes) {
169174
return '(
170175
(opsv.visibility = "visible" AND opsv.visible_timestamp IS NULL)
171176
OR (opsv.visible_timestamp < NOW())
@@ -494,7 +499,7 @@ public static function getNumberOfNewCourseVideos($course_id, $last_visit, $user
494499
if (!$perm->have_perm('dozent', $user_id)) {
495500
$sql .= ' LEFT JOIN oc_playlist_seminar_video AS opsv ON (opsv.playlist_seminar_id = ops.id AND opsv.video_id = opv.video_id)';
496501

497-
$where .= ' AND '. self::getVisibilitySql();
502+
$where .= ' AND '. self::getVisibilitySql($course_id);
498503
}
499504

500505
$sql .= $where;

lib/RouteMap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ public function authenticatedRoutes(RouteCollectorProxy $group)
108108
$group->delete("/courses/{course_id}/playlist/{token}", Routes\Course\CourseRemovePlaylist::class);
109109

110110
$group->put("/courses/{course_id}/upload/{upload}", Routes\Course\CourseSetUpload::class); // TODO: document in api docs
111+
// TODO: document in api docs?
112+
$group->put("/courses/{course_id}/episodes_visibility", Routes\Course\CourseSetDefaultVideosVisibility::class);
111113

112114
$group->get("/courses/videos", Routes\Course\CourseListForUserVideos::class);
113115
$group->get("/courses/videos/playlist/{token}", Routes\Course\CourseListForPlaylistVideos::class);

lib/Routes/Course/CourseConfig.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,31 @@ public function __invoke(Request $request, Response $response, $args)
5353
}
5454
}
5555

56+
// Default Course Episodes Visibility.
57+
// The course specific config option OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY has 3 possible values:
58+
// - default: use the default value from the config (OPENCAST_HIDE_EPISODES) at the time!
59+
// - visible: show the episodes to students by default
60+
// - hidden: hide the episodes from students by default
61+
// Getting with the default value from the config (OPENCAST_HIDE_EPISODES).
62+
$course_hide_episodes = \Config::get()->OPENCAST_HIDE_EPISODES;
63+
$course_default_episodes_visibility = \CourseConfig::get($course_id)->OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY
64+
?? 'default';
65+
if ($course_default_episodes_visibility !== 'default') {
66+
$course_hide_episodes = $course_default_episodes_visibility === 'hidden' ? true : false;
67+
}
68+
5669
$results = [
5770
'series' => [
5871
'series_id' => $series->series_id,
5972
],
60-
'workflow' => SeminarWorkflowConfiguration::getWorkflowForCourse($course_id),
61-
'edit_allowed' => Perm::editAllowed($course_id),
62-
'upload_allowed' => Perm::uploadAllowed($course_id),
63-
'upload_enabled' => \CourseConfig::get($course_id)->OPENCAST_ALLOW_STUDENT_UPLOAD ? 1 : 0,
64-
'has_default_playlist' => Helpers::checkCourseDefaultPlaylist($course_id),
65-
'scheduling_allowed' => Perm::schedulingAllowed($course_id)
73+
'workflow' => SeminarWorkflowConfiguration::getWorkflowForCourse($course_id),
74+
'edit_allowed' => Perm::editAllowed($course_id),
75+
'upload_allowed' => Perm::uploadAllowed($course_id),
76+
'upload_enabled' => \CourseConfig::get($course_id)->OPENCAST_ALLOW_STUDENT_UPLOAD ? 1 : 0,
77+
'has_default_playlist' => Helpers::checkCourseDefaultPlaylist($course_id),
78+
'scheduling_allowed' => Perm::schedulingAllowed($course_id),
79+
'course_hide_episodes' => $course_hide_episodes, // Use this in a course instead of OPENCAST_HIDE_EPISODES!
80+
'course_default_episodes_visibility' => $course_default_episodes_visibility,
6681
];
6782

6883
return $this->createResponse($results, $response);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Opencast\Routes\Course;
4+
5+
use Psr\Http\Message\ResponseInterface as Response;
6+
use Psr\Http\Message\ServerRequestInterface as Request;
7+
use Opencast\Errors\AuthorizationFailedException;
8+
use Opencast\Errors\Error;
9+
use Opencast\OpencastTrait;
10+
use Opencast\OpencastController;
11+
use Opencast\Models\SeminarSeries;
12+
13+
/**
14+
* Set the default visibility of videos in a course using the course config
15+
*/
16+
class CourseSetDefaultVideosVisibility extends OpencastController
17+
{
18+
use OpencastTrait;
19+
20+
public function __invoke(Request $request, Response $response, $args)
21+
{
22+
global $user, $perm;
23+
24+
$course_id = $args['course_id'];
25+
26+
$json = $this->getRequestData($request);
27+
$visibility_option = $json['visibility_option'];
28+
$possible_visibility_options = ['default', 'visible', 'hidden'];
29+
30+
if (!in_array($visibility_option, $possible_visibility_options)) {
31+
throw new Error('Ungültige Sichtbarkeit!', 422);
32+
}
33+
34+
if (empty($course_id) || empty($visibility_option)) {
35+
throw new Error('Es fehlen Parameter!', 422);
36+
}
37+
38+
if (!$perm->have_studip_perm('tutor', $course_id)) {
39+
throw new \AccessDeniedException();
40+
}
41+
42+
$response_code = 204;
43+
$message = [
44+
'type' => 'success',
45+
'text' => _('Die Standardsichtbarkeit der Videos wurde aktualisiert.')
46+
];
47+
try {
48+
\CourseConfig::get($course_id)->store(
49+
'OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY',
50+
$visibility_option
51+
);
52+
} catch (\Throwable $e) {
53+
$response_code = 500;
54+
$message = [
55+
'type' => 'error',
56+
'text' => _('Beim Aktualisieren der Standardsichtbarkeit ist ein Fehler aufgetreten.')
57+
];
58+
}
59+
60+
return $this->createResponse([
61+
'message' => $message,
62+
], $response->withStatus($response_code));
63+
}
64+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
class AddCourseConfigDefaultVisibility extends Migration
3+
{
4+
5+
6+
public function description()
7+
{
8+
return 'Create new course config for default visibility of the episodes';
9+
}
10+
11+
public function up()
12+
{
13+
Config::get()->create('OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY', [
14+
'value' => 'default',
15+
'type' => 'string',
16+
'range' => 'course',
17+
'section' => 'opencast',
18+
'description' => 'Legt den Sichtbarkeitsstatus für Episoden fest, die den Standardwerten im Kurs folgen.'
19+
]);
20+
}
21+
22+
public function down()
23+
{
24+
Config::get()->delete('OPENCAST_COURSE_DEFAULT_EPISODES_VISIBILITY');
25+
}
26+
}

vueapp/components/Courses/CoursesSidebar.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@
217217
{{ $gettext('Standard-Kurswiedergabeliste ändern') }}
218218
</a>
219219
</li>
220+
<li v-if="canEdit">
221+
<a href="#" @click.prevent="$emit('changeDefaultVisibility')">
222+
<studip-icon style="margin-left: -20px;" :shape="changeDefaultVisibilityIcon" role="clickable"/>
223+
{{ $gettext('Standardsichtbarkeit Videos') }}
224+
</a>
225+
</li>
220226
<li v-if="canEdit">
221227
<a href="#" @click.prevent="$emit('copyAll')">
222228
<studip-icon style="margin-left: -20px;" shape="export" role="clickable"/>
@@ -254,7 +260,8 @@ export default {
254260
'sortVideo',
255261
'saveSortVideo',
256262
'cancelSortVideo',
257-
'changeDefaultPlaylist'
263+
'changeDefaultPlaylist',
264+
'changeDefaultVisibility'
258265
],
259266
260267
data() {
@@ -362,6 +369,16 @@ export default {
362369
hasDefaultPlaylist() {
363370
return this.course_config?.has_default_playlist;
364371
},
372+
373+
changeDefaultVisibilityIcon() {
374+
let course_hide_episodes = false;
375+
if (this.course_config.hasOwnProperty('course_hide_episodes')) {
376+
course_hide_episodes = this.course_config.course_hide_episodes;
377+
} else if (this.simple_config_list?.settings && this.simple_config_list.settings.hasOwnProperty('OPENCAST_HIDE_EPISODES')) {
378+
course_hide_episodes = this.simple_config_list.settings.OPENCAST_HIDE_EPISODES;
379+
}
380+
return course_hide_episodes ? 'visibility-invisible' : 'visibility-visible';
381+
}
365382
},
366383
367384
methods: {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<template>
2+
<div>
3+
<StudipDialog
4+
:title="$gettext('Standardsichtbarkeit Videos in dieser Veranstaltung')"
5+
:confirmText="$gettext('Akzeptieren')"
6+
:closeText="$gettext('Schließen')"
7+
:closeClass="'cancel'"
8+
height="260"
9+
width="530"
10+
@close="$emit('cancel')"
11+
@confirm="setCourseEpisodesVisibility"
12+
>
13+
<template v-slot:dialogContent>
14+
<form class="default" @submit="setCourseEpisodesVisibility">
15+
<label>
16+
<input type="radio" name="visibility" value="default"
17+
:checked="currentVisibilityOption == 'default'"
18+
v-model="visibilityOption"
19+
>
20+
{{ $gettext('Systemstandard') + ' (' + $gettext(standardVisibilityText) + ')' }}
21+
</label>
22+
23+
<label>
24+
<input type="radio" name="visibility" value="visible"
25+
:checked="currentVisibilityOption == 'visible'"
26+
v-model="visibilityOption"
27+
>
28+
{{ $gettext('Videos standardmäßig sichtbar für Studierende') }}
29+
</label>
30+
31+
<label>
32+
<input type="radio" name="visibility" value="hidden"
33+
:checked="currentVisibilityOption == 'hidden'"
34+
v-model="visibilityOption"
35+
>
36+
{{ $gettext('Videos standardmäßig unsichtbar für Studierende') }}
37+
</label>
38+
</form>
39+
</template>
40+
</StudipDialog>
41+
</div>
42+
</template>
43+
44+
<script>
45+
import { ref, computed, onBeforeMount } from 'vue'
46+
import { useStore } from "vuex";
47+
import { useGettext } from 'vue3-gettext';
48+
49+
import StudipDialog from "@/components/Studip/StudipDialog.vue";
50+
51+
export default {
52+
name: "EpisodesDefaultVisibilityDialog",
53+
components: {
54+
StudipDialog,
55+
},
56+
emits: ['done', 'cancel'],
57+
58+
setup(props, { emit }) {
59+
const { $gettext } = useGettext();
60+
const store = useStore();
61+
62+
const visibilityOption = ref();
63+
const cid = computed(() => store.getters.cid);
64+
const standardVisibilityText = computed(() => {
65+
return store.getters.simple_config_list?.settings?.OPENCAST_HIDE_EPISODES ?
66+
$gettext('Videos standardmäßig unsichtbar für Studierende') : $gettext('Videos standardmäßig sichtbar für Studierende');
67+
});
68+
const currentVisibilityOption = computed(() => {
69+
return store.getters.course_config?.course_default_episodes_visibility ?? 'default';
70+
});
71+
72+
const setCourseEpisodesVisibility = () => {
73+
store.dispatch('setCourseEpisodesVisibility', {
74+
cid: store.getters.cid,
75+
visibility_option: visibilityOption.value
76+
}).then((data) => {
77+
store.dispatch('loadCourseConfig', store.getters.cid);
78+
store.dispatch('loadCourseConfig', store.getters.cid);
79+
emit('done');
80+
});
81+
}
82+
83+
onBeforeMount(() => {
84+
// Make sure that the visibilityOption gets its inital value from computed variable!
85+
visibilityOption.value = currentVisibilityOption.value;
86+
})
87+
88+
return {
89+
setCourseEpisodesVisibility,
90+
currentVisibilityOption,
91+
visibilityOption,
92+
cid,
93+
standardVisibilityText,
94+
}
95+
}
96+
}
97+
</script>

vueapp/components/Videos/VideoRow.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,11 @@ export default {
357357
}
358358
359359
if (event.seminar_visibility === null || event.seminar_visibility === undefined) {
360-
if (this.simple_config_list.settings.OPENCAST_HIDE_EPISODES) {
361-
return false;
362-
} else {
363-
return true;
360+
if (this.course_config && this.course_config.hasOwnProperty('course_hide_episodes')) {
361+
return this.course_config.course_hide_episodes ? false : true;
364362
}
363+
// Fallback to global setting, when the course_hide_episodes is not set!
364+
return this.simple_config_list.settings.OPENCAST_HIDE_EPISODES ? false : true;
365365
}
366366
367367
if (event.seminar_visibility?.visibility == 'visible') {
@@ -379,7 +379,8 @@ export default {
379379
'downloadSetting',
380380
'videoSortMode',
381381
'currentUser',
382-
'simple_config_list'
382+
'simple_config_list',
383+
'course_config'
383384
]),
384385
385386
showCheckbox() {

vueapp/store/opencast.module.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,11 @@ const actions = {
213213
user: null
214214
});
215215
}
216-
}
216+
},
217+
218+
setCourseEpisodesVisibility({ context }, data) {
219+
return ApiService.put('courses/' + data.cid + '/episodes_visibility', {visibility_option: data.visibility_option})
220+
},
217221
}
218222

219223
const mutations = {

0 commit comments

Comments
 (0)