Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exercise: Add OnlyOffice question type with document editing support - refs BT#22370 #6121

Open
wants to merge 16 commits into
base: 1.11.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6bc5b60
Exercise: Add OnlyOffice question type with document editing support …
christianbeeznest Mar 8, 2025
4d82256
Exercise: Improved OnlyOffice integration and URL handling - refs BT#…
christianbeeznest Mar 9, 2025
a60bfcb
Exercise: Improve OnlyOffice integration with dynamic return URLs - r…
christianbeeznest Mar 9, 2025
e10f338
Exercise: Add missing parameters for docInfo onlyoffice - refs BT#22370
christianbeeznest Mar 10, 2025
b088041
Exercise: Improve file handling with onlyoffice and exercise tracking…
christianbeeznest Mar 10, 2025
e063434
Merge remote-tracking branch 'upstream/1.11.x' into efc-22370
christianbeeznest Mar 11, 2025
bb4ed6d
Exercise: Fix evaluation of Answer in Office doc question - refs BT#2…
christianbeeznest Mar 11, 2025
5fb30f2
Exercise: Improve display onlyoffice doc editor - refs BT#22370
christianbeeznest Mar 12, 2025
07d3aed
Exercise: Add readonly to editor onlyoffice in results - refs BT#22370
christianbeeznest Mar 12, 2025
1169d33
Merge remote-tracking branch 'upstream/1.11.x' into efc-22370
christianbeeznest Mar 19, 2025
1a5bc07
Exercise: Fix student edit permissions & finalization view in OnlyOffice
christianbeeznest Mar 20, 2025
98cf6fe
Exercise: Fix student id to display onlyoffice answer
christianbeeznest Mar 20, 2025
e21d4eb
Merge remote-tracking branch 'upstream/1.11.x' into efc-22370
christianbeeznest Mar 24, 2025
5bc1912
Exercise: Auto-refresh OnlyOffice iframe to show latest edits - refs …
christianbeeznest Mar 24, 2025
c486375
Exercise: Auto-refresh OnlyOffice iframe to show results - refs BT#22370
christianbeeznest Mar 24, 2025
d31bce5
Exercise: Auto-refresh OnlyOffice iframe - refs BT#22370
christianbeeznest Mar 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions main/exercise/AnswerInOfficeDoc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php

/* For licensing terms, see /license.txt */

/**
* Class AnswerInOfficeDoc
* Allows a question type where the answer is written in an Office document.
*
* @author Cristian
*/
class AnswerInOfficeDoc extends Question
{
public $typePicture = 'options_evaluation.png';
public $explanationLangVar = 'AnswerInOfficeDoc';
public $sessionId;
public $userId;
public $exerciseId;
public $exeId;
private $storePath;
private $fileName;
private $filePath;

/**
* Constructor.
*/
public function __construct()
{
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
throw new Exception(get_lang('OnlyOfficePluginRequired'));
}

parent::__construct();
$this->type = ANSWER_IN_OFFICE_DOC;
$this->isContent = $this->getIsContent();
}

/**
* Initialize the file path structure.
*/
public function initFile(int $sessionId, int $userId, int $exerciseId, int $exeId): void
{
$this->sessionId = $sessionId ?: 0;
$this->userId = $userId;
$this->exerciseId = $exerciseId ?: 0;
$this->exeId = $exeId;

$this->storePath = $this->generateDirectory();
$this->fileName = $this->generateFileName();
$this->filePath = $this->storePath . $this->fileName;
}

/**
* Create form for uploading an Office document.
*/
public function createAnswersForm($form): void
{
if (!empty($this->exerciseList)) {
$this->exerciseId = reset($this->exerciseList);
}

$allowedFormats = [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/msword', // .doc
'application/vnd.ms-excel' // .xls
];

$form->addElement('file', 'office_file', get_lang('UploadOfficeDoc'));
$form->addRule('office_file', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('office_file', get_lang('InvalidFileFormat'), 'mimetype', $allowedFormats);

$allowedExtensions = implode(', ', ['.docx', '.xlsx', '.doc', '.xls']);
$form->addElement('static', 'file_hint', get_lang('AllowedFormats'), "<p>{$allowedExtensions}</p>");

if (!empty($this->extra)) {
$fileUrl = api_get_path(WEB_COURSE_PATH) . $this->getStoredFilePath();
$form->addElement('static', 'current_office_file', get_lang('CurrentOfficeDoc'), "<a href='{$fileUrl}' target='_blank'>{$this->extra}</a>");
}

$form->addText('weighting', get_lang('Weighting'), ['class' => 'span1']);

global $text;
$form->addButtonSave($text, 'submitQuestion');

if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
}

/**
* Process the uploaded document and save it.
*/
public function processAnswersCreation($form, $exercise): void
{
if (!empty($_FILES['office_file']['name'])) {
$extension = pathinfo($_FILES['office_file']['name'], PATHINFO_EXTENSION);
$tempFilename = "office_" . uniqid() . "." . $extension;
$tempPath = sys_get_temp_dir() . '/' . $tempFilename;

if (!move_uploaded_file($_FILES['office_file']['tmp_name'], $tempPath)) {
return;
}

$this->weighting = $form->getSubmitValue('weighting');
$this->extra = "";
$this->save($exercise);

$this->exerciseId = $exercise->iid;
$uploadDir = $this->generateDirectory();

if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}

$filename = "office_".$this->iid.".".$extension;
$filePath = $uploadDir . $filename;

if (!rename($tempPath, $filePath)) {
return;
}

$this->extra = $filename;
$this->save($exercise);
}
}

/**
* Generate the necessary directory for OnlyOffice documents.
*/
private function generateDirectory(): string
{
$exercisePath = api_get_path(SYS_COURSE_PATH).$this->course['path']."/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/";

if (!is_dir($exercisePath)) {
mkdir($exercisePath, 0775, true);
}

return rtrim($exercisePath, '/') . '/';
}

/**
* Get the stored file path dynamically.
*/
public function getStoredFilePath(): ?string
{
if (empty($this->extra)) {
return null;
}

return "{$this->course['path']}/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/{$this->extra}";
}

/**
* Get the absolute file path. Returns null if the file doesn't exist.
*/
public function getFileUrl(bool $loadFromDatabase = false): ?string
{
if ($loadFromDatabase) {
$em = Database::getManager();
$result = $em->getRepository('ChamiloCoreBundle:TrackEAttempt')->findOneBy([
'exeId' => $this->exeId,
'userId' => $this->userId,
'questionId' => $this->iid,
'sessionId' => $this->sessionId,
'cId' => $this->course['real_id'],
]);

if (!$result || empty($result->getFilename())) {
return null;
}

$this->fileName = $result->getFilename();
} else {
if (empty($this->extra)) {
return null;
}

$this->fileName = $this->extra;
}

$filePath = $this->getStoredFilePath();

if (is_file(api_get_path(SYS_COURSE_PATH) . $filePath)) {
return $filePath;
}

return null;
}

/**
* Show the question in an exercise.
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'">
<tr>
<th>'.get_lang("Answer").'</th>
</tr>';

return $header;
}

/**
* Generate the file name for the OnlyOffice document.
*/
private function generateFileName(): string
{
return 'office_' . uniqid();
}
}
46 changes: 45 additions & 1 deletion main/exercise/exercise.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3958,7 +3958,8 @@ public function manage_answer(
$answerType == ORAL_EXPRESSION ||
$answerType == CALCULATED_ANSWER ||
$answerType == ANNOTATION ||
$answerType == UPLOAD_ANSWER
$answerType == UPLOAD_ANSWER ||
$answerType == ANSWER_IN_OFFICE_DOC
) {
$nbrAnswers = 1;
}
Expand Down Expand Up @@ -4762,6 +4763,7 @@ function ($answerId) use ($objAnswerTmp) {
break;
case UPLOAD_ANSWER:
case FREE_ANSWER:
case ANSWER_IN_OFFICE_DOC:
if ($from_database) {
$sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
WHERE
Expand Down Expand Up @@ -5423,6 +5425,18 @@ function ($answerId) use ($objAnswerTmp) {
$questionScore,
$results_disabled
);
} elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
$exe_info = Event::get_exercise_results_by_attempt($exeId);
$exe_info = $exe_info[$exeId] ?? null;
ExerciseShowFunctions::displayOnlyOfficeAnswer(
$feedback_type,
$exeId,
$exe_info['exe_user_id'] ?? api_get_user_id(),
$this->iid,
$questionId,
$questionScore,
true
);
} elseif ($answerType == ORAL_EXPRESSION) {
// to store the details of open questions in an array to be used in mail
/** @var OralExpression $objQuestionTmp */
Expand Down Expand Up @@ -5821,6 +5835,18 @@ function ($answerId) use ($objAnswerTmp) {
$results_disabled
);
break;
case ANSWER_IN_OFFICE_DOC:
$exe_info = Event::get_exercise_results_by_attempt($exeId);
$exe_info = $exe_info[$exeId] ?? null;
ExerciseShowFunctions::displayOnlyOfficeAnswer(
$feedback_type,
$exeId,
$exe_info['exe_user_id'] ?? api_get_user_id(),
$this->iid,
$questionId,
$questionScore
);
break;
case ORAL_EXPRESSION:
echo '<tr>
<td>'.
Expand Down Expand Up @@ -6463,6 +6489,24 @@ function ($answerId) use ($objAnswerTmp) {
false,
$questionDuration
);
} elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
$answer = $choice;
$exerciseId = $this->iid;
$questionId = $quesId;
$originalFilePath = $objQuestionTmp->getFileUrl();
$originalExtension = !empty($originalFilePath) ? pathinfo($originalFilePath, PATHINFO_EXTENSION) : 'docx';
$fileName = "response_{$exeId}.{$originalExtension}";
Event::saveQuestionAttempt(
$questionScore,
$answer,
$questionId,
$exeId,
0,
$exerciseId,
false,
$questionDuration,
$fileName
);
} elseif ($answerType == ORAL_EXPRESSION) {
$answer = $choice;
$absFilePath = $objQuestionTmp->getAbsoluteFilePath();
Expand Down
2 changes: 1 addition & 1 deletion main/exercise/exercise_report.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@

// From the database.
$marksFromDatabase = $questionListData[$questionId]['marks'];
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
// From the form.
$params['marks'] = $marks;
if ($marksFromDatabase != $marks) {
Expand Down
7 changes: 4 additions & 3 deletions main/exercise/exercise_show.php
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ function getFCK(vals, marksid) {
case GLOBAL_MULTIPLE_ANSWER:
case FREE_ANSWER:
case UPLOAD_ANSWER:
case ANSWER_IN_OFFICE_DOC:
case ORAL_EXPRESSION:
case MATCHING:
case MATCHING_COMBINATION:
Expand Down Expand Up @@ -612,7 +613,7 @@ function getFCK(vals, marksid) {
if ($isFeedbackAllowed && $action !== 'export') {
$name = 'fckdiv'.$questionId;
$marksname = 'marksName'.$questionId;
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$url_name = get_lang('EditCommentsAndMarks');
} else {
$url_name = get_lang('AddComments');
Expand Down Expand Up @@ -689,7 +690,7 @@ function getFCK(vals, marksid) {
}

if ($is_allowedToEdit && $isFeedbackAllowed && $action !== 'export') {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$marksname = 'marksName'.$questionId;
$arrmarks[] = $questionId;

Expand Down Expand Up @@ -846,7 +847,7 @@ class="exercise_mark_select"
}
}

if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
$scoreToReview = [
'score' => $my_total_score,
'comments' => isset($comnt) ? $comnt : null,
Expand Down
6 changes: 6 additions & 0 deletions main/exercise/question.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ abstract class Question
UPLOAD_ANSWER => ['UploadAnswer.php', 'UploadAnswer'],
MULTIPLE_ANSWER_DROPDOWN => ['MultipleAnswerDropdown.php', 'MultipleAnswerDropdown'],
MULTIPLE_ANSWER_DROPDOWN_COMBINATION => ['MultipleAnswerDropdownCombination.php', 'MultipleAnswerDropdownCombination'],
ANSWER_IN_OFFICE_DOC => ['AnswerInOfficeDoc.php', 'AnswerInOfficeDoc'],
];

/**
Expand Down Expand Up @@ -110,6 +111,7 @@ public function __construct()
FILL_IN_BLANKS,
FILL_IN_BLANKS_COMBINATION,
FREE_ANSWER,
ANSWER_IN_OFFICE_DOC,
ORAL_EXPRESSION,
CALCULATED_ANSWER,
ANNOTATION,
Expand Down Expand Up @@ -1663,6 +1665,9 @@ public static function getQuestionTypeList()
self::$questionTypes[HOT_SPOT_DELINEATION] = null;
unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
}
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
unset(self::$questionTypes[ANSWER_IN_OFFICE_DOC]);
}

return self::$questionTypes;
}
Expand Down Expand Up @@ -2248,6 +2253,7 @@ public function return_header(Exercise $exercise, $counter = null, $score = [])
case FREE_ANSWER:
case UPLOAD_ANSWER:
case ORAL_EXPRESSION:
case ANSWER_IN_OFFICE_DOC:
case ANNOTATION:
$score['revised'] = isset($score['revised']) ? $score['revised'] : false;
if ($score['revised'] == true) {
Expand Down
2 changes: 2 additions & 0 deletions main/inc/lib/api.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@
define('FILL_IN_BLANKS_COMBINATION', 27);
define('MULTIPLE_ANSWER_DROPDOWN_COMBINATION', 28);
define('MULTIPLE_ANSWER_DROPDOWN', 29);
define('ANSWER_IN_OFFICE_DOC', 30);

define('EXERCISE_CATEGORY_RANDOM_SHUFFLED', 1);
define('EXERCISE_CATEGORY_RANDOM_ORDERED', 2);
Expand Down Expand Up @@ -591,6 +592,7 @@
MULTIPLE_ANSWER_TRUE_FALSE.':'.
MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE.':'.
ORAL_EXPRESSION.':'.
ANSWER_IN_OFFICE_DOC.':'.
GLOBAL_MULTIPLE_ANSWER.':'.
MEDIA_QUESTION.':'.
CALCULATED_ANSWER.':'.
Expand Down
Loading
Loading