Skip to content

Commit 6673eb9

Browse files
authored
Merge pull request #6121 from christianbeeznest/efc-22370
Exercise: Add OnlyOffice question type with document editing support - refs BT#22370
2 parents 680d765 + 4b377c5 commit 6673eb9

13 files changed

+639
-114
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
/**
6+
* Class AnswerInOfficeDoc
7+
* Allows a question type where the answer is written in an Office document.
8+
*
9+
* @author Cristian
10+
*/
11+
class AnswerInOfficeDoc extends Question
12+
{
13+
public $typePicture = 'options_evaluation.png';
14+
public $explanationLangVar = 'AnswerInOfficeDoc';
15+
public $sessionId;
16+
public $userId;
17+
public $exerciseId;
18+
public $exeId;
19+
private $storePath;
20+
private $fileName;
21+
private $filePath;
22+
23+
/**
24+
* Constructor.
25+
*/
26+
public function __construct()
27+
{
28+
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
29+
throw new Exception(get_lang('OnlyOfficePluginRequired'));
30+
}
31+
32+
parent::__construct();
33+
$this->type = ANSWER_IN_OFFICE_DOC;
34+
$this->isContent = $this->getIsContent();
35+
}
36+
37+
/**
38+
* Initialize the file path structure.
39+
*/
40+
public function initFile(int $sessionId, int $userId, int $exerciseId, int $exeId): void
41+
{
42+
$this->sessionId = $sessionId ?: 0;
43+
$this->userId = $userId;
44+
$this->exerciseId = $exerciseId ?: 0;
45+
$this->exeId = $exeId;
46+
47+
$this->storePath = $this->generateDirectory();
48+
$this->fileName = $this->generateFileName();
49+
$this->filePath = $this->storePath . $this->fileName;
50+
}
51+
52+
/**
53+
* Create form for uploading an Office document.
54+
*/
55+
public function createAnswersForm($form): void
56+
{
57+
if (!empty($this->exerciseList)) {
58+
$this->exerciseId = reset($this->exerciseList);
59+
}
60+
61+
$allowedFormats = [
62+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
63+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
64+
'application/msword', // .doc
65+
'application/vnd.ms-excel' // .xls
66+
];
67+
68+
$form->addElement('file', 'office_file', get_lang('UploadOfficeDoc'));
69+
$form->addRule('office_file', get_lang('ThisFieldIsRequired'), 'required');
70+
$form->addRule('office_file', get_lang('InvalidFileFormat'), 'mimetype', $allowedFormats);
71+
72+
$allowedExtensions = implode(', ', ['.docx', '.xlsx', '.doc', '.xls']);
73+
$form->addElement('static', 'file_hint', get_lang('AllowedFormats'), "<p>{$allowedExtensions}</p>");
74+
75+
if (!empty($this->extra)) {
76+
$fileUrl = api_get_path(WEB_COURSE_PATH) . $this->getStoredFilePath();
77+
$form->addElement('static', 'current_office_file', get_lang('CurrentOfficeDoc'), "<a href='{$fileUrl}' target='_blank'>{$this->extra}</a>");
78+
}
79+
80+
$form->addText('weighting', get_lang('Weighting'), ['class' => 'span1']);
81+
82+
global $text;
83+
$form->addButtonSave($text, 'submitQuestion');
84+
85+
if (!empty($this->iid)) {
86+
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
87+
} else {
88+
if ($this->isContent == 1) {
89+
$form->setDefaults(['weighting' => '10']);
90+
}
91+
}
92+
}
93+
94+
/**
95+
* Process the uploaded document and save it.
96+
*/
97+
public function processAnswersCreation($form, $exercise): void
98+
{
99+
if (!empty($_FILES['office_file']['name'])) {
100+
$extension = pathinfo($_FILES['office_file']['name'], PATHINFO_EXTENSION);
101+
$tempFilename = "office_" . uniqid() . "." . $extension;
102+
$tempPath = sys_get_temp_dir() . '/' . $tempFilename;
103+
104+
if (!move_uploaded_file($_FILES['office_file']['tmp_name'], $tempPath)) {
105+
return;
106+
}
107+
108+
$this->weighting = $form->getSubmitValue('weighting');
109+
$this->extra = "";
110+
$this->save($exercise);
111+
112+
$this->exerciseId = $exercise->iid;
113+
$uploadDir = $this->generateDirectory();
114+
115+
if (!is_dir($uploadDir)) {
116+
mkdir($uploadDir, 0775, true);
117+
}
118+
119+
$filename = "office_".$this->iid.".".$extension;
120+
$filePath = $uploadDir . $filename;
121+
122+
if (!rename($tempPath, $filePath)) {
123+
return;
124+
}
125+
126+
$this->extra = $filename;
127+
$this->save($exercise);
128+
}
129+
}
130+
131+
/**
132+
* Generate the necessary directory for OnlyOffice documents.
133+
*/
134+
private function generateDirectory(): string
135+
{
136+
$exercisePath = api_get_path(SYS_COURSE_PATH).$this->course['path']."/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/";
137+
138+
if (!is_dir($exercisePath)) {
139+
mkdir($exercisePath, 0775, true);
140+
}
141+
142+
return rtrim($exercisePath, '/') . '/';
143+
}
144+
145+
/**
146+
* Get the stored file path dynamically.
147+
*/
148+
public function getStoredFilePath(): ?string
149+
{
150+
if (empty($this->extra)) {
151+
return null;
152+
}
153+
154+
return "{$this->course['path']}/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/{$this->extra}";
155+
}
156+
157+
/**
158+
* Get the absolute file path. Returns null if the file doesn't exist.
159+
*/
160+
public function getFileUrl(bool $loadFromDatabase = false): ?string
161+
{
162+
if ($loadFromDatabase) {
163+
$em = Database::getManager();
164+
$result = $em->getRepository('ChamiloCoreBundle:TrackEAttempt')->findOneBy([
165+
'exeId' => $this->exeId,
166+
'userId' => $this->userId,
167+
'questionId' => $this->iid,
168+
'sessionId' => $this->sessionId,
169+
'cId' => $this->course['real_id'],
170+
]);
171+
172+
if (!$result || empty($result->getFilename())) {
173+
return null;
174+
}
175+
176+
$this->fileName = $result->getFilename();
177+
} else {
178+
if (empty($this->extra)) {
179+
return null;
180+
}
181+
182+
$this->fileName = $this->extra;
183+
}
184+
185+
$filePath = $this->getStoredFilePath();
186+
187+
if (is_file(api_get_path(SYS_COURSE_PATH) . $filePath)) {
188+
return $filePath;
189+
}
190+
191+
return null;
192+
}
193+
194+
/**
195+
* Show the question in an exercise.
196+
*/
197+
public function return_header(Exercise $exercise, $counter = null, $score = [])
198+
{
199+
$score['revised'] = $this->isQuestionWaitingReview($score);
200+
$header = parent::return_header($exercise, $counter, $score);
201+
$header .= '<table class="'.$this->question_table_class.'">
202+
<tr>
203+
<th>'.get_lang("Answer").'</th>
204+
</tr>';
205+
206+
return $header;
207+
}
208+
209+
/**
210+
* Generate the file name for the OnlyOffice document.
211+
*/
212+
private function generateFileName(): string
213+
{
214+
return 'office_' . uniqid();
215+
}
216+
}

main/exercise/exercise.class.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3958,7 +3958,8 @@ public function manage_answer(
39583958
$answerType == ORAL_EXPRESSION ||
39593959
$answerType == CALCULATED_ANSWER ||
39603960
$answerType == ANNOTATION ||
3961-
$answerType == UPLOAD_ANSWER
3961+
$answerType == UPLOAD_ANSWER ||
3962+
$answerType == ANSWER_IN_OFFICE_DOC
39623963
) {
39633964
$nbrAnswers = 1;
39643965
}
@@ -4762,6 +4763,7 @@ function ($answerId) use ($objAnswerTmp) {
47624763
break;
47634764
case UPLOAD_ANSWER:
47644765
case FREE_ANSWER:
4766+
case ANSWER_IN_OFFICE_DOC:
47654767
if ($from_database) {
47664768
$sql = "SELECT answer, marks FROM $TBL_TRACK_ATTEMPT
47674769
WHERE
@@ -5423,6 +5425,18 @@ function ($answerId) use ($objAnswerTmp) {
54235425
$questionScore,
54245426
$results_disabled
54255427
);
5428+
} elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
5429+
$exe_info = Event::get_exercise_results_by_attempt($exeId);
5430+
$exe_info = $exe_info[$exeId] ?? null;
5431+
ExerciseShowFunctions::displayOnlyOfficeAnswer(
5432+
$feedback_type,
5433+
$exeId,
5434+
$exe_info['exe_user_id'] ?? api_get_user_id(),
5435+
$this->iid,
5436+
$questionId,
5437+
$questionScore,
5438+
true
5439+
);
54265440
} elseif ($answerType == ORAL_EXPRESSION) {
54275441
// to store the details of open questions in an array to be used in mail
54285442
/** @var OralExpression $objQuestionTmp */
@@ -5821,6 +5835,18 @@ function ($answerId) use ($objAnswerTmp) {
58215835
$results_disabled
58225836
);
58235837
break;
5838+
case ANSWER_IN_OFFICE_DOC:
5839+
$exe_info = Event::get_exercise_results_by_attempt($exeId);
5840+
$exe_info = $exe_info[$exeId] ?? null;
5841+
ExerciseShowFunctions::displayOnlyOfficeAnswer(
5842+
$feedback_type,
5843+
$exeId,
5844+
$exe_info['exe_user_id'] ?? api_get_user_id(),
5845+
$this->iid,
5846+
$questionId,
5847+
$questionScore
5848+
);
5849+
break;
58245850
case ORAL_EXPRESSION:
58255851
echo '<tr>
58265852
<td>'.
@@ -6463,6 +6489,24 @@ function ($answerId) use ($objAnswerTmp) {
64636489
false,
64646490
$questionDuration
64656491
);
6492+
} elseif ($answerType == ANSWER_IN_OFFICE_DOC) {
6493+
$answer = $choice;
6494+
$exerciseId = $this->iid;
6495+
$questionId = $quesId;
6496+
$originalFilePath = $objQuestionTmp->getFileUrl();
6497+
$originalExtension = !empty($originalFilePath) ? pathinfo($originalFilePath, PATHINFO_EXTENSION) : 'docx';
6498+
$fileName = "response_{$exeId}.{$originalExtension}";
6499+
Event::saveQuestionAttempt(
6500+
$questionScore,
6501+
$answer,
6502+
$questionId,
6503+
$exeId,
6504+
0,
6505+
$exerciseId,
6506+
false,
6507+
$questionDuration,
6508+
$fileName
6509+
);
64666510
} elseif ($answerType == ORAL_EXPRESSION) {
64676511
$answer = $choice;
64686512
$absFilePath = $objQuestionTmp->getAbsoluteFilePath();

main/exercise/exercise_report.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@
211211

212212
// From the database.
213213
$marksFromDatabase = $questionListData[$questionId]['marks'];
214-
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
214+
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
215215
// From the form.
216216
$params['marks'] = $marks;
217217
if ($marksFromDatabase != $marks) {

main/exercise/exercise_show.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ function getFCK(vals, marksid) {
444444
case GLOBAL_MULTIPLE_ANSWER:
445445
case FREE_ANSWER:
446446
case UPLOAD_ANSWER:
447+
case ANSWER_IN_OFFICE_DOC:
447448
case ORAL_EXPRESSION:
448449
case MATCHING:
449450
case MATCHING_COMBINATION:
@@ -612,7 +613,7 @@ function getFCK(vals, marksid) {
612613
if ($isFeedbackAllowed && $action !== 'export') {
613614
$name = 'fckdiv'.$questionId;
614615
$marksname = 'marksName'.$questionId;
615-
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
616+
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
616617
$url_name = get_lang('EditCommentsAndMarks');
617618
} else {
618619
$url_name = get_lang('AddComments');
@@ -689,7 +690,7 @@ function getFCK(vals, marksid) {
689690
}
690691

691692
if ($is_allowedToEdit && $isFeedbackAllowed && $action !== 'export') {
692-
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
693+
if (in_array($answerType, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
693694
$marksname = 'marksName'.$questionId;
694695
$arrmarks[] = $questionId;
695696

@@ -846,7 +847,7 @@ class="exercise_mark_select"
846847
}
847848
}
848849

849-
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER])) {
850+
if (in_array($objQuestionTmp->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
850851
$scoreToReview = [
851852
'score' => $my_total_score,
852853
'comments' => isset($comnt) ? $comnt : null,

main/exercise/question.class.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ abstract class Question
7575
UPLOAD_ANSWER => ['UploadAnswer.php', 'UploadAnswer'],
7676
MULTIPLE_ANSWER_DROPDOWN => ['MultipleAnswerDropdown.php', 'MultipleAnswerDropdown'],
7777
MULTIPLE_ANSWER_DROPDOWN_COMBINATION => ['MultipleAnswerDropdownCombination.php', 'MultipleAnswerDropdownCombination'],
78+
ANSWER_IN_OFFICE_DOC => ['AnswerInOfficeDoc.php', 'AnswerInOfficeDoc'],
7879
];
7980

8081
/**
@@ -110,6 +111,7 @@ public function __construct()
110111
FILL_IN_BLANKS,
111112
FILL_IN_BLANKS_COMBINATION,
112113
FREE_ANSWER,
114+
ANSWER_IN_OFFICE_DOC,
113115
ORAL_EXPRESSION,
114116
CALCULATED_ANSWER,
115117
ANNOTATION,
@@ -1663,6 +1665,9 @@ public static function getQuestionTypeList()
16631665
self::$questionTypes[HOT_SPOT_DELINEATION] = null;
16641666
unset(self::$questionTypes[HOT_SPOT_DELINEATION]);
16651667
}
1668+
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
1669+
unset(self::$questionTypes[ANSWER_IN_OFFICE_DOC]);
1670+
}
16661671

16671672
return self::$questionTypes;
16681673
}
@@ -2248,6 +2253,7 @@ public function return_header(Exercise $exercise, $counter = null, $score = [])
22482253
case FREE_ANSWER:
22492254
case UPLOAD_ANSWER:
22502255
case ORAL_EXPRESSION:
2256+
case ANSWER_IN_OFFICE_DOC:
22512257
case ANNOTATION:
22522258
$score['revised'] = isset($score['revised']) ? $score['revised'] : false;
22532259
if ($score['revised'] == true) {

main/inc/lib/api.lib.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@
544544
define('FILL_IN_BLANKS_COMBINATION', 27);
545545
define('MULTIPLE_ANSWER_DROPDOWN_COMBINATION', 28);
546546
define('MULTIPLE_ANSWER_DROPDOWN', 29);
547+
define('ANSWER_IN_OFFICE_DOC', 30);
547548

548549
define('EXERCISE_CATEGORY_RANDOM_SHUFFLED', 1);
549550
define('EXERCISE_CATEGORY_RANDOM_ORDERED', 2);
@@ -591,6 +592,7 @@
591592
MULTIPLE_ANSWER_TRUE_FALSE.':'.
592593
MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE.':'.
593594
ORAL_EXPRESSION.':'.
595+
ANSWER_IN_OFFICE_DOC.':'.
594596
GLOBAL_MULTIPLE_ANSWER.':'.
595597
MEDIA_QUESTION.':'.
596598
CALCULATED_ANSWER.':'.

0 commit comments

Comments
 (0)