From 2ecc0a49300bdac9f65be7f6e19dc842b7e039ce Mon Sep 17 00:00:00 2001 From: FarbodZamani <53179227+ferishili@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:37:02 +0100 Subject: [PATCH] Workflow configuration panel during upload (#390) * introducing upload workflow config panel in upload page * automated tests * version bump * fixing behat test for autocomplete feature * fix mismatched types * parameters list completion * new method to get workflow isntance * param list docs completion * adjust the sequece where to init an apibridge instance to cover wfconfig helper class as well * using the local opencast for the unit test * temporarily using oc-16-support branch from tool_opencast plugin * remove api version to fix php unit test --------- Co-authored-by: Thomas Niedermaier --- .github/workflows/moodle-ci.yml | 2 +- addvideo.php | 11 +- batchupload.php | 11 +- classes/local/addvideo_form.php | 17 ++ classes/local/apibridge.php | 31 ++- classes/local/batchupload_form.php | 17 ++ classes/local/ingest_uploader.php | 8 +- classes/local/upload_helper.php | 11 +- .../local/workflowconfiguration_helper.php | 226 ++++++++++++++++++ db/install.xml | 4 +- db/upgrade.php | 24 ++ lang/en/block_opencast.php | 6 + renderer.php | 214 ++++++++++++++++- settings.php | 16 ++ tests/behat/block_opencast_addvideo.feature | 28 ++- .../block_opencast_addvideosbatch.feature | 30 ++- ...ock_opencast_autocomplete_metadata.feature | 30 ++- tests/upload_ingest_with_configpanel_test.php | 198 +++++++++++++++ 18 files changed, 837 insertions(+), 47 deletions(-) create mode 100644 classes/local/workflowconfiguration_helper.php create mode 100644 tests/upload_ingest_with_configpanel_test.php diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 9e224a50..44a13691 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -12,5 +12,5 @@ jobs: with: requires-tool-plugin: true requires-mod-plugin: true - branch-tool-plugin: master + branch-tool-plugin: oc-16-support branch-mod-plugin: master diff --git a/addvideo.php b/addvideo.php index 7b307c65..bc826ec1 100644 --- a/addvideo.php +++ b/addvideo.php @@ -28,6 +28,8 @@ use block_opencast\local\upload_helper; use core\output\notification; use tool_opencast\local\settings_api; +use block_opencast\local\workflowconfiguration_helper; + require_once('../../config.php'); global $PAGE, $OUTPUT, $CFG, $USER, $SITE, $DB; @@ -268,8 +270,15 @@ json_encode($data->scheduledvisibilitygroups) : null; } + // Prepare user defined workflow configurations if enabled and exist. + $workflowconfiguration = null; + $wfconfighelper = workflowconfiguration_helper::get_instance($ocinstanceid); + if ($configpaneldata = $wfconfighelper->get_userdefined_configuration_data($data)) { + $workflowconfiguration = json_encode($configpaneldata); + } + // Update all upload jobs. - upload_helper::save_upload_jobs($ocinstanceid, $courseid, $options, $visibility); + upload_helper::save_upload_jobs($ocinstanceid, $courseid, $options, $visibility, $workflowconfiguration); redirect($redirecturl, get_string('uploadjobssaved', 'block_opencast'), null, notification::NOTIFY_SUCCESS); } diff --git a/batchupload.php b/batchupload.php index c19fe58c..568cfef7 100644 --- a/batchupload.php +++ b/batchupload.php @@ -29,6 +29,8 @@ use block_opencast\local\upload_helper; use core\output\notification; use tool_opencast\local\settings_api; +use block_opencast\local\workflowconfiguration_helper; + require_once('../../config.php'); global $PAGE, $OUTPUT, $CFG, $USER, $SITE, $DB; @@ -243,6 +245,13 @@ json_encode($data->scheduledvisibilitygroups) : null; } + // Prepare user defined workflow configurations if enabled and exist. + $workflowconfiguration = null; + $wfconfighelper = workflowconfiguration_helper::get_instance($ocinstanceid); + if ($configpaneldata = $wfconfighelper->get_userdefined_configuration_data($data)) { + $workflowconfiguration = json_encode($configpaneldata); + } + $error = null; $totalfiles = count($batchuploadedfiles); // Loop through the files and proceed with the upload and cleanup records. @@ -284,7 +293,7 @@ $options->presenter = $newfile->get_itemid(); // Save the upload job. - upload_helper::save_upload_jobs($ocinstanceid, $courseid, $options, $visibility); + upload_helper::save_upload_jobs($ocinstanceid, $courseid, $options, $visibility, $workflowconfiguration); } catch (moodle_exception $e) { $errorcount++; } diff --git a/classes/local/addvideo_form.php b/classes/local/addvideo_form.php index aeaf85c3..1137d82d 100644 --- a/classes/local/addvideo_form.php +++ b/classes/local/addvideo_form.php @@ -61,6 +61,7 @@ public function definition() { $ocinstanceid = $this->_customdata['ocinstanceid']; $apibridge = apibridge::get_instance($ocinstanceid); $eventdefaults = $this->_customdata['eventdefaults']; + $wfconfighelper = workflowconfiguration_helper::get_instance($ocinstanceid); $usechunkupload = class_exists('\local_chunkupload\chunkupload_form_element') && get_config('block_opencast', 'enablechunkupload_' . $ocinstanceid); @@ -309,6 +310,22 @@ public function definition() { } } + // Offering workflow configuration panel settings. + if ($wfconfighelper->can_provide_configuration_panel()) { + $mform->closeHeaderBefore('configurationpanel_header'); + $mform->addElement('header', 'configurationpanel_header', get_string('configurationpanel_header', 'block_opencast')); + $mform->setExpanded('configurationpanel_header', true); + + $configpanelexplanation = html_writer::tag('p', get_string('configurationpanelheader_explanation', 'block_opencast')); + $mform->addElement('html', $configpanelexplanation); + + $renderer->render_configuration_panel_form_elements( + $mform, + $wfconfighelper->get_upload_workflow_configuration_panel(), + $wfconfighelper->get_allowed_upload_configurations() + ); + } + $mform->closeHeaderBefore('upload_filepicker'); $mform->addElement('header', 'upload_filepicker', get_string('upload', 'block_opencast')); diff --git a/classes/local/apibridge.php b/classes/local/apibridge.php index 93ea8618..fe7d1b9e 100644 --- a/classes/local/apibridge.php +++ b/classes/local/apibridge.php @@ -253,12 +253,13 @@ public function ingest_add_attachment($mediapackage, $flavor, $file) { * Ingests a mediapackage. * @param string $mediapackage Mediapackage * @param string $uploadworkflow workflow definition is to start after ingest + * @param array $workflowconfiguration workflow configuration as array * @return string Workflow instance that was started * @throws dml_exception * @throws moodle_exception * @throws opencast_connection_exception */ - public function ingest($mediapackage, $uploadworkflow = '') { + public function ingest($mediapackage, $uploadworkflow = '', $workflowconfiguration = []) { $ingestapi = $this->get_ingest_api(); if (empty($uploadworkflow)) { @@ -268,9 +269,9 @@ public function ingest($mediapackage, $uploadworkflow = '') { $uploadtimeout = get_config('block_opencast', 'uploadtimeout'); if ($uploadtimeout !== false) { $timeout = intval($uploadtimeout); - $response = $ingestapi->setRequestTimeout($timeout)->ingest($mediapackage, $uploadworkflow); + $response = $ingestapi->setRequestTimeout($timeout)->ingest($mediapackage, $uploadworkflow, '', $workflowconfiguration); } else { - $response = $ingestapi->ingest($mediapackage, $uploadworkflow); + $response = $ingestapi->ingest($mediapackage, $uploadworkflow, '', $workflowconfiguration); } $code = $response['code']; @@ -1167,6 +1168,7 @@ public function create_event($job) { global $DB; $event = new event(); + $wfconfighelper = workflowconfiguration_helper::get_instance($this->ocinstanceid); // Get initial visibility object. $initialvisibility = visibility_helper::get_initial_visibility($job); @@ -1216,7 +1218,8 @@ public function create_event($job) { $acl = $event->get_json_acl(); $metadata = $event->get_meta_data(); - $processing = $event->get_processing($this->ocinstanceid); + $processingdata = $wfconfighelper->get_workflow_processing_data($job->workflowconfiguration); + $processing = $processingdata['processing_json']; $scheduling = ''; $presenter = null; $presentation = null; @@ -1883,6 +1886,26 @@ public function get_workflow_definition($id) { return false; } + /** + * Retrieves a workflow instance from Opencast. + * @param string $id Workflow instance id + * @param bool $withoperations flag to get instance with operations + * @param bool $withconfiguration flag to get instance with configurations + * @return false|mixed Workflow instance or false if not successful + */ + public function get_workflow_instance($id, $withoperations = false, $withconfiguration = true) { + $response = $this->api->opencastapi->workflowsApi->get( + $id, + $withoperations, + $withconfiguration + ); + if ($response['code'] === 200) { + return $response['body']; + } + + return false; + } + /** * Helperfunction to get the list of available workflows to be used in the plugin's settings. * diff --git a/classes/local/batchupload_form.php b/classes/local/batchupload_form.php index 2b66876f..b770ac5a 100644 --- a/classes/local/batchupload_form.php +++ b/classes/local/batchupload_form.php @@ -59,6 +59,7 @@ public function definition() { $ocinstanceid = $this->_customdata['ocinstanceid']; $apibridge = apibridge::get_instance($ocinstanceid); $eventdefaults = $this->_customdata['eventdefaults']; + $wfconfighelper = new workflowconfiguration_helper($ocinstanceid); $mform = $this->_form; @@ -286,6 +287,22 @@ public function definition() { } } + // Offering workflow configuration panel settings. + if ($wfconfighelper->can_provide_configuration_panel()) { + $mform->closeHeaderBefore('configurationpanel_header'); + $mform->addElement('header', 'configurationpanel_header', get_string('configurationpanel_header', 'block_opencast')); + $mform->setExpanded('configurationpanel_header', true); + + $configpanelexplanation = html_writer::tag('p', get_string('configurationpanelheader_explanation', 'block_opencast')); + $mform->addElement('html', $configpanelexplanation); + + $renderer->render_configuration_panel_form_elements( + $mform, + $wfconfighelper->get_upload_workflow_configuration_panel(), + $wfconfighelper->get_allowed_upload_configurations() + ); + } + // Batch Upload section. $mform->closeHeaderBefore('batchupload_form_header'); diff --git a/classes/local/ingest_uploader.php b/classes/local/ingest_uploader.php index e5685d96..8ee5ade4 100644 --- a/classes/local/ingest_uploader.php +++ b/classes/local/ingest_uploader.php @@ -74,6 +74,7 @@ class ingest_uploader { public static function create_event($job) { global $DB; $apibridge = apibridge::get_instance($job->ocinstanceid); + $wfconfighelper = workflowconfiguration_helper::get_instance($job->ocinstanceid); switch ($job->status) { case self::STATUS_INGEST_CREATING_MEDIA_PACKAGE: @@ -216,7 +217,10 @@ public static function create_event($job) { } case self::STATUS_INGEST_INGESTING: try { - $workflow = $apibridge->ingest($job->mediapackage); + // Prepare workflow configuration beforehand. + $processingdata = $wfconfighelper->get_workflow_processing_data($job->workflowconfiguration); + $workflowconfiguration = $processingdata['configuration']; + $workflow = $apibridge->ingest($job->mediapackage, '', $workflowconfiguration); mtrace('... video uploaded'); // Move on to next status. self::update_status_with_mediapackage($job, upload_helper::STATUS_UPLOADED); @@ -228,6 +232,8 @@ public static function create_event($job) { $event = new stdClass(); $event->identifier = $values[array_search('MP:MEDIAPACKAGE', array_column($values, 'tag'))]['attributes']['ID']; + $event->workflowid = $values[array_search('MP:WORKFLOW', + array_column($values, 'tag'))]['attributes']['ID']; return $event; } catch (opencast_connection_exception $e) { diff --git a/classes/local/upload_helper.php b/classes/local/upload_helper.php index 22a973ef..e179e962 100644 --- a/classes/local/upload_helper.php +++ b/classes/local/upload_helper.php @@ -153,8 +153,10 @@ public static function get_upload_jobs($ocinstanceid, $courseid) { * @param int $courseid Course id * @param object $options Options * @param object $visibility Visibility object + * @param string $workflowconfiguration Workflow configuration */ - public static function save_upload_jobs($ocinstanceid, $courseid, $options, $visibility = null) { + public static function save_upload_jobs($ocinstanceid, $courseid, $options, $visibility = null, + $workflowconfiguration = null) { global $DB, $USER; // Find the current files for the jobs. @@ -224,6 +226,12 @@ public static function save_upload_jobs($ocinstanceid, $courseid, $options, $vis $job->timecreated = time(); $job->timemodified = $job->timecreated; $job->ocinstanceid = $ocinstanceid; + + // Add workflow processing data to the uploadjob as json string. + if (!empty($workflowconfiguration)) { + $job->workflowconfiguration = $workflowconfiguration; + } + $uploadjobid = $DB->insert_record('block_opencast_uploadjob', $job); $options->uploadjobid = $uploadjobid; @@ -605,6 +613,7 @@ protected function process_upload_job($job) { if ($event) { $stepsuccessful = true; $job->opencasteventid = $event->identifier; + $job->workflowid = (int) $event->workflowid; $DB->update_record('block_opencast_uploadjob', $job); } break; diff --git a/classes/local/workflowconfiguration_helper.php b/classes/local/workflowconfiguration_helper.php new file mode 100644 index 00000000..bc3bf345 --- /dev/null +++ b/classes/local/workflowconfiguration_helper.php @@ -0,0 +1,226 @@ +. + +/** + * Workflow configuration helper. + * @package block_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_opencast\local; + +use html_writer; + +/** + * Workflow configuration Helper. + * @package block_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class workflowconfiguration_helper { + + /** @var string The upload workflow mapping hidden input id. */ + const MAPPING_INPUT_HIDDEN_ID = 'configpanelmapping'; + + /** @var ?workflowconfiguration_helper the static instance of the class. */ + private static $instance = null; + + /** @var int The opencast instance id. */ + private $ocinstanceid; + + /** @var apibridge The apibridge instance. */ + private $apibridge; + + /** @var stdClass the upload workflow object. */ + private $uploadworkflow; + + /** @var string The upload workflow id. */ + private $uploadworkflowid; + + /** + * The construct method for this class. + * + * @param int $ocinstanceid the opencast instance id. + */ + public function __construct(int $ocinstanceid) { + $this->ocinstanceid = $ocinstanceid; + $this->apibridge = apibridge::get_instance($ocinstanceid); + $this->set_uploadworkflowid(); + $this->set_uploadworkflow(); + } + + /** + * Get the singleton instance of this class. + * + * @param int $ocinstanceid the opencast instance id. + * + * @return workflowconfiguration_helper an instance of the class. + */ + public static function get_instance(int $ocinstanceid): workflowconfiguration_helper { + if (is_null(self::$instance)) { + self::$instance = new workflowconfiguration_helper($ocinstanceid); + } + return self::$instance; + } + + /** + * Sets the upload workflow id from the config, falls back to "ng-schedule-and-upload" if not configured yet. + */ + private function set_uploadworkflowid() { + $uploadworkflowid = get_config('block_opencast', 'uploadworkflow_' . $this->ocinstanceid); + // Falling back to the general "ng-schedule-and-upload" workflow. + if (empty($uploadworkflowid)) { + $uploadworkflowid = 'ng-schedule-and-upload'; + } + $this->uploadworkflowid = $uploadworkflowid; + } + + /** + * Sets the upload workflow object by calling it from opencast API. + */ + private function set_uploadworkflow() { + $uploadworkflow = $this->apibridge->get_workflow_definition($this->uploadworkflowid); + $this->uploadworkflow = $uploadworkflow ?? null; + } + + /** + * Detemines whether all requirements to provide the upload configuration panel to the teachers in upload page are met. + * + * @return boolean whether or the upload configuration panel cpoiuld be provided to the users. + */ + public function can_provide_configuration_panel(): bool { + return !empty($this->uploadworkflow) && + !empty($this->uploadworkflow->configuration_panel) && + !empty(get_config('block_opencast', 'enableuploadwfconfigpanel_' . $this->ocinstanceid)); + } + + /** + * Compiles and convert the user defined configuration panel data received after the upload forms are submitted. + * + * @param stdClass $formdata the form data object recieved after form submittion. + * + * @return array the user defined configuration panel data + */ + public function get_userdefined_configuration_data(\stdClass $formdata): array { + $configpaneldata = []; + if ($this->can_provide_configuration_panel() && property_exists($formdata, self::MAPPING_INPUT_HIDDEN_ID)) { + $configpanelmapping = json_decode($formdata->{self::MAPPING_INPUT_HIDDEN_ID}, true); + foreach ($configpanelmapping as $cpid => $mappingtype) { + $isboolean = $mappingtype === 'boolean'; + if (property_exists($formdata, $cpid)) { + $value = boolval($formdata->$cpid); + if ($isboolean) { + $value = !empty($value) ? 'true' : 'false'; + } + if ($mappingtype === 'date') { + $dobj = new \DateTime("now", new \DateTimeZone("UTC")); + $dobj->setTimestamp(intval($value)); + $value = $dobj->format('Y-m-d\TH:i:s\Z'); + } + $configpaneldata[$cpid] = $value; + } else if ($isboolean) { + $configpaneldata[$cpid] = 'false'; + } + } + } + return $configpaneldata; + } + + /** + * Read the configuration and applies the comma separation mechanism to return the string to array of alloed config ids. + * + * @return array the list of allowed config panel elements ids. + */ + public function get_allowed_upload_configurations(): array { + $alloweduploadwfconfigs = get_config('block_opencast', 'alloweduploadwfconfigs_' . $this->ocinstanceid); + $alloweduploadwfconfigids = []; + if (!empty(trim($alloweduploadwfconfigs))) { + $alloweduploadwfconfigids = explode(',', $alloweduploadwfconfigs); + $alloweduploadwfconfigids = array_map('trim', $alloweduploadwfconfigids); + } + return $alloweduploadwfconfigids; + } + + /** + * Get sthe upload workflow configuration panel. + * + * @return string | null the configuration panel or null if not available. + */ + public function get_upload_workflow_configuration_panel(): ?string { + $configpanel = null; + if (!empty($this->uploadworkflow) && !empty($this->uploadworkflow->configuration_panel)) { + $configpanel = (string) $this->uploadworkflow->configuration_panel; + } + return $configpanel; + } + + /** + * Gets the workflow processing data to pass to the event creation calls (api or ingest). + * + * @param ?string $jobworkflowconfiguration the workflow configuration stored in the uploadjob or null if not defined. + * + * @return array the workflow processing data to pass to the event creation calls. It contains most usable output such as: + * - workflow: the upload workflow + * - processing: the array processing + * - processing_json: the json encoded string of processing array + * - configuration_json: the json encoded string of configuration array + * - configuration: the array list of all workflow configuration (defaults and user defineds). + */ + public function get_workflow_processing_data(?string $jobworkflowconfiguration = null): array { + + $processing = []; + $processing['workflow'] = $this->uploadworkflowid; + + // Default workflow configurations. + $processing['configuration'] = [ + "flagForCutting" => "false", + "flagForReview" => "false", + "publishToHarvesting" => "false", + "straightToPublishing" => "true", + ]; + + // Take care of engane publishing. + $publistoengage = get_config('block_opencast', 'publishtoengage_' . $this->ocinstanceid); + $publistoengage = (empty($publistoengage)) ? "false" : "true"; + + $processing['configuration']['publishToEngage'] = $publistoengage; + + if ($this->can_provide_configuration_panel() && !empty($jobworkflowconfiguration)) { + $alloweduploadwfconfigids = $this->get_allowed_upload_configurations(); + + $workflowconfigurationarr = json_decode($jobworkflowconfiguration, true); + foreach ($workflowconfigurationarr as $configid => $value) { + if (!empty($alloweduploadwfconfigids) && !in_array($configid, $alloweduploadwfconfigids)) { + continue; + } + $processing['configuration'][$configid] = (string) $value; + } + } + + $result = [ + 'workflow' => $this->uploadworkflowid, + 'processing' => $processing, + 'processing_json' => json_encode($processing), + 'configuration_json' => json_encode($processing['configuration']), + 'configuration' => $processing['configuration'], + ]; + + return $result; + } +} diff --git a/db/install.xml b/db/install.xml index 16d4bad5..e25f604f 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -24,6 +24,8 @@ + + diff --git a/db/upgrade.php b/db/upgrade.php index b67235de..db571bb1 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -861,5 +861,29 @@ function xmldb_block_opencast_upgrade($oldversion) { upgrade_block_savepoint(true, 2024061400, 'opencast'); } + if ($oldversion < 2024093000) { + + $table = new xmldb_table('block_opencast_uploadjob'); + + // Define field workflowconfiguration to be added to block_opencast_uploadjob. + $field = new xmldb_field('workflowconfiguration', XMLDB_TYPE_TEXT, null, null, null, null, null, 'mediapackage'); + + // Conditionally launch add field workflowconfiguration. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field workflowid to be added to block_opencast_uploadjob. + $field = new xmldb_field('workflowid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'workflowconfiguration'); + + // Conditionally launch add field workflowid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Opencast savepoint reached. + upgrade_block_savepoint(true, 2024093000, 'opencast'); + } + return true; } diff --git a/lang/en/block_opencast.php b/lang/en/block_opencast.php index 860cf373..dd005cf4 100644 --- a/lang/en/block_opencast.php +++ b/lang/en/block_opencast.php @@ -168,6 +168,8 @@ $string['adminchoice_noworkflow'] = "-- No workflow --"; $string['allowdownloadtranscriptionsetting'] = 'Allow download transcriptions'; $string['allowdownloadtranscriptionsetting_desc'] = 'When enabled, the transcription download button will be displayed in the manage transcriptions page, by which teachers are able to download the transcription\'s file.
Notice: In case you are using Opencast 13 or later, you need to make sure that all prerequisites including LTI features and permissions to access /assets/ endpoint for LTI users, are set correctly as it is mandatory to perform LTI call.'; +$string['alloweduploadwfconfigs'] = 'Allowed upload workflow configurations'; +$string['alloweduploadwfconfigsdesc'] = 'A comma separated list of allowed upload workflow configuration ids to be shown to the teachers in upload page, which already exist in the workflow configuration panel.
For example: straightToPublishing,whisper_de,vosk_en
NOTE: if empty, all elements in the workflow configuration panel such as input and select elements will be provided to the teachers.'; $string['allowunassign'] = 'Allow unassign from course'; $string['allowunassigndesc'] = 'Delete the assignment of a course series to control visibility in filepicker and course lists. This feature is only available, when it is possible to have events without series in opencast. Please ask the admistrator of the opencast system before activating this.'; $string['appearance_overview_settingheader'] = 'Overview page'; @@ -235,6 +237,8 @@ $string['changingownersuccess'] = 'The ownership was successfully transferred.'; $string['claimowner_explanation'] = 'Currently, nobody owns the video {$a}.
You can claim the ownership or set another person as owner.
Notice: You might loose the right to access the video if you do not claim the ownership yourself.'; $string['claimownerseries_explanation'] = 'Currently, nobody owns the series {$a}.
You can claim the ownership or set another person as owner.
Notice: You might loose the right to access the series if you do not claim the ownership yourself.'; +$string['configurationpanel_header'] = 'Processing Settings'; +$string['configurationpanelheader_explanation'] = 'The following settings options are available to customized the upload process according to your likings.'; $string['connection_failure'] = 'Could not reach Opencast server.'; $string['contributor'] = 'Contributor(s)'; $string['coursefullnameunknown'] = 'Unkown coursename'; @@ -330,6 +334,8 @@ $string['enableschedulingchangevisibility_massaction'] = 'Schedule a visibility change for the selected video(s)'; $string['enableschedulingchangevisibilitydesc'] = 'Set a date and a visibility status for the event in future, which will be performed using a scheduled task.'; $string['enableschedulingchangevisibilitydesc_massaction'] = 'Set a date and a visibility status for the selected video(s) in future, which will be performed using a scheduled task.'; +$string['enableuploadwfconfigpanel'] = 'Show workflow configurations during upload'; +$string['enableuploadwfconfigpaneldesc'] = 'If activated, the configuration panel of the upload workflow will is shown to the teachers during uplaod video in the upload page. With this feature teachers are able to decide further processing options or input values during upload process in Opencast depending on the workflow structure.'; $string['engageplayerintegration'] = 'Engage player integration'; $string['engageredirect'] = 'Redirect to engage player'; $string['engageurl'] = 'URL of the Opencast Engage server'; diff --git a/renderer.php b/renderer.php index 17aa33c2..58e533c3 100644 --- a/renderer.php +++ b/renderer.php @@ -35,7 +35,7 @@ use block_opencast\local\liveupdate_helper; use tool_opencast\local\settings_api; use tool_opencast\seriesmapping; - +use block_opencast\local\workflowconfiguration_helper; /** * Renderer class for block opencast. * @@ -1511,4 +1511,216 @@ public function prepare_transcription_items_for_the_menu($mediapackagesubs, $cou } return $items; } + + /** + * Looks through the html string of workflow configuration panel and + * converts select elements as well as input elements of types (checkbox, text, datetime, radio) + * into moodle form elements. + * + * @param MoodleQuickForm $mform referenced moodle form of upload videos single and batch. + * @param string $configurationpanelhtml workflow configuration panel html as string. + * @param array $alloweduploadwfconfigids list of allowed configuration element ids. + */ + public function render_configuration_panel_form_elements(&$mform, $configurationpanelhtml, $alloweduploadwfconfigids = []) { + + if (empty($configurationpanelhtml)) { + return; + } + + // Make sure the html string is valid. + $html = $this->close_tags_in_html_string($configurationpanelhtml); + + // Initialize the dom and xpath instances. + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->loadHTML($html); + $xpath = new \DOMXpath($dom); + + // Extracting all inputs and selects in one loop to maintain the sequence and convert them into moodle form elements. + $elements = $xpath->query('//input | //select'); + $radiotexts = []; + $defaults = []; + $configpanelelementmapping = []; + foreach ($elements as $element) { + $id = trim($element->getAttribute('id')); + // If it is decided to provide only allowed configs, we filter them here! + if (!empty($alloweduploadwfconfigids) && !in_array($id, $alloweduploadwfconfigids)) { + continue; + } + $nodetype = $element->tagName; + $name = trim($element->getAttribute('name')); + $required = $element->hasAttribute('required') ?? false; + $labeltext = $this->get_dom_label_text($xpath, $dom, $id); + $title = $labeltext ?? $id; + $default = null; + $moodleid = $id; + $mappingtype = 'text'; + + // Conver input to moodle form element. + if ($nodetype === 'input') { + $type = trim($element->getAttribute('type')); + $value = trim($element->getAttribute('value')) ?? null; + + // Support for radio elements. + if ($type === 'radio') { + $moodleid = $name; + $parenttext = ''; + if (!array_key_exists($moodleid, $radiotexts)) { + $parenttext = $this->get_parent_label_text($xpath, $dom, $id); + $radiotexts[$moodleid] = $parenttext; + } + $mform->addElement('radio', $moodleid, $parenttext, $title, $value); + $ischecked = $element->hasAttribute('checked'); + if (!empty($ischecked)) { + $default = $value; + } + } + + // Support for number and text elements. + if ($type === 'number' || $type === 'text') { + $mform->addElement('text', $moodleid, $title); + $elementtype = $type === 'number' ? PARAM_INT : PARAM_TEXT; + $mform->setType($moodleid, $elementtype); + if (!is_null($value)) { + $default = $value; + } + } + + // Support for date and datetime elements. + if (substr($type, 0, 4) === 'date') { + $mappingtype = 'date'; + $moodletype = substr($type, 0, 8) === 'datetime' ? 'date_time_selector' : 'date_selector'; + $mform->addElement($moodletype, $moodleid, $title); + if (!is_null($value)) { + $default = $value; + } + } + + // Support for checkbox elements. + if ($type === 'checkbox') { + $mappingtype = 'boolean'; + $mform->addElement('checkbox', $moodleid, $title); + if (!is_null($value)) { + $default = $value; + } + } + + } else if ($nodetype === 'select') { // Conver select to moodle form element. + // Extract options. + $options = []; + $optionnodes = $element->childNodes; + $selected = null; + foreach ($optionnodes as $node) { + if ($node->tagName === 'option') { + $optionvalue = $node->getAttribute('value'); + $options[$optionvalue] = $node->textContent; + if ($node->hasAttribute('selected')) { + $selected = $optionvalue; + } + } + } + + $mform->addElement('select', $moodleid, $title, $options); + if (!is_null($selected)) { + $default = $selected; + } + } + + // Add required rule. + if ($required) { + $mform->addRule($moodleid, get_string('required'), 'required'); + } + + // Add default value. + if (!is_null($default)) { + $defaults[$moodleid] = $default; + } + + // We need to keep track of moodleids and mapping types for the elements added by configuration panel. + if (!array_key_exists($moodleid, $configpanelelementmapping)) { + $configpanelelementmapping[$moodleid] = $mappingtype; + } + } + + // Apply defaults. + if (!empty($defaults)) { + foreach ($defaults as $moodleid => $value) { + $mform->setDefault($moodleid, $value); + } + } + + if (!empty($configpanelelementmapping)) { + $mform->addElement('hidden', + workflowconfiguration_helper::MAPPING_INPUT_HIDDEN_ID, + json_encode($configpanelelementmapping) + ); + $mform->setType(workflowconfiguration_helper::MAPPING_INPUT_HIDDEN_ID, PARAM_TEXT); + } + } + + /** + * Get the label text of an element. + * First lookign for "for" attribute, if not found, looks for preceding then following label element. + * + * @param DOMXpath $xpath the xpath object of the html. + * @param DOMDocument $dom the dom object of the html + * @param string $id the element id to look for its label text. + * + * @return string | null the label text or null if not found. + */ + private function get_dom_label_text($xpath, $dom, $id) { + $element = $dom->getElementById($id); + $parent = $element->parentNode; + $tagname = $element->tagName; + $directlabelquery = "//label[contains(@for, '{$id}')]"; + $text = $this->get_text_from_domxpath($xpath, $directlabelquery, $parent); + // If the text is empty as of now, that means there is no direct label tag "for" this element. + // We try to look for any preceding or following label respectively. + if (empty($text)) { + // First preceding label. + $precedinglabelquery = "//{$tagname}[@id='{$id}']/preceding-sibling::label"; + $text = $this->get_text_from_domxpath($xpath, $precedinglabelquery, $parent); + + // If it is still empty, then following label. + if (empty($text)) { + $followinglabelquery = "//{$tagname}[@id='{$id}']/following-sibling::label"; + + // If it reaches here and is still null, then it is supposed to fallback to id! + $text = $this->get_text_from_domxpath($xpath, $followinglabelquery, $parent); + } + } + return $text; + } + + /** + * Get the label text of the parent element. + * Considering the first label element of the parent. + * + * @param DOMXpath $xpath the xpath object of the html. + * @param DOMDocument $dom the dom object of the html + * @param string $id the element id to look for its parent's label text. + * + * @return string | null the label text or null if not found. + */ + private function get_parent_label_text($xpath, $dom, $id) { + $element = $dom->getElementById($id); + $parent = $element->parentNode; + return $this->get_text_from_domxpath($xpath, 'label[1]/text()', $parent); + } + + /** + * A helper function to evalute a DOMXpath query string and return the node value (text) if found. + * + * @param DOMXpath $xpath the xpath object of the html. + * @param string $query the xpat query to look for. + * @param DOMDocument $contextnode a portion or target dom object. + * + * @return string | null the text of the element, or null if not found. + */ + private function get_text_from_domxpath($xpath, $query, $contextnode = null) { + $elementnodes = $xpath->evaluate($query, $contextnode); + if ($elementnodes->length > 0) { + return (string) $elementnodes->item(0)->nodeValue; + } + return null; + } } diff --git a/settings.php b/settings.php index 1debeb73..48614f58 100644 --- a/settings.php +++ b/settings.php @@ -208,6 +208,22 @@ 'ng-schedule-and-upload', $workflowchoices )); + $generalsettings->add(new admin_setting_configcheckbox('block_opencast/enableuploadwfconfigpanel_' . $instance->id, + get_string('enableuploadwfconfigpanel', 'block_opencast'), + get_string('enableuploadwfconfigpaneldesc', 'block_opencast'), + 0 + )); + + $generalsettings->add(new admin_setting_configtext('block_opencast/alloweduploadwfconfigs_' . $instance->id, + get_string('alloweduploadwfconfigs', 'block_opencast'), + get_string('alloweduploadwfconfigsdesc', 'block_opencast'), + '', + PARAM_TEXT + )); + + $generalsettings->hide_if('block_opencast/alloweduploadwfconfigs_' . $instance->id, + 'block_opencast/enableuploadwfconfigpanel_' . $instance->id, 'notchecked'); + $generalsettings->add(new admin_setting_configcheckbox('block_opencast/publishtoengage_' . $instance->id, get_string('publishtoengage', 'block_opencast'), get_string('publishtoengagedesc', 'block_opencast'), diff --git a/tests/behat/block_opencast_addvideo.feature b/tests/behat/block_opencast_addvideo.feature index 1e621a5b..76d2d190 100644 --- a/tests/behat/block_opencast_addvideo.feature +++ b/tests/behat/block_opencast_addvideo.feature @@ -16,24 +16,30 @@ Feature: Add videos as Teacher | teacher1 | C1 | editingteacher | And I setup the default settigns for opencast plugins And the following config values are set as admin: - | config | value | plugin | - | apiurl_1 | http://172.17.0.1:8080 | tool_opencast | - | apipassword_1 | opencast | tool_opencast | - | apiusername_1 | admin | tool_opencast | - | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | - | limituploadjobs_1 | 0 | block_opencast | - | group_creation_1 | 0 | block_opencast | - | group_name_1 | Moodle_course_[COURSEID] | block_opencast | - | series_name_1 | Course_Series_[COURSEID] | block_opencast | - | enablechunkupload_1 | 0 | block_opencast | + | config | value | plugin | + | apiurl_1 | http://172.17.0.1:8080 | tool_opencast | + | apipassword_1 | opencast | tool_opencast | + | apiusername_1 | admin | tool_opencast | + | apiversion_1 | v1.10.0 | tool_opencast | + | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | + | limituploadjobs_1 | 0 | block_opencast | + | group_creation_1 | 0 | block_opencast | + | group_name_1 | Moodle_course_[COURSEID] | block_opencast | + | series_name_1 | Course_Series_[COURSEID] | block_opencast | + | enablechunkupload_1 | 0 | block_opencast | + | uploadworkflow_1 | schedule-and-upload | block_opencast | + | enableuploadwfconfigpanel_1 | 1 | block_opencast | + | alloweduploadwfconfigs_1 | straightToPublishing | block_opencast | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I add the "Opencast Videos" block - Scenario: Opencast Add video page implemented + Scenario: Opencast Add video page implemented with configuration panel Given I click on "Go to overview..." "link" When I click on "Add video" "button" Then I should see "You can drag and drop files here to add them." + And I should see "Processing Settings" + And I should see "Straight to publishing" @_file_upload @javascript Scenario: Opencast Upload Video diff --git a/tests/behat/block_opencast_addvideosbatch.feature b/tests/behat/block_opencast_addvideosbatch.feature index e86a25b8..f7995cc1 100644 --- a/tests/behat/block_opencast_addvideosbatch.feature +++ b/tests/behat/block_opencast_addvideosbatch.feature @@ -16,25 +16,31 @@ Feature: Add videos batch as Teacher | teacher1 | C1 | editingteacher | And I setup the default settigns for opencast plugins And the following config values are set as admin: - | config | value | plugin | - | apiurl_1 | http://172.17.0.1:8080 | tool_opencast | - | apipassword_1 | opencast | tool_opencast | - | apiusername_1 | admin | tool_opencast | - | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | - | limituploadjobs_1 | 0 | block_opencast | - | group_creation_1 | 0 | block_opencast | - | group_name_1 | Moodle_course_[COURSEID] | block_opencast | - | series_name_1 | Course_Series_[COURSEID] | block_opencast | - | enablechunkupload_1 | 0 | block_opencast | - | batchuploadenabled_1 | 1 | block_opencast | + | config | value | plugin | + | apiurl_1 | http://172.17.0.1:8080 | tool_opencast | + | apipassword_1 | opencast | tool_opencast | + | apiusername_1 | admin | tool_opencast | + | apiversion_1 | v1.10.0 | tool_opencast | + | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | + | limituploadjobs_1 | 0 | block_opencast | + | group_creation_1 | 0 | block_opencast | + | group_name_1 | Moodle_course_[COURSEID] | block_opencast | + | series_name_1 | Course_Series_[COURSEID] | block_opencast | + | enablechunkupload_1 | 0 | block_opencast | + | batchuploadenabled_1 | 1 | block_opencast | + | uploadworkflow_1 | schedule-and-upload | block_opencast | + | enableuploadwfconfigpanel_1 | 1 | block_opencast | + | alloweduploadwfconfigs_1 | straightToPublishing | block_opencast | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I add the "Opencast Videos" block - Scenario: Opencast Add videos (batch) page implemented + Scenario: Opencast Add videos (batch) page implemented with configuration panel Given I click on "Go to overview..." "link" When I click on "Add videos (batch)" "button" Then I should see "You can drag and drop files here to add them." + And I should see "Processing Settings" + And I should see "Straight to publishing" @_file_upload @javascript Scenario: Opencast Batch Upload Video diff --git a/tests/behat/block_opencast_autocomplete_metadata.feature b/tests/behat/block_opencast_autocomplete_metadata.feature index 40fb224b..d8a90ab8 100644 --- a/tests/behat/block_opencast_autocomplete_metadata.feature +++ b/tests/behat/block_opencast_autocomplete_metadata.feature @@ -22,19 +22,23 @@ Feature: Check and set autocompletion suggestions | manager1 | C1 | manager | And I setup the default settigns for opencast plugins And the following config values are set as admin: - | config | value | plugin | - | apiurl_1 | http://testapi:8080 | tool_opencast | - | apipassword_1 | opencast | tool_opencast | - | apiusername_1 | admin | tool_opencast | - | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | - | limituploadjobs_1 | 0 | block_opencast | - | group_creation_1 | 0 | block_opencast | - | group_name_1 | Moodle_course_[COURSEID] | block_opencast | - | series_name_1 | Course_Series_[COURSEID] | block_opencast | - | enablechunkupload_1 | 0 | block_opencast | - | workflow_roles_1 | republish-metadata | block_opencast | - | metadata_1 | [{"name":"creator","datatype":"autocomplete","required":0,"readonly":0,"param_json":null}] | block_opencast | - | metadataseries_1 | [{"name":"creator","datatype":"autocomplete","required":0,"readonly":0,"param_json":null}] | block_opencast | + | config | value | plugin | + | apiurl_1 | http://testapi:8080 | tool_opencast | + | apipassword_1 | opencast | tool_opencast | + | apiusername_1 | admin | tool_opencast | + | ocinstances | [{"id":1,"name":"Default","isvisible":true,"isdefault":true}] | tool_opencast | + | limituploadjobs_1 | 0 | block_opencast | + | group_creation_1 | 0 | block_opencast | + | group_name_1 | Moodle_course_[COURSEID] | block_opencast | + | series_name_1 | Course_Series_[COURSEID] | block_opencast | + | enablechunkupload_1 | 0 | block_opencast | + | showpublicationchannels_1 | 0 | block_opencast | + | showenddate_1 | 0 | block_opencast | + | showlocation_1 | 0 | block_opencast | + | aclcontrolafter_1 | 0 | block_opencast | + | workflow_roles_1 | republish-metadata | block_opencast | + | metadata_1 | [{"name":"creator","datatype":"autocomplete","required":0,"readonly":0,"param_json":null}] | block_opencast | + | metadataseries_1 | [{"name":"creator","datatype":"autocomplete","required":0,"readonly":0,"param_json":null}] | block_opencast | And I setup the opencast test api And I upload a testvideo And I log in as "admin" diff --git a/tests/upload_ingest_with_configpanel_test.php b/tests/upload_ingest_with_configpanel_test.php new file mode 100644 index 00000000..c1628f23 --- /dev/null +++ b/tests/upload_ingest_with_configpanel_test.php @@ -0,0 +1,198 @@ +. + +/** + * Test Upload video with ingest and user defined configuration panel. + * @package block_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_opencast; + +use advanced_testcase; +use block_opencast\local\apibridge; +use block_opencast\local\upload_helper; +use block_opencast\local\workflowconfiguration_helper; +use coding_exception; +use context_course; +use dml_exception; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Test Upload video with ingest and user defined configuration panel. + * @package block_opencast + * @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V. + * @author Farbod Zamani Boroujeni + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class upload_ingest_with_configpanel_test extends advanced_testcase { + + + /** @var string Test api url. */ + private $apiurl = 'http://127.0.0.1:8080'; + /** @var string Test api username. */ + private $apiusername = 'admin'; + /** @var string Test api password. */ + private $apipassword = 'opencast'; + /** @var int the curl timeout in milliseconds */ + private $apitimeout = 2000; + /** @var int the curl connecttimeout in milliseconds */ + private $apiconnecttimeout = 1000; + + /** + * Uploads a file to the opencast server using ingest with user defined configration panel, + * then checks if it was transmitted and the workflow configuration has been receieved by opencast correctly. + * + * @covers \block_opencast\local\upload_helper \block_opencast\local\workflowconfiguration_helper + * @throws coding_exception + * @throws dml_exception + */ + public function test_upload_ingest_configpanel(): void { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Setup course with block, groups and users. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + $coursecontext = context_course::instance($course->id); + + $teacher = $generator->create_user(); + $generator->enrol_user($teacher->id, $course->id, 'editingteacher'); + + // Tool settings. + set_config('apiurl_1', $this->apiurl, 'tool_opencast'); + set_config('apiusername_1', $this->apiusername, 'tool_opencast'); + set_config('apipassword_1', $this->apipassword, 'tool_opencast'); + set_config('apitimeout_1', $this->apitimeout, 'tool_opencast'); + set_config('apiconnecttimeout_1', $this->apiconnecttimeout, 'tool_opencast'); + // Block settings. + set_config('ingestupload_1', 1, 'block_opencast'); + set_config('uploadworkflow_1', 'schedule-and-upload', 'block_opencast'); + set_config('enableuploadwfconfigpanel_1', 1, 'block_opencast'); + set_config('alloweduploadwfconfigs_1', 'straightToPublishing', 'block_opencast'); + set_config('limituploadjobs_1', 2, 'block_opencast'); + set_config('series_name_1', '[COURSENAME]', 'block_opencast'); + set_config('roles_1', + '[{"rolename":"ROLE_ADMIN","actions":"write,read","permanent":1},' . + '{"rolename":"ROLE_GROUP_MH_DEFAULT_ORG_EXTERNAL_APPLICATIONS","actions":"write,read","permanent":1},' . + '{"rolename":"[COURSEID]_Instructor","actions":"write,read","permanent":1},' . + '{"rolename":"[COURSEGROUPID]_Learner","actions":"read","permanent":0}]', + 'block_opencast'); + + // Upload file. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('block_opencast'); + + $record = []; + $record['courseid'] = $course->id; + $filename = $CFG->dirroot . '/blocks/opencast/tests/fixtures/test.mp4'; + $record['filecontent'] = file_get_contents($filename); + + $file = $plugingenerator->create_file($record); + $this->assertInstanceOf('stored_file', $file); + $obj = [ + 'id' => 'title', + 'value' => 'test', + ]; + $metadata[] = $obj; + $options = new stdClass(); + $options->metadata = json_encode($metadata); + $options->presenter = $file ? $file->get_itemid() : ''; + $options->presentation = $file ? $file->get_itemid() : ''; + + apibridge::set_testing(false); + $apibridge = apibridge::get_instance(1, true); + + // Workflow configuration helper assertions. + $wfconfighelper = workflowconfiguration_helper::get_instance(1); + $this->assertTrue($wfconfighelper->can_provide_configuration_panel()); + $this->assertNotEmpty($wfconfighelper->get_upload_workflow_configuration_panel()); + $this->assertNotEmpty($wfconfighelper->get_allowed_upload_configurations()); + + $configpanelelementmapping = ['straightToPublishing' => 'boolean']; + $formdata = new stdClass(); + $formdata->{$wfconfighelper::MAPPING_INPUT_HIDDEN_ID} = json_encode($configpanelelementmapping); + + // Having the straightToPublishing as false, in order to bypass the default value and check it later. + $formdata->straightToPublishing = 0; + + $configpaneldata = $wfconfighelper->get_userdefined_configuration_data($formdata); + $this->assertArrayHasKey('straightToPublishing', $configpaneldata); + + $workflowconfiguration = json_encode($configpaneldata); + + upload_helper::save_upload_jobs(1, $course->id, $options, null, $workflowconfiguration); + + // Check upload job. + $jobs = $DB->get_records('block_opencast_uploadjob'); + $this->assertCount(1, $jobs); + + $uploadhelper = new upload_helper(); + $isuploaded = false; + $limiter = 5; + $counter = 0; + do { + $isuploaded = $this->notest_check_uploaded_video($course->id, $apibridge); + $counter++; + if ($counter >= $limiter) { + break; + } + } while (!$isuploaded); + + // Check if video was uploaded. + $videos = $apibridge->get_course_videos($course->id); + + $this->assertEmpty($videos->error, 'There was an error: ' . $videos->error); + $this->assertCount(1, $videos->videos); + + // Now we look for the workflowid in the uploadjob table. + $opencasteventid = $videos->videos[0]->identifier; + $uploadjob = $DB->get_record('block_opencast_uploadjob', ['opencasteventid' => $opencasteventid]); + $this->assertNotEmpty($uploadjob->workflowid); + + $workflowinstanceid = $uploadjob->workflowid; + $workflowinstance = $apibridge->get_workflow_instance($workflowinstanceid); + $this->assertNotEmpty($workflowinstance); + $this->assertEquals("false", $workflowinstance->configuration->straightToPublishing); + } + + /** + * Checks, if the video is available after upload, by running cron first to make sure upload video took place successfully. + * + * @param int $courseid Course ID + * @param apibridge $apibridge the apibridge instance + * + * @return bool true if the video is avialable, false otherwise. + */ + private function notest_check_uploaded_video($courseid, $apibridge) { + $uploadhelper = new upload_helper(); + // Prevent mtrace output, which would be considered risky. + ob_start(); + $uploadhelper->cron(); + ob_end_clean(); + sleep(15); + $videos = $apibridge->get_course_videos($courseid); + return (!empty($videos->videos)) ? true : false; + } +}