diff --git a/mod/data/backup/moodle2/backup_data_stepslib.php b/mod/data/backup/moodle2/backup_data_stepslib.php index 5a7f0ea45ad..61409616df4 100644 --- a/mod/data/backup/moodle2/backup_data_stepslib.php +++ b/mod/data/backup/moodle2/backup_data_stepslib.php @@ -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 diff --git a/mod/data/backup/moodle2/restore_data_stepslib.php b/mod/data/backup/moodle2/restore_data_stepslib.php index 7ac138b8ff6..c7bd9119aea 100644 --- a/mod/data/backup/moodle2/restore_data_stepslib.php +++ b/mod/data/backup/moodle2/restore_data_stepslib.php @@ -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; diff --git a/mod/data/db/tag.php b/mod/data/db/tag.php new file mode 100644 index 00000000000..425c4ce8b52 --- /dev/null +++ b/mod/data/db/tag.php @@ -0,0 +1,34 @@ +. + +/** + * Tag areas in component mod_data + * + * @package mod_data + * @copyright 2017 Andrew Hancox + * @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', + ), +); diff --git a/mod/data/edit.php b/mod/data/edit.php index 615aa22feb1..5c6a6bf944c 100644 --- a/mod/data/edit.php +++ b/mod/data/edit.php @@ -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! diff --git a/mod/data/lang/en/data.php b/mod/data/lang/en/data.php index 315d53881fb..35334421aa8 100644 --- a/mod/data/lang/en/data.php +++ b/mod/data/lang/en/data.php @@ -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'; diff --git a/mod/data/lib.php b/mod/data/lib.php index 498a5c01338..d80314cf1aa 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -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 .= ''; + + $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, diff --git a/mod/data/locallib.php b/mod/data/locallib.php index 93be6ac168e..afedf5ce132 100644 --- a/mod/data/locallib.php +++ b/mod/data/locallib.php @@ -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 . '
' . $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. * diff --git a/mod/data/styles.css b/mod/data/styles.css index 1e06bca6b4b..f7ef18a1f29 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -146,3 +146,7 @@ #page-mod-data-edit .mod-data-input { margin-left: 10px; } + +#page-mod-data-edit .datatagcontrol { + padding-left: 10px; +} diff --git a/mod/data/templates.php b/mod/data/templates.php index 4d68d9fe127..8fa48c53518 100644 --- a/mod/data/templates.php +++ b/mod/data/templates.php @@ -259,6 +259,11 @@ if ($mode != 'csstemplate' and $mode != 'jstemplate') { echo ''; } echo ''; + if (core_tag_tag::is_enabled('mod_data', 'data_records')) { + echo ''; + echo ''; + echo ''; + } } // Print special tags. fix for MDL-7031 @@ -284,6 +289,11 @@ if ($mode != 'csstemplate' and $mode != 'jstemplate') { echo ''; echo ''; echo ''; + + if (core_tag_tag::is_enabled('mod_data', 'data_records')) { + echo ''; + } + if ($mode != 'singletemplate') { // more points to single template - not useable there echo ''; diff --git a/mod/data/version.php b/mod/data/version.php index b273586a120..80de85fa816 100644 --- a/mod/data/version.php +++ b/mod/data/version.php @@ -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;