Skip to content

Commit cd334d5

Browse files
authored
Merge pull request #129 from catalyst/issue66-add-searchapi
Issue #66: Add search API
2 parents ba37892 + 6eace55 commit cd334d5

File tree

6 files changed

+653
-4
lines changed

6 files changed

+653
-4
lines changed

README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@
66

77
An activity module for managing custom defined content types which are 'first class' concepts in Moodle. This is to enable course author to think in proper concepts that matter to them and not worry about the rendering of each content type which will be defined centrally.
88

9+
## Branches ##
10+
The following maps the plugin version to use depending on your Moodle version.
11+
12+
| Moodle version | Branch |
13+
|-------------------| ------------------|
14+
| Moodle 3.9 to 4.0 | MOODLE_39_STABLE |
15+
| Moodle 4.1 | MOODLE_401_STABLE |
16+
917
## Installation
1018

1119
Step 1: Install the activity module
1220
-----------------------------------
1321

1422
Using git submodule:
1523
```
16-
git submodule add [email protected]:catalyst/moodle-mod_cms.git mod/cms
24+
git submodule add -b MOODLE_401_STABLE [email protected]:catalyst/moodle-mod_cms.git mod/cms
1725
```
1826

1927
Or you can download as a zip from github

classes/search/cmsfield.php

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.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 mod_cms\search;
18+
19+
defined('MOODLE_INTERNAL') || die();
20+
21+
require_once($CFG->dirroot . '/mod/cms/lib.php');
22+
23+
/**
24+
* Define search area.
25+
*
26+
* @package mod_cms
27+
* @author Tomo Tsuyuki <[email protected]>
28+
* @copyright 2024 Catalyst IT
29+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30+
*/
31+
class cmsfield extends \core_search\base_mod {
32+
33+
/**
34+
* @var array Internal quick static cache.
35+
*/
36+
protected $cmsdata = [];
37+
38+
/**
39+
* @var array Internal quick static cache.
40+
*/
41+
protected $defaultvalues = null;
42+
43+
/**
44+
* Returns recordset containing required data for indexing cmsfield records.
45+
*
46+
* @param int $modifiedfrom timestamp
47+
* @param \context|null $context Optional context to restrict scope of returned results
48+
* @return \moodle_recordset|null Recordset (or null if no results)
49+
*/
50+
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
51+
global $DB;
52+
53+
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql(
54+
$context, 'cms', 'mc');
55+
if ($contextjoin === null) {
56+
return null;
57+
}
58+
59+
// Search area is from customfield_data, but if the record is missing from activity, use default value.
60+
$sqlgroupconcat = $DB->sql_group_concat("mcd.value", ', ', 'mcf.sortorder');
61+
$sql = "SELECT ccms.id, ccms.course AS courseid, ccms.typeid, cmcf.name AS fieldname, cmcf.type,
62+
cdata.dataid dataid, cdata.value AS value, cdata.valueformat AS valueformat,
63+
cdata.timecreated AS timecreated, cdata.timemodified AS timemodified
64+
FROM {cms} ccms
65+
JOIN (
66+
SELECT mc.id, MAX(mcf.id) AS fieldid,
67+
MAX(mcd.id) dataid, {$sqlgroupconcat} AS value, MAX(mcd.valueformat) AS valueformat,
68+
MAX(mcd.timecreated) AS timecreated, MAX(mcd.timemodified) AS timemodified
69+
FROM {cms} mc
70+
JOIN {customfield_data} mcd ON mc.id = mcd.instanceid
71+
JOIN {customfield_field} mcf ON mcf.id = mcd.fieldid
72+
JOIN {customfield_category} mcc ON mcf.categoryid = mcc.id
73+
$contextjoin
74+
WHERE mcd.timemodified >= ? AND mcc.component = 'mod_cms' AND mcc.area = 'cmsfield'
75+
AND mcf.type IN ('textarea', 'text')
76+
GROUP BY mc.id
77+
) cdata ON ccms.id = cdata.id
78+
JOIN {customfield_field} cmcf ON cmcf.id = cdata.fieldid
79+
UNION
80+
SELECT mc.id, mc.course AS courseid, mc.typeid, null AS fieldname, null AS type,
81+
null AS dataid, null AS value, null AS valueformat,
82+
mc.timecreated timecreated, mc.timemodified timemodified
83+
FROM {cms} mc
84+
LEFT JOIN {customfield_data} mcd ON mc.id = mcd.instanceid
85+
WHERE mcd.id IS NULL AND mc.timecreated >= ?
86+
ORDER BY timemodified ASC";
87+
return $DB->get_recordset_sql($sql, array_merge($contextparams, [$modifiedfrom, $modifiedfrom]));
88+
}
89+
90+
/**
91+
* Returns the document associated with this data id.
92+
*
93+
* @param stdClass $record
94+
* @param array $options
95+
* @return \core_search\document
96+
*/
97+
public function get_document($record, $options = []) {
98+
global $DB;
99+
try {
100+
$cm = $this->get_cm('cms', $record->id, $record->courseid);
101+
$context = \context_module::instance($cm->id);
102+
} catch (\dml_missing_record_exception $ex) {
103+
// Notify it as we run here as admin, we should see everything.
104+
debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
105+
$ex->getMessage(), DEBUG_DEVELOPER);
106+
return false;
107+
} catch (\dml_exception $ex) {
108+
// Notify it as we run here as admin, we should see everything.
109+
debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER);
110+
return false;
111+
}
112+
113+
$defaultvalues = $this->get_default_values();
114+
115+
// Check if it's default value or not.
116+
if (empty($record->dataid)) {
117+
$title = $defaultvalues[$record->typeid]->fieldname ?? '';
118+
$value = $defaultvalues[$record->typeid]->value ?? '';
119+
if (isset($defaultvalues[$record->typeid]->valueformat)) {
120+
$valueformat = $defaultvalues[$record->typeid]->valueformat;
121+
} else {
122+
if ($record->type == 'textarea') {
123+
$valueformat = FORMAT_HTML;
124+
} else {
125+
$valueformat = FORMAT_PLAIN;
126+
}
127+
}
128+
} else {
129+
$title = $record->fieldname;
130+
$value = $record->value;
131+
$valueformat = $record->valueformat;
132+
}
133+
134+
// Prepare associative array with data from DB.
135+
$doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
136+
$doc->set('title', content_to_text($title, false));
137+
$doc->set('content', content_to_text($value, $valueformat));
138+
$doc->set('contextid', $context->id);
139+
$doc->set('courseid', $record->courseid);
140+
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
141+
$doc->set('modified', $record->timemodified);
142+
143+
// Check if this document should be considered new.
144+
if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $record->timecreated)) {
145+
// If the document was created after the last index time, it must be new.
146+
$doc->set_is_new(true);
147+
}
148+
149+
return $doc;
150+
}
151+
152+
/**
153+
* Get default value for cms custom field.
154+
*
155+
* @return array
156+
*/
157+
protected function get_default_values() {
158+
global $DB;
159+
if (is_null($this->defaultvalues)) {
160+
$defaultvalues = [];
161+
$sql = "SELECT mcf.id fieldid, mct.id typeid, mcf.configdata, mcf.name fieldname
162+
FROM {cms_types} mct
163+
JOIN {customfield_category} mcc ON mcc.itemid = mct.id
164+
JOIN {customfield_field} mcf ON mcf.categoryid = mcc.id
165+
WHERE mcc.component = 'mod_cms' AND mcc.area = 'cmsfield' AND mcf.type IN ('textarea', 'text')
166+
ORDER BY mct.id, mcf.sortorder";
167+
$cmstypes = $DB->get_records_sql($sql);
168+
foreach ($cmstypes as $cmstype) {
169+
if (empty($defaultvalues[$cmstype->typeid])) {
170+
$data = new \stdClass();
171+
$configdata = json_decode($cmstype->configdata);
172+
$data->value = $configdata->defaultvalue ?? 'Default value';
173+
$data->valueformat = $configdata->defaultvalueformat ?? 0;
174+
$data->fieldname = $cmstype->fieldname;
175+
} else {
176+
$data = $defaultvalues[$cmstype->typeid];
177+
$configdata = json_decode($cmstype->configdata);
178+
$data->value .= ', ' . $configdata->defaultvalue;
179+
}
180+
$defaultvalues[$cmstype->typeid] = $data;
181+
}
182+
$this->defaultvalues = $defaultvalues;
183+
}
184+
return $this->defaultvalues;
185+
}
186+
187+
/**
188+
* Whether the user can access the document or not.
189+
*
190+
* @param int $id data id
191+
* @return bool
192+
*/
193+
public function check_access($id) {
194+
try {
195+
$data = $this->get_data($id);
196+
$cminfo = $this->get_cm('cms', $data->id, $data->courseid);
197+
$context = \context_module::instance($cminfo->id);
198+
} catch (\dml_missing_record_exception $ex) {
199+
return \core_search\manager::ACCESS_DELETED;
200+
} catch (\dml_exception $ex) {
201+
return \core_search\manager::ACCESS_DENIED;
202+
}
203+
204+
// Recheck uservisible although it should have already been checked in core_search.
205+
if ($cminfo->uservisible === false) {
206+
return \core_search\manager::ACCESS_DENIED;
207+
}
208+
209+
if (!has_capability('mod/cms:view', $context)) {
210+
return \core_search\manager::ACCESS_DENIED;
211+
}
212+
213+
return \core_search\manager::ACCESS_GRANTED;
214+
}
215+
216+
/**
217+
* Link to the cms.
218+
*
219+
* @param \core_search\document $doc
220+
* @return \moodle_url
221+
*/
222+
public function get_doc_url(\core_search\document $doc) {
223+
$contextmodule = \context::instance_by_id($doc->get('contextid'));
224+
$cm = get_coursemodule_from_id('cms', $contextmodule->instanceid, $doc->get('courseid'), true);
225+
return new \moodle_url('/course/view.php', ['id' => $doc->get('courseid'), 'section' => $cm->sectionnum]);
226+
}
227+
228+
/**
229+
* Link to the cms.
230+
*
231+
* @param \core_search\document $doc
232+
* @return \moodle_url
233+
*/
234+
public function get_context_url(\core_search\document $doc) {
235+
$contextmodule = \context::instance_by_id($doc->get('contextid'));
236+
return new \moodle_url('/mod/cms/view.php', ['id' => $contextmodule->instanceid]);
237+
}
238+
239+
/**
240+
* Returns the specified data from its internal cache.
241+
*
242+
* @throws \dml_missing_record_exception
243+
* @param int $id
244+
* @return stdClass
245+
*/
246+
protected function get_data($id) {
247+
global $DB;
248+
if (empty($this->cmsdata[$id])) {
249+
$sql = "SELECT mc.id, mc.course AS courseid
250+
FROM {cms} mc
251+
WHERE mc.id = :id";
252+
$this->cmsdata[$id] = $DB->get_record_sql($sql, ['id' => $id], MUST_EXIST);
253+
}
254+
return $this->cmsdata[$id];
255+
}
256+
}

lang/en/cms.php

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@
103103
$string['error:no_instance_hash'] = 'Module {$a} has no instance hash.';
104104
$string['error:no_config_hash'] = 'Module {$a} has no config hash.';
105105

106+
// Search strings.
107+
$string['search:cmsfield'] = 'CMS';
108+
106109
// Site datasource strings.
107110
$string['site:displayname'] = 'Site Info';
108111

tests/generator/lib.php

+20
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1616

1717
use mod_cms\local\model\cms_types;
18+
use mod_cms\customfield\cmsfield_handler;
1819
use core_customfield\category_controller;
1920
use core_customfield\field_controller;
2021

@@ -47,6 +48,25 @@ public function create_instance($record = null, array $options = null) {
4748
return parent::create_instance($record, (array) $options);
4849
}
4950

51+
/**
52+
* Create new cms module instance with custom data
53+
*
54+
* @param array|stdClass $record
55+
* @param null|array $options
56+
* @return stdClass
57+
*/
58+
public function create_instance_with_data($record = null, ?array $options = null): object {
59+
$record = (object)$record;
60+
$cms = parent::create_instance($record, $options);
61+
62+
// Save customfield.
63+
$handler = cmsfield_handler::create($cms->typeid);
64+
$record->id = $cms->id;
65+
$handler->instance_form_save($record);
66+
67+
return $cms;
68+
}
69+
5070
/**
5171
* Get generator for custom fields.
5272
* @return core_customfield_generator

0 commit comments

Comments
 (0)