Skip to content

Commit db71448

Browse files
committed
Add course series verification and related API integration
- Implemented `verifyCourseSeriesExists` action in Vuex store to check if a series exists for a course. - Enhanced `mounted` lifecycle method in `Course.vue` to verify course series and notify users if a new series is created. (in async) - Updated `getSeries` method in `SeriesClient` to return 404 status if the series does not exist and check_existence flag is true. - Created `CourseSeriesVerification` REST Endpoint to handle series existence checks and creation logic.
1 parent 034a519 commit db71448

File tree

6 files changed

+147
-11
lines changed

6 files changed

+147
-11
lines changed

lib/Models/REST/SeriesClient.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,23 @@ public function __construct($config_id = 1)
2525
* Retrieve series metadata for a given series identifier from Opencast
2626
*
2727
* @param string series_id Identifier for a Series
28+
* @param bool check_existence a flag to check the existence of the series
2829
*
29-
* @return array|boolean response of a series, or false if unable to get
30+
* @return mix [array|boolean|int] response of a series, or false if unable to get or 404 if not found and $check_existence is true
3031
*/
31-
public function getSeries($series_id)
32+
public function getSeries($series_id, $check_existence = false)
3233
{
3334
$response = $this->opencastApi->series->get($series_id);
3435

3536
if ($response['code'] == 200) {
3637
return $response['body'];
3738
}
39+
40+
if ($check_existence && $response['code'] == 404) {
41+
// Series not found, so we return 404!
42+
return 404;
43+
}
44+
3845
return false;
3946
}
4047

lib/RouteMap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public function authenticatedRoutes(RouteCollectorProxy $group)
101101

102102
$group->get("/courses/{course_id}/config", Routes\Course\CourseConfig::class);
103103
$group->get("/courses/{course_id}/playlists", Routes\Course\CourseListPlaylist::class);
104+
$group->post("/courses/{course_id}/verifyCourseSeriesExists", Routes\Course\CourseSeriesVerification::class);
104105

105106
$group->get("/courses/{course_id}/{semester_filter}/schedule", Routes\Course\CourseListSchedule::class);
106107

lib/Routes/Course/CourseConfig.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ public function __invoke(Request $request, Response $response, $args)
3434

3535
$series = SeminarSeries::findOneBySeminar_id($course_id);
3636

37+
$config_id = $series->config_id ?? \Config::get()->OPENCAST_DEFAULT_SERVER;
38+
3739
if (empty($series)) {
3840
// only tutor or above should be able to trigger this series creation!
3941
$required_course_perm = \Config::get()->OPENCAST_TUTOR_EPISODE_PERM ? 'tutor' : 'dozent';
4042
if ($perm->have_studip_perm($required_course_perm, $course_id)) {
4143
// No series for this course yet! Create one!
42-
$config_id = \Config::get()->OPENCAST_DEFAULT_SERVER;
44+
4345
$series_client = new SeriesClient($config_id);
4446
$series_id = $series_client->createSeriesForSeminar($course_id);
4547

@@ -79,6 +81,8 @@ public function __invoke(Request $request, Response $response, $args)
7981
'scheduling_allowed' => Perm::schedulingAllowed($course_id),
8082
'course_hide_episodes' => $course_hide_episodes, // Use this in a course instead of OPENCAST_HIDE_EPISODES!
8183
'course_default_episodes_visibility' => $course_default_episodes_visibility,
84+
// To make is easier to recognize the server in the frontend
85+
'config_id' => $config_id,
8286
];
8387

8488
return $this->createResponse($results, $response);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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\Error;
8+
use Opencast\OpencastTrait;
9+
use Opencast\OpencastController;
10+
use Opencast\Models\SeminarSeries;
11+
use Opencast\Models\REST\SeriesClient;
12+
use Opencast\Providers\Perm;
13+
14+
15+
/**
16+
* Make sure that a series exists for the course. If not, create a new one!
17+
*/
18+
class CourseSeriesVerification extends OpencastController
19+
{
20+
use OpencastTrait;
21+
22+
public function __invoke(Request $request, Response $response, $args)
23+
{
24+
global $perm;
25+
26+
$course_id = $args['course_id'];
27+
$json = $this->getRequestData($request);
28+
29+
$series_id = $json['series_id'] ?? null;
30+
if (empty($series_id) || empty($course_id)) {
31+
throw new Error('Es fehlen Parameter!', 422);
32+
}
33+
34+
$config_id = $json['config_id'] ?? \Config::get()->OPENCAST_DEFAULT_SERVER;
35+
36+
// Check if user has proper right to perform this action.
37+
// [User must be enrolled in the seminar]
38+
// [User must have the right to upload in the seminar!]
39+
if (!$perm->have_studip_perm('user', $course_id) || !Perm::uploadAllowed($course_id)) {
40+
// throw new \AccessDeniedException();
41+
// If the user does not have the right to upload, we don't throw an error, but we return a message that this process cannot be performed!
42+
return $this->createResponse([
43+
'message' => [
44+
'type' => 'warning',
45+
'text' => _('Die Überprüfung der Serie ist nicht möglich!'),
46+
]
47+
], $response);
48+
}
49+
50+
// Default message, not necessarily needed, but it makes it easier to understand what is going on and makes the debugging and higher level information easier.
51+
$message = [
52+
'type' => 'success',
53+
'text' => _('Die Serie ist gültig.'),
54+
];
55+
try {
56+
// Set a flag to force re-creationg of the series in Opencast.
57+
$recreate_series_needed = false;
58+
$series = SeminarSeries::findBySeries_id($series_id);
59+
60+
if (empty($series)) {
61+
// If the series is not found, we need to create one.
62+
$recreate_series_needed = true;
63+
} else {
64+
// If the series is found, we need to check if it is still valid.
65+
$series_client = new SeriesClient($config_id);
66+
$series_data = $series_client->getSeries($series_id, true);
67+
if ($series_data == 404) {
68+
$recreate_series_needed = true;
69+
}
70+
}
71+
72+
if ($recreate_series_needed) {
73+
74+
$series_client = new SeriesClient($config_id);
75+
$series_id = $series_client->createSeriesForSeminar($course_id);
76+
77+
if ($series_id) {
78+
$series = SeminarSeries::create([
79+
'config_id' => $config_id,
80+
'seminar_id' => $course_id,
81+
'series_id' => $series_id,
82+
]);
83+
$series->store();
84+
}
85+
// The goal is to create a new one when it does not exist, so we return 201 Created.
86+
return $response->withStatus(201);
87+
}
88+
} catch (\Throwable $th) {
89+
// If something goes wrong, we catch the error and return the message but in 200 code so that it does not break the flow of the application.
90+
$message = [
91+
'type' => 'error',
92+
'text' => _('Die Überprüfung der Serie ist fehlergeschlagen') . ': ' . $th->getMessage(),
93+
];
94+
}
95+
96+
return $this->createResponse([
97+
'message' => $message,
98+
], $response);
99+
}
100+
}

vueapp/store/config.module.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ export const actions = {
7575
});
7676
},
7777

78+
async verifyCourseSeriesExists(context, params) {
79+
return ApiService.post('courses/' + params.cid + '/verifyCourseSeriesExists',
80+
{
81+
config_id: params.config_id,
82+
series_id: params.series_id
83+
}
84+
);
85+
},
86+
7887
async configListUpdate(context, params) {
7988
return ApiService.put('global_config', params);
8089
},

vueapp/views/Course.vue

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,32 @@ export default {
148148
}
149149
},
150150
151-
mounted() {
152-
this.$store.dispatch('loadCurrentUser');
153-
this.$store.dispatch('loadCourseConfig', this.cid)
154-
.then((course_config) => {
155-
if (!course_config?.series?.series_id) {
151+
async mounted() {
152+
await this.$store.dispatch('loadCurrentUser');
153+
const course_config = await this.$store.dispatch('loadCourseConfig', this.cid);
154+
155+
if (!course_config?.series?.series_id) {
156+
this.$store.dispatch('addMessage', {
157+
type: 'warning',
158+
text: this.$gettext('Die Kurskonfiguration konnte nicht vollständig abgerufen werden, daher ist das Hochladen von Videos momentan nicht möglich.')
159+
});
160+
} else {
161+
// So here we do a verification of the course series.
162+
let params = {
163+
cid: this.cid,
164+
series_id: course_config.series.series_id,
165+
config_id: course_config.config_id
166+
};
167+
const series_verification_response = await this.$store.dispatch('verifyCourseSeriesExists', params);
168+
169+
// By the new series creation, which happens only when the response status is 201 (Created), we inform the user about it!
170+
if (series_verification_response?.status === 201) {
156171
this.$store.dispatch('addMessage', {
157-
type: 'warning',
158-
text: this.$gettext('Die Kurskonfiguration konnte nicht vollständig abgerufen werden, daher ist das Hochladen von Videos momentan nicht möglich.')
172+
type: 'info',
173+
text: this.$gettext('Eine neue Serie wurde für diesen Kurs erstellt.'),
159174
});
160175
}
161-
});
176+
}
162177
}
163178
};
164179
</script>

0 commit comments

Comments
 (0)