Skip to content

Commit 1ee6f7d

Browse files
ferishilitgloeggl
andauthored
Add course series verification and related API integration [Course Series Auto re-creation] (#1382)
* 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. * remove unwanted code * changes according to discussion: - no perm check - using a new OC_WARNING log * correct migration number * Update 113_add_new_log_action_oc_warning.php --------- Co-authored-by: Till <[email protected]>
1 parent 864762e commit 1ee6f7d

File tree

8 files changed

+183
-14
lines changed

8 files changed

+183
-14
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/Models/SeminarSeries.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,60 @@ public static function getSeries($course_id, $offline = false)
7171
return $return;
7272
}
7373

74+
/**
75+
* Ensures that a course series exists for the given course and configuration.
76+
*
77+
* If the series does not exist in the database or is not found in Opencast,
78+
* this method will attempt to re-create a new series for the seminar and store it.
79+
*
80+
* @param int $config_id The Opencast server configuration ID.
81+
* @param string $course_id The Stud.IP course ID.
82+
* @param string $series_id The Opencast series ID to check or create.
83+
* @return bool True if a new series was created, false otherwise.
84+
*/
85+
public static function ensureSeriesExists($config_id, $course_id, $series_id)
86+
{
87+
// Set a flag to force re-creationg of the series in Opencast.
88+
$recreate_series_needed = false;
89+
$series_client = new SeriesClient($config_id);
90+
$seminar_series = self::findOneBySQL('seminar_id = ? AND series_id = ?', [$course_id, $series_id]);
91+
92+
if (empty($seminar_series)) {
93+
// If the series is not found, we need to create one.
94+
$recreate_series_needed = true;
95+
} else {
96+
// If the series is found, we need to check if it is still valid.
97+
$series_data = $series_client->getSeries($series_id, true);
98+
if ($series_data == 404) {
99+
$recreate_series_needed = true;
100+
}
101+
}
102+
103+
if ($recreate_series_needed) {
104+
105+
$new_series_id = $series_client->createSeriesForSeminar($course_id);
106+
107+
if ($new_series_id) {
108+
// Make sure the old/lost series record is removed from Stud.IP as well.
109+
if (!empty($seminar_series)) {
110+
$seminar_series->delete();
111+
}
112+
$new_seminar_series = SeminarSeries::create([
113+
'config_id' => $config_id,
114+
'seminar_id' => $course_id,
115+
'series_id' => $new_series_id,
116+
]);
117+
$new_seminar_series->store();
118+
119+
$log_info = "Die Kurs-Serie (ID: {$series_id}) ist verloren gegangen und eine neue wurde erstellt (ID: {$new_series_id}).";
120+
\StudipLog::log('OC_WARNINGS', $course_id, null, $log_info);
121+
return true;
122+
}
123+
}
124+
125+
return false;
126+
}
127+
74128
/**
75129
* Invalidate the cache of recorded series IDs before storing a series.
76130
* Then proceeds with the parent store method.

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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
$message = [
37+
'type' => 'success',
38+
'text' => _('Die Serie ist gültig.'),
39+
];
40+
try {
41+
$series_has_been_recreated = SeminarSeries::ensureSeriesExists($config_id, $course_id, $series_id);
42+
if ($series_has_been_recreated) {
43+
// The goal is to create a new one when it does not exist, so we return 201 Created.
44+
return $response->withStatus(201);
45+
}
46+
} catch (\Throwable $th) {
47+
// 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.
48+
$message = [
49+
'type' => 'error',
50+
'text' => _('Die Überprüfung der Serie ist fehlergeschlagen') . ': ' . $th->getMessage(),
51+
];
52+
}
53+
54+
// In any case, we are obliged to return something in favor of sticking to API Response.
55+
return $this->createResponse([
56+
'message' => $message,
57+
], $response);
58+
}
59+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
class AddNewLogActionOcWarning extends Migration
4+
{
5+
const OC_WARNING_LOG_ACTON_NAME = 'OC_WARNINGS';
6+
const PLUGINCLASSNAME = 'OpencastV3';
7+
public function description()
8+
{
9+
return 'Registering new log action "OC_WARNING" for Opencast Plugin, in order to log the warnings throughout the app.';
10+
}
11+
12+
public function up()
13+
{
14+
$description = 'Opencast: Warnungen / Meldungen';
15+
$info_template = '[Opencast Warnung]: %info - (Wo: %affected, Wer: %user)';
16+
StudipLog::registerActionPlugin(
17+
self::OC_WARNING_LOG_ACTON_NAME,
18+
$description,
19+
$info_template,
20+
self::PLUGINCLASSNAME
21+
);
22+
}
23+
24+
public function down()
25+
{
26+
StudipLog::unregisterAction(self::OC_WARNING_LOG_ACTON_NAME);
27+
}
28+
}

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: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,24 @@ 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) {
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-
}
161-
});
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+
await this.$store.dispatch('verifyCourseSeriesExists', params);
168+
}
162169
}
163170
};
164171
</script>

0 commit comments

Comments
 (0)