Skip to content

Commit b9d922e

Browse files
committed
wip
1 parent a0b4b8d commit b9d922e

File tree

5 files changed

+361
-9
lines changed

5 files changed

+361
-9
lines changed

classes/constants.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ class constants {
4747
public const FILEAREA_RESPONSE_FILES = 'response_files';
4848
/** @var string */
4949
public const QT_VAR_RESPONSE_FILES = 'files';
50-
50+
/** @var string */
51+
public const QT_VAR_EDITORS = 'editors';
5152

5253
/** @var string */
5354
public const FILEAREA_OPTIONS = 'options';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
// This file is part of the QuestionPy Moodle plugin - https://questionpy.org
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace qtype_questionpy\local\attempt_ui;
18+
19+
20+
use core\exception\coding_exception;
21+
use DOMElement;
22+
use DOMNode;
23+
use file_exception;
24+
use moodle_exception;
25+
use question_attempt;
26+
use stored_file_creation_exception;
27+
28+
/**
29+
* Represents a `<qpy:X/>` element in the question UI XML.
30+
*
31+
* @package qtype_questionpy
32+
* @author Maximilian Haye
33+
* @copyright 2025 TU Berlin, innoCampus {@link https://www.questionpy.org}
34+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35+
*/
36+
interface custom_xhtml_element {
37+
/**
38+
* Parses the given DOMElement if possible.
39+
*
40+
* @param DOMElement $element
41+
* @return static|null
42+
*/
43+
public static function from_element(DOMElement $element): ?static;
44+
45+
/**
46+
* Renders this element to a DOMNode.
47+
*
48+
* @param question_attempt $qa
49+
* @param question_ui_renderer $renderer
50+
* @return DOMNode
51+
* @throws coding_exception
52+
* @throws file_exception
53+
* @throws moodle_exception
54+
* @throws stored_file_creation_exception
55+
*/
56+
public function render(question_attempt $qa, question_ui_renderer $renderer): DOMNode;
57+
}

classes/local/attempt_ui/qpy_file_upload.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* @copyright 2025 TU Berlin, innoCampus {@link https://www.questionpy.org}
4040
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4141
*/
42-
class qpy_file_upload {
42+
class qpy_file_upload implements custom_xhtml_element {
4343
/**
4444
* Trivial private constructor. Use {@see from_element()}.
4545
* @param DOMElement $element
@@ -81,12 +81,12 @@ public function get_limits_in(context $context): validatable_upload_limits {
8181
}
8282

8383
/**
84-
* Creates a new {@see qpy_file_upload} from a given {@see DOMElement}.
84+
* Parses the given DOMElement if possible.
8585
*
8686
* @param DOMElement $element
87-
* @return self|null
87+
* @return static|null
8888
*/
89-
public static function from_element(DOMElement $element): ?self {
89+
public static function from_element(DOMElement $element): ?static {
9090
$name = $element->getAttribute('name');
9191
if (!$name) {
9292
debugging('qpy:file-upload without a name');
@@ -102,10 +102,6 @@ public static function from_element(DOMElement $element): ?self {
102102
* @param question_attempt $qa
103103
* @param question_ui_renderer $renderer
104104
* @return DOMNode
105-
* @throws coding_exception
106-
* @throws file_exception
107-
* @throws moodle_exception
108-
* @throws stored_file_creation_exception
109105
*/
110106
public function render(question_attempt $qa, question_ui_renderer $renderer): DOMNode {
111107
if ($renderer->options->readonly) {
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<?php
2+
// This file is part of the QuestionPy Moodle plugin - https://questionpy.org
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
namespace qtype_questionpy\local\attempt_ui;
18+
19+
use core\context;
20+
use core\di;
21+
use core\exception\coding_exception;
22+
use core_renderer;
23+
use DOMDocument;
24+
use DOMElement;
25+
use DOMNode;
26+
use file_exception;
27+
use form_filemanager;
28+
use moodle_exception;
29+
use MoodleQuickForm_editor;
30+
use qtype_questionpy\local\files\response_file_service;
31+
use qtype_questionpy\local\files\validatable_upload_limits;
32+
use qtype_questionpy\utils;
33+
use qtype_questionpy_renderer;
34+
use question_attempt;
35+
use stdClass;
36+
use stored_file_creation_exception;
37+
38+
defined('MOODLE_INTERNAL') || die();
39+
40+
global $CFG;
41+
require_once($CFG->libdir . '/form/editor.php');
42+
43+
/**
44+
* Represents a `<qpy:rich-text-editor/>` element in the question UI XML.
45+
*
46+
* @package qtype_questionpy
47+
* @author Maximilian Haye
48+
* @copyright 2025 TU Berlin, innoCampus {@link https://www.questionpy.org}
49+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
50+
*/
51+
class qpy_rich_text_editor implements custom_xhtml_element {
52+
// The default in MoodleQuickForm_editor.
53+
/** @var int */
54+
private const RETURN_TYPES = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK;
55+
56+
/**
57+
* Trivial private constructor. Use {@see from_element()}.
58+
* @param DOMElement $element
59+
* @param string $name
60+
*/
61+
private function __construct(
62+
/** @var DOMElement */
63+
private readonly DOMElement $element,
64+
/** @var string */
65+
public readonly string $name,
66+
/** @var bool */
67+
public readonly bool $required,
68+
) {
69+
}
70+
71+
/**
72+
* Gets the limits that should be validated for the current user in the given context when using this upload field.
73+
*
74+
* @param context $context
75+
* @return validatable_upload_limits
76+
*/
77+
public function get_limits_in(context $context): validatable_upload_limits {
78+
global $CFG, $PAGE;
79+
require_once($CFG->libdir . '/formslib.php'); // For EDITOR_UNLIMITED_FILES.
80+
81+
$maxfiles = $this->element->getAttribute('max-files');
82+
$maxfiles = is_numeric($maxfiles) ? intval($maxfiles) : EDITOR_UNLIMITED_FILES;
83+
84+
$maxbytes = $this->element->getAttribute('max-bytes-per-file');
85+
$maxbytes = is_numeric($maxbytes) ? intval($maxbytes) : FILE_AREA_MAX_BYTES_UNLIMITED;
86+
$coursemaxbytes = 0;
87+
if (!empty($PAGE->course->maxbytes)) {
88+
$coursemaxbytes = $PAGE->course->maxbytes;
89+
}
90+
$maxbytes = get_user_max_upload_file_size($context, $CFG->maxbytes, $coursemaxbytes, $maxbytes);
91+
92+
$areamaxbytes = $this->element->getAttribute('max-bytes-total');
93+
$areamaxbytes = is_numeric($areamaxbytes) ? intval($areamaxbytes) : FILE_AREA_MAX_BYTES_UNLIMITED;
94+
95+
return new validatable_upload_limits($maxfiles, $maxbytes, $areamaxbytes);
96+
}
97+
98+
/**
99+
* Parses the given DOMElement if possible.
100+
*
101+
* @param DOMElement $element
102+
* @return static|null
103+
*/
104+
public static function from_element(DOMElement $element): ?static {
105+
$name = $element->getAttribute('name');
106+
if (!$name) {
107+
debugging('qpy:file-upload without a name');
108+
return null;
109+
}
110+
111+
$required = $element->hasAttribute('required');
112+
113+
return new static($element, $name, $required);
114+
}
115+
116+
/**
117+
* Renders this element to a DOMNode.
118+
*
119+
* @param question_attempt $qa
120+
* @param question_ui_renderer $renderer
121+
* @return DOMNode
122+
* @throws coding_exception
123+
* @throws file_exception
124+
* @throws moodle_exception
125+
* @throws stored_file_creation_exception
126+
*/
127+
public function render(question_attempt $qa, question_ui_renderer $renderer): DOMNode {
128+
$id = uniqid('qpy-editor-');
129+
130+
$editor = editors_get_preferred_editor();
131+
132+
$fragment = $this->element->ownerDocument->createDocumentFragment();
133+
134+
$textarea = $this->element->ownerDocument->createElement('textarea');
135+
$textarea->setAttribute('name', $this->name);
136+
$textarea->setAttribute('id', $id);
137+
$textarea->setAttribute('rows', 15);
138+
$textarea->setAttribute('cols', 80);
139+
$fragment->append($textarea);
140+
141+
$limits = $this->get_limits_in($renderer->options->context);
142+
143+
$fpoptions = [];
144+
if ($limits->maxfiles !== 0) {
145+
$combinedfilearea = $renderer->prepare_combined_draft_area($qa);
146+
$rfs = di::get(response_file_service::class);
147+
global $USER;
148+
$splitdraftitemid = $rfs->prepare_split_draft_area($this->name, $USER->id, $combinedfilearea);
149+
150+
// This is used to tell the qbehaviour what draft areas to save.
151+
$renderer->draftareas[$this->name] = $splitdraftitemid;
152+
153+
$fpoptions = $this->get_fpoptions($limits, $splitdraftitemid, $renderer->options->context);
154+
}
155+
156+
// $uploadsoptions = match ($limits->maxfiles) {
157+
// 0 => [
158+
// 'enable_filemanagement' => false,
159+
// ],
160+
// default => [
161+
// 'subdirs' => false,
162+
// 'maxfiles' => $limits->maxbytes,
163+
// 'maxbytes' => $limits->maxbytes,
164+
// 'areamaxbytes' => $limits->areamaxbytes,
165+
// ]
166+
// };
167+
//
168+
// $meditor = new MoodleQuickForm_editor(
169+
// elementName: $this->name,
170+
// elementLabel: null,
171+
// attributes: [
172+
// 'data-qpy_editor' => 'true',
173+
// ],
174+
// options: [
175+
// 'context' => $renderer->options->context,
176+
// ...$uploadsoptions,
177+
// ]
178+
// );
179+
// $meditor->_generateId();
180+
//
181+
// $meditor->setValue([
182+
// 'text' => '',
183+
// 'format' => 1,
184+
// 'itemid' => $splitdraftitemid,
185+
// ]);
186+
187+
188+
$editor->use_editor($id, [
189+
'context' => $renderer->options->context,
190+
'enable_filemanagement' => $limits->maxfiles !== 0,
191+
'required' => $this->required,
192+
'maxfiles' => $limits->maxfiles,
193+
'subdirs' => false,
194+
'maxbytes' => $limits->maxbytes,
195+
'areamaxbytes' => $limits->areamaxbytes,
196+
'return_types' => self::RETURN_TYPES,
197+
], $fpoptions);
198+
199+
// return dom_utils::html_to_fragment($this->element->ownerDocument, $meditor->toHtml());
200+
return $fragment;
201+
}
202+
203+
/**
204+
* @param validatable_upload_limits $limits
205+
* @param int $draftitemid
206+
* @param context $context
207+
* @return array
208+
* @throws coding_exception
209+
*/
210+
private function get_fpoptions(validatable_upload_limits $limits, int $draftitemid, context $context): array {
211+
// Mostly stolen from MoodleQuickForm_editor.
212+
213+
if ($limits->maxfiles == 0) {
214+
return [];
215+
}
216+
217+
$fpoptions = [];
218+
219+
$args = new stdClass();
220+
// Need these three to filter repositories list.
221+
$args->accepted_types = ['web_image'];
222+
$args->return_types = self::RETURN_TYPES;
223+
$args->context = $context;
224+
$args->env = 'filepicker';
225+
226+
// Advimage plugin.
227+
$imageoptions = initialise_filepicker($args);
228+
$imageoptions->context = $context;
229+
$imageoptions->client_id = uniqid();
230+
$imageoptions->maxbytes = $limits->maxbytes;
231+
$imageoptions->areamaxbytes = $limits->areamaxbytes;
232+
$imageoptions->env = 'editor';
233+
$imageoptions->itemid = $draftitemid;
234+
235+
// Moodlemedia plugin.
236+
$args->accepted_types = ['video', 'audio'];
237+
$mediaoptions = initialise_filepicker($args);
238+
$mediaoptions->context = $context;
239+
$mediaoptions->client_id = uniqid();
240+
$mediaoptions->maxbytes = $limits->maxbytes;
241+
$mediaoptions->areamaxbytes = $limits->areamaxbytes;
242+
$mediaoptions->env = 'editor';
243+
$mediaoptions->itemid = $draftitemid;
244+
245+
// Advlink plugin.
246+
$args->accepted_types = '*';
247+
$linkoptions = initialise_filepicker($args);
248+
$linkoptions->context = $context;
249+
$linkoptions->client_id = uniqid();
250+
$linkoptions->maxbytes = $limits->maxbytes;
251+
$linkoptions->areamaxbytes = $limits->areamaxbytes;
252+
$linkoptions->env = 'editor';
253+
$linkoptions->itemid = $draftitemid;
254+
255+
// Subtitles, for multimedia.
256+
$args->accepted_types = ['.vtt'];
257+
$subtitleoptions = initialise_filepicker($args);
258+
$subtitleoptions->context = $context;
259+
$subtitleoptions->client_id = uniqid();
260+
$subtitleoptions->maxbytes = $limits->maxbytes;
261+
$subtitleoptions->areamaxbytes = $limits->areamaxbytes;
262+
$subtitleoptions->env = 'editor';
263+
$subtitleoptions->itemid = $draftitemid;
264+
265+
if (has_capability('moodle/h5p:deploy', $context)) {
266+
// Only set H5P Plugin settings if the user can deploy new H5P content.
267+
// H5P plugin.
268+
$args->accepted_types = ['.h5p'];
269+
$h5poptions = initialise_filepicker($args);
270+
$h5poptions->context = $context;
271+
$h5poptions->client_id = uniqid();
272+
$h5poptions->maxbytes = $limits->maxbytes;
273+
$h5poptions->areamaxbytes = $limits->areamaxbytes;
274+
$h5poptions->env = 'editor';
275+
$h5poptions->itemid = $draftitemid;
276+
$fpoptions['h5p'] = $h5poptions;
277+
}
278+
279+
$fpoptions['image'] = $imageoptions;
280+
$fpoptions['media'] = $mediaoptions;
281+
$fpoptions['link'] = $linkoptions;
282+
$fpoptions['subtitle'] = $subtitleoptions;
283+
284+
return $fpoptions;
285+
}
286+
}

0 commit comments

Comments
 (0)