MDL-57455 mod_data: Implement tagging

This commit is contained in:
Andrew Hancox 2017-09-14 16:57:00 +08:00 committed by Mark Nelson
parent d8e9a23c48
commit ca5f3e0a2e
10 changed files with 407 additions and 3 deletions

View File

@ -47,6 +47,9 @@ class backup_data_activity_structure_step extends backup_activity_structure_step
'assessed', 'assesstimestart', 'assesstimefinish', 'defaultsort',
'defaultsortdir', 'editany', 'notification', 'timemodified', 'config', 'completionentries'));
$tags = new backup_nested_element('tags');
$tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
$fields = new backup_nested_element('fields');
$field = new backup_nested_element('field', array('id'), array(
@ -84,6 +87,9 @@ class backup_data_activity_structure_step extends backup_activity_structure_step
$record->add_child($ratings);
$ratings->add_child($rating);
$record->add_child($tags);
$tags->add_child($tag);
// Define sources
$data->set_source_table('data', array('id' => backup::VAR_ACTIVITYID));
@ -104,6 +110,16 @@ class backup_data_activity_structure_step extends backup_activity_structure_step
'component' => backup_helper::is_sqlparam('mod_data'),
'ratingarea' => backup_helper::is_sqlparam('entry')));
$rating->set_source_alias('rating', 'value');
$tag->set_source_sql('SELECT t.id, t.name, t.rawname
FROM {tag} t
JOIN {tag_instance} ti
ON ti.tagid = t.id
WHERE ti.itemtype = ?
AND ti.component = ?
AND ti.itemid = ?', array(
backup_helper::is_sqlparam('data_records'),
backup_helper::is_sqlparam('mod_data'),
backup::VAR_PARENTID));
}
// Define id annotations

View File

@ -42,6 +42,7 @@ class restore_data_activity_structure_step extends restore_activity_structure_st
$paths[] = new restore_path_element('data_record', '/activity/data/records/record');
$paths[] = new restore_path_element('data_content', '/activity/data/records/record/contents/content');
$paths[] = new restore_path_element('data_rating', '/activity/data/records/record/ratings/rating');
$paths[] = new restore_path_element('data_tag', '/activity/data/records/record/tags/tag');
}
// Return the paths wrapped into standard activity structure
@ -121,6 +122,25 @@ class restore_data_activity_structure_step extends restore_activity_structure_st
$this->set_mapping('data_content', $oldid, $newitemid, true); // files by this itemname
}
/**
* Add tags to restored records.
*
* @param stdClass $data Tag
*/
protected function process_data_tag($data) {
$data = (object)$data;
if (!core_tag_tag::is_enabled('mod_data', 'data_records')) { // Tags disabled in server, nothing to process.
return;
}
$tag = $data->rawname;
$itemid = $this->get_new_parentid('data_record');
$context = context_module::instance($this->task->get_moduleid());
core_tag_tag::add_item_tag('mod_data', 'data_records', $itemid, $context, $tag);
}
protected function process_data_rating($data) {
global $DB;

34
mod/data/db/tag.php Normal file
View File

@ -0,0 +1,34 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tag areas in component mod_data
*
* @package mod_data
* @copyright 2017 Andrew Hancox <andrewdchancox@googlemail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tagareas = array(
array(
'itemtype' => 'data_records',
'component' => 'mod_data',
'callback' => 'mod_data_get_tagged_records',
'callbackfile' => '/mod/data/locallib.php',
),
);

View File

@ -33,8 +33,7 @@ $d = optional_param('d', 0, PARAM_INT); // database id
$rid = optional_param('rid', 0, PARAM_INT); //record id
$cancel = optional_param('cancel', '', PARAM_RAW); // cancel an add
$mode ='addtemplate'; //define the mode for this page, only 1 mode available
$tags = optional_param_array('tags', [], PARAM_TAGLIST);
$url = new moodle_url('/mod/data/edit.php');
if ($rid !== 0) {
@ -182,6 +181,7 @@ if ($datarecord = data_submitted() and confirm_sesskey()) {
if ($processeddata->validated) {
// Enough data to update the record.
data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata);
core_tag_tag::set_item_tags('mod_data', 'data_records', $rid, $context, $tags);
$viewurl = new moodle_url('/mod/data/view.php', array(
'd' => $data->id,
@ -209,6 +209,8 @@ if ($datarecord = data_submitted() and confirm_sesskey()) {
// Now populate the fields contents of the new record.
data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata);
core_tag_tag::set_item_tags('mod_data', 'data_records', $recordid, $context, $tags);
if (!empty($datarecord->saveandview)) {
$viewurl = new moodle_url('/mod/data/view.php', array(
'd' => $data->id,
@ -287,6 +289,12 @@ if ($data->addtemplate){
$patterns[] = "[[".$field->field->name."#id]]";
$replacements[] = 'field_'.$field->field->id;
}
if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
$patterns[] = "##tags##";
$replacements[] = data_generate_tag_form($rid);
}
$newtext = str_ireplace($patterns, $replacements, $data->{$mode});
} else { //if the add template is not yet defined, print the default form!

View File

@ -345,6 +345,7 @@ $string['subplugintype_datafield'] = 'Database field type';
$string['subplugintype_datafield_plural'] = 'Database field types';
$string['subplugintype_datapreset'] = 'Preset';
$string['subplugintype_datapreset_plural'] = 'Presets';
$string['tagarea_data_records'] = 'Data records';
$string['teachersandstudents'] = '{$a->teachers} and {$a->students}';
$string['templates'] = 'Templates';
$string['templatesaved'] = 'Template saved';

View File

@ -622,6 +622,17 @@ function data_generate_default_template(&$data, $template, $recordid=0, $form=fa
$token
);
}
if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
$label = new html_table_cell(get_string('tags') . ':');
if ($form) {
$cell = data_generate_tag_form();
} else {
$cell = new html_table_cell('##tags##');
}
$table->data[] = new html_table_row(array($label, $cell));
}
if ($template == 'listtemplate') {
$cell = new html_table_cell('##edit## ##more## ##delete## ##approve## ##disapprove## ##export##');
$cell->colspan = 2;
@ -666,6 +677,59 @@ function data_generate_default_template(&$data, $template, $recordid=0, $form=fa
}
}
/**
* Build the form elements to manage tags for a record.
*
* @param int $recordid
* @return string
*/
function data_generate_tag_form($recordid) {
global $CFG, $DB, $PAGE;
$tagtypestoshow = \core_tag_area::get_showstandard('mod_data', 'data_records');
$showstandard = ($tagtypestoshow != core_tag_tag::HIDE_STANDARD);
$typenewtags = ($tagtypestoshow != core_tag_tag::STANDARD_ONLY);
$str = html_writer::start_tag('div', array('class' => 'datatagcontrol'));
$namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname';
$existingtags = core_tag_tag::get_item_tags_array('mod_data', 'data_records', $recordid);
if ($showstandard) {
$tags = $DB->get_records_menu('tag', array(
'isstandard' => 1,
'tagcollid' => \core_tag_area::get_collection('mod_data', 'data_records')
), $namefield, 'id,' . $namefield . ' as fieldname');
$tags = $existingtags + $tags;
} else {
$tags = $existingtags;
}
$str .= '<select class="custom-select" name="tags[]" id="tags" multiple>';
foreach ($tags as $tagid => $tag) {
$selected = key_exists($tagid, $existingtags) ? 'selected' : '';
$str .= "<option value='$tag' $selected>$tag</option>";
}
$str .= '</select>';
$PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array(
'#tags',
$typenewtags,
'',
get_string('entertags', 'tag'),
false,
$showstandard,
get_string('noselection', 'form')
)
);
$str .= html_writer::end_tag('div');
return $str;
}
/**
* Search for a field name and replaces it with another one in all the
@ -1442,6 +1506,12 @@ function data_print_template($template, $records, $data, $search='', $page=0, $r
$replacement[] = '';
}
if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
$patterns[] = "##tags##";
$replacement[] = $OUTPUT->tag_list(
core_tag_tag::get_item_tags('mod_data', 'data_records', $record->id), '', 'data-tags');
}
// actual replacement of the tags
$newtext = str_ireplace($patterns, $replacement, $data->{$template});
@ -3947,6 +4017,8 @@ function data_delete_record($recordid, $data, $courseid, $cmid) {
data_rss_delete_file($data);
}
core_tag_tag::remove_all_item_tags('mod_data', 'data_records', $recordid);
// Trigger an event for deleting this record.
$event = \mod_data\event\record_deleted::create(array(
'objectid' => $deleterecord->id,

View File

@ -771,6 +771,245 @@ function data_get_entries_left_to_view($data, $numentries, $canmanageentries) {
return 0;
}
/**
* Returns data records tagged with a specified tag.
*
* This is a callback used by the tag area mod_data/data_records to search for data records
* tagged with a specific tag.
*
* @param core_tag_tag $tag
* @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
* are displayed on the page and the per-page limit may be bigger
* @param int $fromctx context id where the link was displayed, may be used by callbacks
* to display items in the same context first
* @param int $ctx context id where to search for records
* @param bool $rec search in subcontexts as well
* @param int $page 0-based number of page being displayed
* @return \core_tag\output\tagindex
*/
function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) {
global $DB, $OUTPUT, $USER;
$perpage = $exclusivemode ? 20 : 5;
// Build the SQL query.
$ctxselect = context_helper::get_preload_record_columns_sql('ctx');
$query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid,
d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
FROM {data_records} dr
JOIN {data} d
ON d.id = dr.dataid
JOIN {modules} m
ON m.name = 'data'
JOIN {course_modules} cm
ON cm.module = m.id AND cm.instance = d.id
JOIN {tag_instance} tt
ON dr.id = tt.itemid
JOIN {course} c
ON cm.course = c.id
JOIN {context} ctx
ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
WHERE tt.itemtype = :itemtype
AND tt.tagid = :tagid
AND tt.component = :component
AND cm.deletioninprogress = 0
AND dr.id %ITEMFILTER%
AND c.id %COURSEFILTER%";
$params = array(
'itemtype' => 'data_records',
'tagid' => $tag->id,
'component' => 'mod_data',
'coursemodulecontextlevel' => CONTEXT_MODULE
);
if ($ctx) {
$context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
$query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
$params['contextid'] = $context->id;
$params['path'] = $context->path . '/%';
}
$query .= " ORDER BY ";
if ($fromctx) {
// In order-clause specify that modules from inside "fromctx" context should be returned first.
$fromcontext = context::instance_by_id($fromctx);
$query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
$params['fromcontextid'] = $fromcontext->id;
$params['frompath'] = $fromcontext->path . '/%';
}
$query .= ' c.sortorder, cm.id, dr.id';
$totalpages = $page + 1;
// Use core_tag_index_builder to build and filter the list of items.
$builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1);
$now = time();
$entrycount = [];
$activitygroupmode = [];
$usergroups = [];
$titlefields = [];
while ($item = $builder->has_item_that_needs_access_check()) {
context_helper::preload_from_record($item);
$modinfo = get_fast_modinfo($item->courseid);
$cm = $modinfo->get_cm($item->cmid);
$context = \context_module::instance($cm->id);
$courseid = $item->courseid;
if (!$builder->can_access_course($courseid)) {
$builder->set_accessible($item, false);
continue;
}
if (!has_capability('mod/data:viewentry', $context)) {
$builder->set_accessible($item, false);
continue;
}
if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom)
|| ($item->timeviewto && $now > $item->timeviewto))) {
$builder->set_accessible($item, false);
continue;
}
if ($USER->id != $item->userid && $item->approval && !$item->approved) {
$builder->set_accessible($item, false);
continue;
}
if ($item->requiredentriestoview) {
if (!isset($entrycount[$item->dataid])) {
$entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid));
}
$sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid];
$builder->set_accessible($item, $sufficiententries);
}
if (!isset($activitygroupmode[$cm->id])) {
$activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm);
}
if (!isset($usergroups[$item->groupid])) {
$usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id);
}
if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) {
$builder->set_accessible($item, false);
continue;
}
$builder->set_accessible($item, true);
}
$items = $builder->get_items();
if (count($items) > $perpage) {
$totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
array_pop($items);
}
// Build the display contents.
if ($items) {
$tagfeed = new core_tag\output\tagfeed();
foreach ($items as $item) {
context_helper::preload_from_record($item);
$modinfo = get_fast_modinfo($item->courseid);
$cm = $modinfo->get_cm($item->cmid);
$pageurl = new moodle_url('/mod/data/view.php', array(
'rid' => $item->id,
'd' => $item->dataid
));
if (!isset($titlefields[$item->dataid])) {
$titlefields[$item->dataid] = data_get_tag_title_field($item->dataid);
}
$pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item);
$pagename = html_writer::link($pageurl, $pagename);
$courseurl = course_get_url($item->courseid, $cm->sectionnum);
$cmname = html_writer::link($cm->url, $cm->get_formatted_name());
$coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
$coursename = html_writer::link($courseurl, $coursename);
$icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
$tagfeed->add($icon, $pagename, $cmname . '<br>' . $coursename);
}
$content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode,
$fromctx, $ctx, $rec, $page, $totalpages);
}
}
/**
* Get the title of a field to show when displaying tag results.
*
* @param int $dataid The id of the data field
* @return stdClass The field data from the 'data_fields' table as well as it's priority
*/
function data_get_tag_title_field($dataid) {
global $DB, $CFG;
$validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url');
$fields = $DB->get_records('data_fields', ['dataid' => $dataid]);
$template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]);
$filteredfields = [];
foreach ($fields as $field) {
if (!in_array($field->type, $validfieldtypes)) {
continue;
}
$field->addtemplateposition = strpos($template, '[['.$field->name.']]');
if ($field->addtemplateposition === false) {
continue;
}
require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
$classname = 'data_field_' . $field->type;
$field->priority = $classname::get_priority();
$filteredfields[] = $field;
}
$sort = function($record1, $record2) {
// If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content.
if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) {
if ($record1->priority === $record2->priority) {
return $record1->id < $record2->id ? 1 : -1;
}
return $record1->priority < $record2->priority ? -1 : 1;
} else if ($record1->required && !$record2->required) {
return 1;
} else {
return -1;
}
};
usort($filteredfields, $sort);
return array_shift($filteredfields);
}
/**
* Get the title of an entry to show when displaying tag results.
*
* @param stdClass $field The field from the 'data_fields' table
* @param stdClass $entry The entry from the 'data_records' table
* @return string The title of the entry
*/
function data_get_tag_title_for_entry($field, $entry) {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php');
$classname = 'data_field_' . $field->type;
$sql = "SELECT dc.*
FROM {data_content} dc
INNER JOIN {data_fields} df
ON dc.fieldid = df.id
WHERE df.id = :fieldid
AND dc.recordid = :recordid";
$fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id));
return $classname::get_content_value($fieldcontents);
}
/**
* Search entries in a database.
*

View File

@ -146,3 +146,7 @@
#page-mod-data-edit .mod-data-input {
margin-left: 10px;
}
#page-mod-data-edit .datatagcontrol {
padding-left: 10px;
}

View File

@ -259,6 +259,11 @@ if ($mode != 'csstemplate' and $mode != 'jstemplate') {
echo '<option value="[['.$field->name.'#id]]" title="'.$field->description.' id">'.$field->name.' id - [['.$field->name.'#id]]</option>';
}
echo '</optgroup>';
if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
echo '<optgroup label="'.get_string('other', 'data').'">';
echo '<option value="##tags##">' . get_string('tags') . ' - ##tags##</option>';
echo '</optgroup>';
}
}
// Print special tags. fix for MDL-7031
@ -284,6 +289,11 @@ if ($mode != 'csstemplate' and $mode != 'jstemplate') {
echo '<option value="##user##">' .get_string('user'). ' - ##user##</option>';
echo '<option value="##userpicture##">' . get_string('userpic') . ' - ##userpicture##</option>';
echo '<option value="##approvalstatus##">' .get_string('approvalstatus', 'data'). ' - ##approvalstatus##</option>';
if (core_tag_tag::is_enabled('mod_data', 'data_records')) {
echo '<option value="##tags##">' . get_string('tags') . ' - ##tags##</option>';
}
if ($mode != 'singletemplate') {
// more points to single template - not useable there
echo '<option value="##comments##">' .get_string('comments', 'data'). ' - ##comments##</option>';

View File

@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2017051500; // The current module version (Date: YYYYMMDDXX)
$plugin->version = 2017091400; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2017050500; // Requires this Moodle version
$plugin->component = 'mod_data'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;