ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç js.php000064400000004700152265517740005710 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\manager; use mod_data\preset; define('NO_MOODLE_COOKIES', true); // session not used here require_once('../../config.php'); $id = optional_param('id', 0, PARAM_INT); // Course module id. $d = optional_param('d', 0, PARAM_INT); // Database id. $presetfullname = optional_param('preset', '', PARAM_PATH); // The directory the preset is in. $lifetime = 600; // Seconds to cache this stylesheet. $url = new moodle_url('/mod/data/js.php'); $manager = null; if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); $instance = $manager->get_instance(); } else { // We must have the database activity id. $d = required_param('d', PARAM_INT); $instance = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($instance); } $url->param('d', $instance->id); // Get the content. if ($presetfullname) { $url->param('preset', $presetfullname); $preset = preset::create_from_fullname($manager, $presetfullname); $content = $preset->get_template_content('jstemplate'); $lifetime = 60; // Preset preview does not need a long cache. } else { $content = $instance->jstemplate; } $PAGE->set_url($url); header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); header('Expires: ' . gmdate("D, d M Y H:i:s", time() + $lifetime) . ' GMT'); header('Cache-control: max_age = '. $lifetime); header('Pragma: '); header('Content-type: application/javascript; charset=utf-8'); // Correct MIME type. echo $content; templates.php000064400000011466152265517740007301 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\manager; require_once('../../config.php'); require_once('lib.php'); $id = optional_param('id', 0, PARAM_INT); // course module id $d = optional_param('d', 0, PARAM_INT); // database id $mode = optional_param('mode', 'addtemplate', PARAM_ALPHA); $action = optional_param('action', '', PARAM_ALPHA); $useeditor = optional_param('useeditor', null, PARAM_BOOL); $url = new moodle_url('/mod/data/templates.php'); if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); $url->param('d', $cm->instance); } else { // We must have $d. $instance = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($instance); $cm = $manager->get_coursemodule(); $course = get_course($cm->course); $url->param('d', $d); } $instance = $manager->get_instance(); $context = $manager->get_context(); $url->param('mode', $mode); $PAGE->set_url($url); require_login($course, false, $cm); require_capability('mod/data:managetemplates', $context); if ($action == 'resetalltemplates') { require_sesskey(); $manager->reset_all_templates(); redirect($PAGE->url, get_string('templateresetall', 'mod_data'), null, \core\output\notification::NOTIFY_SUCCESS); } $manager->set_template_viewed(); if ($useeditor !== null) { // The useeditor param was set. Update the value for this template. data_set_config($instance, "editor_{$mode}", !!$useeditor); } $PAGE->requires->js('/mod/data/data.js'); switch ($mode) { case 'asearchtemplate': $title = get_string('asearchtemplate', 'data'); break; case 'csstemplate': $title = get_string('csstemplate', 'data'); break; case 'jstemplate': $title = get_string('jstemplate', 'data'); break; case 'listtemplate': $title = get_string('listtemplate', 'data'); break; case 'rsstemplate': $title = get_string('rsstemplate', 'data'); break; case 'singletemplate': $title = get_string('singletemplate', 'data'); break; default: if ($manager->has_fields()) { $title = get_string('addtemplate', 'data'); } else { $title = get_string('data:managetemplates', 'data'); } break; } $titleparts = [ $title, format_string($instance->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('admin'); $PAGE->force_settings_menu(true); $PAGE->activityheader->disable(); $PAGE->add_body_class('limitedwidth'); echo $OUTPUT->header(); $renderer = $manager->get_renderer(); // Check if it is an empty database with no fields. if (!$manager->has_fields()) { echo $renderer->render_templates_zero_state($manager); echo $OUTPUT->footer(); // Don't check the rest of the options. There is no field, there is nothing else to work with. exit; } $actionbar = new \mod_data\output\action_bar($instance->id, $url); echo $actionbar->get_templates_action_bar(); if (($formdata = data_submitted()) && confirm_sesskey()) { if (!empty($formdata->defaultform)) { // Reset the template to default. if (!empty($formdata->resetall)) { $manager->reset_all_templates(); $notificationstr = get_string('templateresetall', 'mod_data'); } else { $manager->reset_template($mode); $notificationstr = get_string('templatereset', 'data'); } } else { $manager->update_templates($formdata); $notificationstr = get_string('templatesaved', 'data'); } } if (!empty($notificationstr)) { echo $OUTPUT->notification($notificationstr, 'notifysuccess'); } $templateeditor = new \mod_data\output\template_editor($manager, $mode); echo $renderer->render($templateeditor); /// Finish the page echo $OUTPUT->footer(); styles.css000064400000010227152265517740006621 0ustar00.path-mod-data-field .c0, #page-mod-data-view #sortsearch .c0 { text-align: right; } #page-mod-data-view .approve img.icon { width: 34px; height: 34px; } .path-mod-data #id_entrieshdr div .alert.alert-danger { margin-bottom: 2rem; } #page-mod-data-view img.list_picture { border: 0; } #page-mod-data-view div.search_none { display: none; } #page-mod-data-view div.search_inline, #page-mod-data-view form#latlongfieldbrowse { display: inline; } #page-mod-data-view div#data_adv_form { margin-left: auto; margin-right: auto; } #page-mod-data-edit .basefieldinput { width: 300px; } /** Styles for preset.php **/ #page-mod-data-preset .presetmapping table { text-align: left; margin-left: auto; margin-right: auto; } #page-mod-data-preset .overwritesettings { margin-bottom: 1em; } #page-mod-data-preset table.presets { margin-left: auto; margin-right: auto; } /** Styles for the search form **/ #page-mod-data-view .datapreferences label { display: inline-block; } /** Styles for field.php **/ .path-mod-data-field .sortdefault { margin: 1em 0; } .path-mod-data-field .fieldname, .path-mod-data-field .fielddescription { width: 300px; } .path-mod-data-field textarea.optionstextarea { width: 300px; height: 150px; } .path-mod-data-field input.textareafieldsize { width: 50px; } .path-mod-data-field input.picturefieldsize { width: 70px; } .path-mod-data .action-icon img.portfolio-add-icon { margin-left: 0; } /** UI Usability Hacks **/ #page-mod-data-export #notice span { padding: 0 10px; } /*rtl:ignore*/ #page-mod-data-edit input[id*="url"] { text-align: left; direction: ltr; } .mod-data-default-template td { vertical-align: top; } .mod-data-default-template .template-field { text-align: right; } .mod-data-default-template .template-token { text-align: left; } .mod-data-default-template .controls { text-align: center; } .mod-data-default-template .searchcontrols { text-align: right; } #page-mod-data-view .notapproved { background-color: #fff3cd; color: #856404; border-radius: 0.5em; padding: 0.5em 1em; gap: 1em; } #page-mod-data-templates td.save_template, #page-mod-data-templates .template_heading { text-align: center; } #page-mod-data-templates #availabletags_wrapper { max-width: 250px; } #page-mod-data-edit .req { cursor: help; } #page-mod-data-edit .inline-req .req { position: absolute; } #page-mod-data-edit .inline-req { text-align: left; } #page-mod-data-edit .datatagcontrol { padding-left: 10px; } .preset_action_menu .dropdown-toggle::after, .entry-actionsmenu .dropdown-toggle::after, .fields-actions .dropdown-toggle::after { display: none; } .defaulttemplate-single-body img.list_picture { max-width: 100%; } .defaulttemplate-list-body img.list_picture { max-width: 100%; } /* Date information displayed in templates/presets */ .data-timeinfo { color: var(–gray); } /* Legacy icons */ .dataicon, .portfolio-add-icon { color: var(--dark); } /* Preset preview styles */ #page-mod-data-preset .nopreview { border: 1px solid var(--secondary); padding: 0.2rem; border-radius: 5px; } /* Disable links and buttons in the preset preview */ .template-preview-content .preview-disabled, .template-preview-content .action-icon, .template-preview-content .dropdown-menu a, .template-preview-content .data-field-link, .template-preview-content .data-field-html a, .template-preview-content .data-field-html button { pointer-events: none; } /* Actions menu */ .presetsactions .dropdown-toggle, .presetsactions .dropdown-toggle:hover, .presetsactions .show > .btn-secondary.dropdown-toggle, .entriesactions .dropdown-toggle, .entriesactions .dropdown-toggle:hover, .entriesactions .show > .btn-secondary.dropdown-toggle, #page-mod-data-view .whitebutton .btn-secondary, #page-mod-data-preset .whitebutton .btn-secondary, #page-mod-data-field- .whitebutton .btn-secondary, #page-mod-data-templates .whitebutton .btn-secondary { background: white; border-color: var(--bs-primary); color: var(--bs-primary); } #page-mod-data-preset .generaltable { margin-bottom: 10rem; } renderer.php000064400000030554152265517740007110 0ustar00. /** * Database activity renderer. * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\manager; defined('MOODLE_INTERNAL') || die(); class mod_data_renderer extends plugin_renderer_base { /** * @deprecated since Moodle 4.1 MDL-75140 - please do not use this class any more. */ #[\core\attribute\deprecated('mod_data_renderer::importing_preset()', since: '4.1', mdl: 'MDL-75140', final: true)] public function import_setting_mappings(): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Importing a preset on a database module. * * @param stdClass $datamodule Database module to import to. * @param \mod_data\local\importer\preset_importer $importer Importer instance to use for the importing. * * @return string */ public function importing_preset(stdClass $datamodule, \mod_data\local\importer\preset_importer $importer): string { $strwarning = get_string('mappingwarning', 'data'); $params = $importer->settings; $newfields = $params->importfields; $currentfields = $params->currentfields; $html = html_writer::start_tag('div', ['class' => 'presetmapping']); $html .= html_writer::start_tag('form', ['method' => 'post', 'action' => '']); $html .= html_writer::start_tag('div'); $html .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'action', 'value' => 'finishimport']); $html .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]); $html .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'd', 'value' => $datamodule->id]); $inputselector = $importer->get_preset_selector(); $html .= html_writer::empty_tag( 'input', ['type' => 'hidden', 'name' => $inputselector['name'], 'value' => $inputselector['value']] ); if (!empty($newfields)) { $table = new html_table(); $table->data = array(); foreach ($newfields as $nid => $newfield) { $row = array(); $row[0] = html_writer::tag('label', $newfield->name, array('for'=>'id_'.$newfield->name)); $attrs = ['name' => 'field_' . $nid, 'id' => 'id_' . $newfield->name, 'class' => 'form-select']; $row[1] = html_writer::start_tag('select', $attrs); $selected = false; foreach ($currentfields as $cid => $currentfield) { if ($currentfield->type != $newfield->type) { continue; } if ($currentfield->name == $newfield->name) { $row[1] .= html_writer::tag( 'option', get_string('mapexistingfield', 'data', $currentfield->name), ['value' => $cid, 'selected' => 'selected'] ); $selected = true; } else { $row[1] .= html_writer::tag( 'option', get_string('mapexistingfield', 'data', $currentfield->name), ['value' => $cid] ); } } if ($selected) { $row[1] .= html_writer::tag('option', get_string('mapnewfield', 'data'), array('value'=>'-1')); } else { $row[1] .= html_writer::tag('option', get_string('mapnewfield', 'data'), array('value'=>'-1', 'selected'=>'selected')); } $row[1] .= html_writer::end_tag('select'); $table->data[] = $row; } $html .= html_writer::table($table); $html .= html_writer::tag('p', $strwarning); } else { $html .= $this->output->notification(get_string('nodefinedfields', 'data')); } $html .= html_writer::start_tag('div', array('class'=>'overwritesettings')); $attrs = ['type' => 'checkbox', 'name' => 'overwritesettings', 'id' => 'overwritesettings', 'class' => 'me-2']; $html .= html_writer::empty_tag('input', $attrs); $html .= html_writer::tag('label', get_string('overwritesettings', 'data'), ['for' => 'overwritesettings']); $html .= html_writer::end_tag('div'); $actionbuttons = html_writer::start_div(); $cancelurl = new moodle_url('/mod/data/field.php', ['d' => $datamodule->id]); $actionbuttons .= html_writer::tag('a', get_string('cancel') , [ 'href' => $cancelurl->out(false), 'class' => 'btn btn-secondary mx-1', 'role' => 'button', ]); $actionbuttons .= html_writer::empty_tag('input', [ 'type' => 'submit', 'class' => 'btn btn-primary mx-1', 'value' => get_string('continue'), ]); $actionbuttons .= html_writer::end_div(); $stickyfooter = new core\output\sticky_footer($actionbuttons); $html .= $this->render($stickyfooter); $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('form'); $html .= html_writer::end_tag('div'); return $html; } /** * Renders the action bar for the field page. * * @param \mod_data\output\fields_action_bar $actionbar * @return string The HTML output */ public function render_fields_action_bar(\mod_data\output\fields_action_bar $actionbar): string { $data = $actionbar->export_for_template($this); return $this->render_from_template('mod_data/action_bar', $data); } /** * Renders the fields page footer. * * @param manager $manager the instance manager * @return string The HTML output * * @deprecated since Moodle 4.5 - please do not use this function anymore */ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.5')] public function render_fields_footer(manager $manager): string { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); $cm = $manager->get_coursemodule(); $pageurl = new moodle_url('/mod/data/templates.php', ['id' => $cm->id]); return $this->render_from_template('mod_data/fields_footer', [ 'pageurl' => $pageurl->out(false), ]); } /** * Renders the action bar for the view page. * * @param \mod_data\output\view_action_bar $actionbar * @return string The HTML output */ public function render_view_action_bar(\mod_data\output\view_action_bar $actionbar): string { $data = $actionbar->export_for_template($this); return $this->render_from_template('mod_data/view_action_bar', $data); } /** * Renders the action bar for the template page. * * @param \mod_data\output\templates_action_bar $actionbar * @return string The HTML output */ public function render_templates_action_bar(\mod_data\output\templates_action_bar $actionbar): string { $data = $actionbar->export_for_template($this); return $this->render_from_template('mod_data/templates_action_bar', $data); } /** * Renders the action bar for the preset page. * * @param \mod_data\output\presets_action_bar $actionbar * @return string The HTML output */ public function render_presets_action_bar(\mod_data\output\presets_action_bar $actionbar): string { $data = $actionbar->export_for_template($this); return $this->render_from_template('mod_data/presets_action_bar', $data); } /** * Renders the presets table in the preset page. * * @param \mod_data\output\presets $presets * @return string The HTML output */ public function render_presets(\mod_data\output\presets $presets): string { $data = $presets->export_for_template($this); return $this->render_from_template('mod_data/presets', $data); } /** * Renders the default template. * * @param \mod_data\output\defaulttemplate $template * @return string The HTML output */ public function render_defaulttemplate(\mod_data\output\defaulttemplate $template): string { $data = $template->export_for_template($this); return $this->render_from_template($template->get_templatename(), $data); } /** * Renders the action bar for the zero state (no fields created) page. * * @param \mod_data\manager $manager The manager instance. * * @return string The HTML output */ public function render_database_zero_state(\mod_data\manager $manager): string { $actionbar = new \mod_data\output\zero_state_action_bar($manager); $data = $actionbar->export_for_template($this); if (empty($data)) { // No actions for the user. $data['title'] = get_string('activitynotready'); $data['intro'] = get_string('comebacklater'); $data['noitemsimgurl'] = $this->output->image_url('noentries_zero_state', 'mod_data')->out(); } else { $data['title'] = get_string('startbuilding', 'mod_data'); $data['intro'] = get_string('createactivity', 'mod_data'); $data['noitemsimgurl'] = $this->output->image_url('view_zero_state', 'mod_data')->out(); } return $this->render_from_template('mod_data/zero_state', $data); } /** * Renders the action bar for an empty database view page. * * @param \mod_data\manager $manager The manager instance. * * @return string The HTML output */ public function render_empty_database(\mod_data\manager $manager): string { $actionbar = new \mod_data\output\empty_database_action_bar($manager); $data = $actionbar->export_for_template($this); $data['noitemsimgurl'] = $this->output->image_url('view_zero_state', 'mod_data')->out(); return $this->render_from_template('mod_data/view_noentries', $data); } /** * Renders the action bar for the zero state (no fields created) page. * * @param \mod_data\manager $manager The manager instance. * * @return string The HTML output */ public function render_fields_zero_state(\mod_data\manager $manager): string { $data = [ 'noitemsimgurl' => $this->output->image_url('fields_zero_state', 'mod_data')->out(), 'title' => get_string('nofields', 'mod_data'), 'intro' => get_string('createfields', 'mod_data'), ]; if ($manager->can_manage_templates()) { $actionbar = new \mod_data\output\action_bar($manager->get_instance()->id, $this->page->url); $createfieldbutton = $actionbar->get_create_fields(); $data['createfieldbutton'] = $createfieldbutton->export_for_template($this); } return $this->render_from_template('mod_data/zero_state', $data); } /** * Renders the action bar for the templates zero state (no fields created) page. * * @param \mod_data\manager $manager The manager instance. * * @return string The HTML output */ public function render_templates_zero_state(\mod_data\manager $manager): string { $actionbar = new \mod_data\output\zero_state_action_bar($manager); $data = $actionbar->export_for_template($this); $data['title'] = get_string('notemplates', 'mod_data'); $data['intro'] = get_string('createtemplates', 'mod_data'); $data['noitemsimgurl'] = $this->output->image_url('templates_zero_state', 'mod_data')->out(); return $this->render_from_template('mod_data/zero_state', $data); } } db/tag.php000064400000002200152265517740006425 0ustar00. /** * 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', ), ); db/services.php000064400000013123152265517740007503 0ustar00. /** * Database external functions and service definitions. * * @package mod_data * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ $functions = array( 'mod_data_get_databases_by_courses' => array( 'classname' => 'mod_data_external', 'methodname' => 'get_databases_by_courses', 'description' => 'Returns a list of database instances in a provided set of courses, if no courses are provided then all the database instances the user has access to will be returned.', 'type' => 'read', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_view_database' => array( 'classname' => 'mod_data_external', 'methodname' => 'view_database', 'description' => 'Simulate the view.php web interface data: trigger events, completion, etc...', 'type' => 'write', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_get_data_access_information' => array( 'classname' => 'mod_data_external', 'methodname' => 'get_data_access_information', 'description' => 'Return access information for a given database.', 'type' => 'read', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_get_entries' => array( 'classname' => 'mod_data_external', 'methodname' => 'get_entries', 'description' => 'Return the complete list of entries of the given database.', 'type' => 'read', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_get_entry' => array( 'classname' => 'mod_data_external', 'methodname' => 'get_entry', 'description' => 'Return one entry record from the database, including contents optionally.', 'type' => 'read', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_get_fields' => array( 'classname' => 'mod_data_external', 'methodname' => 'get_fields', 'description' => 'Return the list of configured fields for the given database.', 'type' => 'read', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_search_entries' => array( 'classname' => 'mod_data_external', 'methodname' => 'search_entries', 'description' => 'Search for entries in the given database.', 'type' => 'read', 'capabilities' => 'mod/data:viewentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_approve_entry' => array( 'classname' => 'mod_data_external', 'methodname' => 'approve_entry', 'description' => 'Approves or unapproves an entry.', 'type' => 'write', 'capabilities' => 'mod/data:approve', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_delete_entry' => array( 'classname' => 'mod_data_external', 'methodname' => 'delete_entry', 'description' => 'Deletes an entry.', 'type' => 'write', 'capabilities' => 'mod/data:manageentries', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_add_entry' => array( 'classname' => 'mod_data_external', 'methodname' => 'add_entry', 'description' => 'Adds a new entry.', 'type' => 'write', 'capabilities' => 'mod/data:writeentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_update_entry' => array( 'classname' => 'mod_data_external', 'methodname' => 'update_entry', 'description' => 'Updates an existing entry.', 'type' => 'write', 'capabilities' => 'mod/data:writeentry', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), 'mod_data_delete_saved_preset' => array( 'classname' => 'mod_data\external\delete_saved_preset', 'description' => 'Delete site user preset.', 'type' => 'write', 'ajax' => true, 'capabilities' => 'mod/data:manageuserpresets', ), 'mod_data_get_mapping_information' => array( 'classname' => 'mod_data\external\get_mapping_information', 'description' => 'Get importing information', 'type' => 'read', 'ajax' => true, 'capabilities' => 'mod/data:managetemplates', ), ); db/subplugins.json000064400000000312152265517740010231 0ustar00{ "subplugintypes": { "datafield": "field", "datapreset": "preset" }, "plugintypes": { "datafield": "mod/data/field", "datapreset": "mod/data/preset" } } db/log.php000064400000003203152265517740006437 0ustar00. /** * Definition of log events * * @package mod_data * @category log * @copyright 2010 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $logs = array( array('module'=>'data', 'action'=>'view', 'mtable'=>'data', 'field'=>'name'), array('module'=>'data', 'action'=>'add', 'mtable'=>'data', 'field'=>'name'), array('module'=>'data', 'action'=>'update', 'mtable'=>'data', 'field'=>'name'), array('module'=>'data', 'action'=>'record delete', 'mtable'=>'data', 'field'=>'name'), array('module'=>'data', 'action'=>'fields add', 'mtable'=>'data_fields', 'field'=>'name'), array('module'=>'data', 'action'=>'fields update', 'mtable'=>'data_fields', 'field'=>'name'), array('module'=>'data', 'action'=>'templates saved', 'mtable'=>'data', 'field'=>'name'), array('module'=>'data', 'action'=>'templates def', 'mtable'=>'data', 'field'=>'name'), );db/access.php000064400000014630152265517740007125 0ustar00. /** * Plugin capabilities * * @package mod_data * @copyright 2006 Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'mod/data:addinstance' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/course:manageactivities' ), 'mod/data:viewentry' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'frontpage' => CAP_ALLOW, // needed for databases on the frontpage 'guest' => CAP_ALLOW, 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:writeentry' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:rate' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:viewrating' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:viewanyrating' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/data:viewrating' ), 'mod/data:viewallratings' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'mod/data:viewrating' ), 'mod/data:approve' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:manageentries' => array( 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:managetemplates' => array( 'riskbitmask' => RISK_SPAM | RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:viewalluserpresets' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ), 'mod/data:manageuserpresets' => array( 'riskbitmask' => RISK_SPAM | RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'manager' => CAP_ALLOW ) ), 'mod/data:exportentry' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'manager' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ) ), 'mod/data:exportownentry' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'manager' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'student' => CAP_ALLOW, ) ), 'mod/data:exportallentries' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'manager' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ) ), 'mod/data:exportuserinfo' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'manager' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ) ), 'mod/data:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'guest' => CAP_ALLOW, 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ) ) ); $deprecatedcapabilities = [ 'mod/data:comment' => ['message' => 'This capability should not be used anymore.'], 'mod/data:managecomments' => ['message' => 'This capability should not be used anymore.'], ]; db/upgrade.php000064400000005367152265517740007322 0ustar00. /** * This file keeps track of upgrades to the data module * * Sometimes, changes between versions involve * alterations to database structures and other * major things that may break installations. * * The upgrade function in this file will attempt * to perform all the necessary actions to upgrade * your older installation to the current version. * * If there's something it cannot do itself, it * will tell you what you need to do. * * The commands in here will all be database-neutral, * using the methods of database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @package mod_data * @copyright 2006 Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ function xmldb_data_upgrade($oldversion) { global $DB; // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2023061300) { // Clean orphan data_records. $sql = "SELECT d.id FROM {data} d LEFT JOIN {data_fields} f ON d.id = f.dataid WHERE f.id IS NULL"; $emptydatas = $DB->get_records_sql($sql); if (!empty($emptydatas)) { $dataids = array_keys($emptydatas); [$datainsql, $dataparams] = $DB->get_in_or_equal($dataids, SQL_PARAMS_NAMED, 'data'); $DB->delete_records_select('data_records', "dataid $datainsql", $dataparams); } // Data savepoint reached. upgrade_mod_savepoint(true, 2023061300, 'data'); } // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } db/install.xml000064400000020022152265517740007333 0ustar00
lang/en/deprecated.txt000064400000000110152265517740010736 0ustar00tagsdeleted,mod_data data:comment,mod_data data:managecomments,mod_data lang/en/data.php000064400000073113152265517740007534 0ustar00. /** * Strings for component 'mod_data', language 'en', branch 'MOODLE_20_STABLE' * * @package mod_data * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['action'] = 'Action'; $string['actionsmenu'] = 'Actions menu'; $string['add'] = 'Add entry'; $string['addcomment'] = 'Add comment'; $string['addtemplate'] = 'Add entry template'; $string['advancedsearch'] = 'Advanced search'; $string['allowcomments'] = 'Allow comments on entries'; $string['alttext'] = 'Alternative text'; $string['approvalstatus'] = 'Approval status'; $string['approve'] = 'Approve'; $string['approved'] = 'Approved'; $string['areacontent'] = 'Fields'; $string['ascending'] = 'Ascending'; $string['asearchtemplate'] = 'Advanced search template'; $string['atmaxentry'] = 'You have entered the maximum number of entries allowed!'; $string['authorfirstname'] = 'First name'; $string['authorlastname'] = 'Last name'; $string['autogenallforms'] = 'Generate all default templates'; $string['autolinkurl'] = 'Autolink the URL'; $string['availablefromdate'] = 'Available from'; $string['availabletags'] = 'Available tags'; $string['availabletags_help'] = 'Tags are placeholders in the template, which will be replaced by data or other items, such as an edit icon, when entries are edited or viewed. Fields have the format [[fieldname]]. All other tags have the format ##sometag##. Only the tags that are in the "Available tags" list may be used for the current template.'; $string['availabletodate'] = 'Available to'; $string['availabletodatevalidation'] = 'The available to date cannot be before the available from date.'; $string['bynameondate'] = 'by {$a->name} - {$a->date}'; $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; $string['cancel'] = 'Cancel'; $string['cannotaccesspresentsother'] = 'You are not allowed to access presets from other users'; $string['cannotadd'] = 'Can not add entries!'; $string['cannotdeletepreset'] = 'Error deleting preset'; $string['cannotoverwritepreset'] = 'Error overwriting preset'; $string['cannotunziptopreset'] = 'Cannot unzip to the preset directory'; $string['cannotapplypreset'] = 'Error applying the preset.'; $string['closebeforeopen'] = 'You have specified an end date before the start date.'; $string['columns'] = 'columns'; $string['comment'] = 'Comment'; $string['commentdeleted'] = 'Comment deleted'; $string['commentempty'] = 'Comment was empty'; $string['comments'] = 'Comments'; $string['commentsaved'] = 'Comment saved'; $string['commentsn'] = '{$a} comment(s)'; $string['commentsoff'] = 'Comments feature is not enabled'; $string['completiondetail:entries'] = 'Make entries: {$a}'; $string['completionentriescount'] = 'Add entries'; $string['completionentriesdesc'] = 'Minimum number of entries required: {$a}'; $string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all databases. You will still need to turn feeds on manually in the settings for each database.'; $string['confirmdeletefield'] = 'You are about to delete this field, are you sure?'; $string['confirmdeleterecord'] = 'Are you sure you want to delete this entry?'; $string['confirmdeleterecords'] = 'Are you sure you want to delete these entries?'; $string['createactivity'] = 'Create your own fields to collect data, or use a preset which includes fields already.'; $string['createfields'] = 'Create fields to collect different types of data.'; $string['createtemplates'] = 'Templates define the interface of your activity. Once you create fields, templates will be created automatically. Alternatively, you can use a preset, which includes fields and templates.'; $string['csstemplate'] = 'Custom CSS'; $string['csvfailed'] = 'Unable to read the raw data from the CSV file'; $string['csvfile'] = 'CSV or ZIP containing a CSV file'; $string['csvimport'] = 'CSV file import'; $string['csvimport_help'] = 'Entries may be imported via a plain text file with a list of field names as the first line, then the data, with one record per line.'; $string['csvwithselecteddelimiter'] = 'CSV'; $string['data:addinstance'] = 'Add a new database'; $string['data:approve'] = 'Approve and undo approved entries'; $string['data:exportallentries'] = 'Export all database entries'; $string['data:exportentry'] = 'Export a database entry'; $string['data:exportownentry'] = 'Export own database entry'; $string['data:exportuserinfo'] = 'Export user information'; $string['data:manageentries'] = 'Manage entries'; $string['data:managetemplates'] = 'Manage templates'; $string['data:manageuserpresets'] = 'Manage presets'; $string['data:rate'] = 'Rate entries'; $string['data:readentry'] = 'Read entries'; $string['data:viewalluserpresets'] = 'View presets from all users'; $string['data:viewallratings'] = 'View all raw ratings given by individuals'; $string['data:viewanyrating'] = 'View total ratings that anyone received'; $string['data:viewentry'] = 'View entries'; $string['data:viewrating'] = 'View the total rating you received'; $string['data:writeentry'] = 'Write entries'; $string['data:view'] = 'View database activity'; $string['date'] = 'Date'; $string['dateentered'] = 'Date entered'; $string['datemodified'] = 'Last edited:'; $string['defaultfielddelimiter'] = '(default is the comma character)'; $string['defaultfieldenclosure'] = '(default is none)'; $string['defaultsortfield'] = 'Default sort field'; $string['delcheck'] = 'Bulk delete checkbox'; $string['delete'] = 'Delete'; $string['deleteallentries'] = 'All entries'; $string['deletecomment'] = 'Are you sure you want to delete this comment?'; $string['deleteconfirm'] = 'Delete preset {$a}?'; $string['deleted'] = 'deleted'; $string['deleteentry'] = 'Delete entry'; $string['deletefield'] = 'Delete field'; $string['deletenotenrolled'] = 'Entries by users not enrolled'; $string['deletewarning'] = 'Deleting a preset removes it from the list of available presets in all courses.'; $string['descending'] = 'Descending'; $string['directorynotapreset'] = '{$a->directory} Not a preset: missing files: {$a->missing_files}'; $string['disapprove'] = 'Undo approval'; $string['download'] = 'Download'; $string['edit'] = 'Edit'; $string['editcomment'] = 'Edit comment'; $string['editentry'] = 'Edit entry'; $string['editfield'] = 'Edit field'; $string['editordisable'] = 'Disable editor'; $string['editorenable'] = 'Enable code editor'; $string['editpreset'] = 'Edit preset'; $string['emptyadd'] = 'The Add template is empty, generating a default form...'; $string['emptyaddform'] = 'You did not fill out any fields!'; $string['enabletemplateeditorcheck'] = 'Are you sure you want to enable the editor? This may result in content being altered when the template is saved.'; $string['eventfieldcreated'] = 'Field created'; $string['eventfielddeleted'] = 'Field deleted'; $string['eventfieldupdated'] = 'Field updated'; $string['eventrecordcreated'] = 'Record created'; $string['eventrecorddeleted'] = 'Record deleted'; $string['eventrecordupdated'] = 'Record updated'; $string['eventtemplateupdated'] = 'Template updated'; $string['eventtemplateviewed'] = 'Templates viewed'; $string['fileencoding'] = 'Encoding'; $string['emptypresetname'] = 'Preset name or file cannot be empty'; $string['entries'] = 'Entries'; $string['entrieslefttoadd'] = 'You must add {$a->entriesleft} more entry/entries to complete this activity.'; $string['entrieslefttoaddtoview'] = 'You must add {$a->entrieslefttoview} more entry/entries before you can view other participants\' entries.'; $string['entry'] = 'Entry'; $string['entrysaved'] = 'Your entry has been saved'; $string['errordatafilenotfound'] = 'The file could not be imported. Accepted file types are CSV or a ZIP containing a CSV file in the format used for exporting entries.'; $string['errormustbeteacher'] = 'You need to be a teacher to use this page!'; $string['errorpresetexists'] = 'A preset with this name already exists.'; $string['errorpresetexistsbutnotoverwrite'] = 'A preset with this name already exists. Choose a different name.'; $string['errormustsupplyvalue'] = 'You must supply a value here.'; $string['errorpresetnotfound'] = 'Preset with name {$a} not found.'; $string['example'] = 'Database module example'; $string['excel'] = 'Excel'; $string['export'] = 'Export'; $string['exportaszip'] = 'Export as zip'; $string['exportaszip_help'] = 'The export as zip feature allows you to save the templates and fields as a preset zip for download. The zip may then be imported to another course.'; $string['exportedtozip'] = 'Exported to temporary zip...'; $string['exportentries'] = 'Export entries'; $string['exportformat'] = 'Export format'; $string['exportoptions'] = 'Export options'; $string['exportownentries'] = 'Export your own entries only? ({$a->mine}/{$a->all})'; $string['exportpreset'] = 'Export preset'; $string['failedpresetdelete'] = 'An error was encountered while trying to delete the preset.'; $string['fieldadded'] = 'Field added'; $string['fieldallowautolink'] = 'Allow autolink'; $string['fielddeleted'] = 'Field deleted'; $string['fielddelimiter'] = 'Field separator'; $string['fielddescription'] = 'Field description'; $string['fieldenclosure'] = 'Field enclosure'; $string['fieldheight'] = 'Height'; $string['fieldheightlistview'] = 'Height (in pixels) in list view'; $string['fieldheightsingleview'] = 'Height (in pixels) in single view'; $string['fieldmappings'] = 'Field mappings'; $string['fieldmappings_help'] = 'This menu allows you to keep the data from the existing database. To preserve the data in a field, you must map it to a new field, where the data will appear. Any field can also be left blank, with no information copied into it. Any old field not mapped to a new one will be lost and all its data removed. You can only map fields of the same type, so each drop-down menu will have different fields in it. Also, you must be careful not to try and map one old field to more than one new field.'; $string['fieldname'] = 'Field name'; $string['fieldnametype'] = '{$a->name} ({$a->type})'; $string['fieldnotmatched'] = 'The following fields in your file are not known in this database: {$a}'; $string['fieldoptions'] = 'Options (one per line)'; $string['fields'] = 'Fields'; $string['fieldshelp'] = 'Create fields to collect different types of data. Fields define the structure of the entries in your database.'; $string['fieldsinformationtags'] = 'Field information'; $string['fieldsnavigation'] = 'Fields tertiary navigation'; $string['fieldtagdescription'] = '{$a} description'; $string['fieldtagname'] = '{$a} name'; $string['fieldtagid'] = '{$a} ID'; $string['fieldupdated'] = 'Field updated'; $string['fieldwidth'] = 'Width'; $string['fieldwidthlistview'] = 'Width (in pixels) in list view'; $string['fieldwidthsingleview'] = 'Width (in pixels) in single view'; $string['file'] = 'File'; $string['filesnotgenerated'] = 'Not all files were generated: {$a}'; $string['filtername'] = 'Database auto-linking'; $string['footer'] = 'Footer'; $string['forcelinkname'] = 'Forced name for the link'; $string['foundnorecords'] = 'No records found. Clear all'; $string['foundrecords'] = 'Found {$a->num} out of {$a->max} records. Clear all'; $string['fromfile'] = 'Import from zip file'; $string['fromfile_help'] = 'The import from zip file feature allows you to browse for and upload a preset zip of templates and fields.'; $string['generateerror'] = 'Not all files generated!'; $string['header'] = 'Header'; $string['headeraddtemplate'] = 'Define the interface for adding or editing entries.'; $string['headerasearchtemplate'] = 'Define the interface for the advanced search.'; $string['headercsstemplate'] = 'Add custom CSS to templates.'; $string['headerjstemplate'] = 'Add custom JavaScript for the List, Single or Add entry templates.'; $string['headerlisttemplate'] = 'Define the appearance of List view.'; $string['headerrsstemplate'] = 'Define the appearance of entries in RSS feeds.'; $string['headersingletemplate'] = 'Define the appearance of Single view.'; $string['checkbox'] = 'Checkboxes'; $string['id'] = 'Entry ID'; $string['chooseexportfields'] = 'Choose the fields you wish to export'; $string['chooseexportformat'] = 'Choose the format you wish to export to'; $string['chooseorupload'] = 'Choose file'; $string['choosepreset'] = 'Preset file'; $string['expired'] = 'Sorry, this activity closed on {$a} and is no longer available'; $string['importentries'] = 'Import entries'; $string['importpreset'] = 'Import preset'; $string['importapreset'] = 'Import a preset'; $string['importsuccess'] = 'Preset applied.'; $string['importpresetmissingcapability'] = 'You don\'t have permission to import a preset.'; $string['includeapproval'] = 'Include approval status'; $string['includefiles'] = 'Include files in export'; $string['includetags'] = 'Include tags'; $string['includetime'] = 'Include time added/modified'; $string['includeuserdetails'] = 'Include user details'; $string['indicator:cognitivedepth'] = 'Database cognitive'; $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in a Database activity.'; $string['indicator:cognitivedepthdef'] = 'Database cognitive'; $string['indicator:cognitivedepthdef_help'] = 'The participant has reached this percentage of the cognitive engagement offered by the Database activities during this analysis interval (Levels = No view, View, Submit)'; $string['indicator:cognitivedepthdef_link'] = 'Learning_analytics_indicators#Cognitive_depth'; $string['indicator:socialbreadth'] = 'Database social'; $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in a Database activity.'; $string['indicator:socialbreadthdef'] = 'Database social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Database activities during this analysis interval (Levels = No participation, Participant alone)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; $string['insufficiententries'] = 'more entries needed to view this database'; $string['intro'] = 'Description'; $string['invalidaccess'] = 'This page was not accessed correctly'; $string['invalidfieldid'] = 'Field ID is incorrect'; $string['invalidfieldname'] = 'Please choose another name for this field'; $string['invalidfieldtype'] = 'Field type is incorrect'; $string['invalidid'] = 'Incorrect data ID'; $string['missingfieldtype'] = 'Field type for {$a->name} not found'; $string['missingfieldtypes'] = 'The following fields do not have their corresponding field types installed and will not be included in the Add entry form. You need to check the Add entry template and remove any field names.'; $string['missingfieldtypeimport'] = 'The following fields were not imported because their corresponding field types are not installed:'; $string['unknown'] = 'Unknown field'; $string['invalidpreset'] = '{$a} is not a preset.'; $string['invalidrecord'] = 'Incorrect record'; $string['invalidurl'] = 'The URL you just entered is not valid'; $string['importandapply'] = 'Import preset and apply'; $string['importpreset_desc'] = 'The preset will be applied to this activity, creating fields and templates. It won\'t appear in the list of presets.'; $string['jstemplate'] = 'Custom JavaScript'; $string['latitude'] = 'Latitude'; $string['latlong'] = 'Coordinates'; $string['latlongboth'] = 'Both latitude and longitude are required.'; $string['latlongdownloadallhint'] = 'Download link for all entries as KML'; $string['latlongkmllabelling'] = 'How to label items in KML files (Google Earth)'; $string['latlonglinkservicesdisplayed'] = 'Link-out services to display'; $string['latlongotherfields'] = 'Other fields'; $string['list'] = 'View list'; $string['listview'] = 'List view'; $string['listtemplate'] = 'List view template'; $string['longitude'] = 'Longitude'; $string['manageapproved'] = 'Allow editing of approved entries'; $string['manageapproved_help'] = 'Allow students to edit or delete their entries after they’ve been approved. Edited entries will need to be reapproved.'; $string['managefields'] = 'Manage fields'; $string['mapexistingfield'] = 'Map to {$a}'; $string['mapnewfield'] = 'Create a new field'; $string['mappingwarning'] = 'All old fields not mapped to a new field will be lost and all data in that field will be removed.'; $string['mapping:fieldstocreate'] = 'Fields to be created: {$a}'; $string['mapping:fieldstodelete'] = 'Existing fields to be deleted: {$a}'; $string['mapping:warningmessagedeleteandcreate'] = 'If fields to be deleted are of the same type as fields to be created, you may be able to map them in Map fields.'; $string['mapping:warningmessagedelete'] = 'If fields to be deleted are of the same type as new fields in the preset you\'re applying, you may be able to map them in Map fields.'; $string['mapping:mapfields'] = 'Map fields'; $string['mapping:applypresets'] = 'Apply preset'; $string['mapping:dialogtitle:usepreset'] = 'Apply preset {$a}?'; $string['maxentries'] = 'Maximum number of entries'; $string['maxentries_help'] = 'The maximum number of entries a student is allowed to submit for this activity.'; $string['maxsize'] = 'Maximum size'; $string['menu'] = 'Menu'; $string['menuchoose'] = 'Choose...'; $string['missingdata'] = 'Data ID or object must be provided for field class.'; $string['missingfield'] = 'Programmer error: You must specify field and/or data when defining field class.'; $string['modulename'] = 'Database'; $string['modulename_help'] = 'Database enables participants to create, maintain, and search a collection of entries. The structure of the entries is defined using different types of fields, such as short text, dropdown list, text area, URL, image, or file. As a teacher, you can comment and rate entries. You can also allow students to rate and comment on entries (peer evaluation). Ratings can be aggregated to form a final grade, which is recorded in the gradebook. If the Database auto-linking filter is enabled, any entries in a database will be automatically linked where the words or phrases appear within the course. You can use Database to: * Create a collaborative collection of links, books, book reviews, journal references, etc. * Display student-created photos, posters, websites or poems for peer comment and review.'; $string['modulename_link'] = 'mod/data/view'; $string['modulenameplural'] = 'Databases'; $string['more'] = 'More'; $string['moreurl'] = 'More URL'; $string['movezipfailed'] = 'Can\'t move zip'; $string['multientry'] = 'Repeated entry'; $string['multimenu'] = 'Menu (Multi-select)'; $string['multipletags'] = 'Multiple tags found! Template not saved'; $string['newentry'] = 'New entry'; $string['newfield'] = 'Create a field'; $string['newfield_help'] = 'A field allows the input of data. Each entry in a database activity can have multiple fields of multiple types such as a date field, which allows participants to select a day, month and year from a drop-down menu, a picture field, which allows participants to upload an image file, or a checkbox field, which allows participants to select one or more options. Each field must have a unique field name. The field description is optional.'; $string['noaccess'] = 'You do not have access to this page'; $string['nodefinedfields'] = 'New preset has no defined fields!'; $string['nofieldcontent'] = 'Field content not found'; $string['nofieldindatabase'] = 'There are no fields defined for this database.'; $string['nofields'] = 'No fields yet'; $string['nolisttemplate'] = 'List view template is not yet defined'; $string['nomatch'] = 'No matching entries found!'; $string['nomaximum'] = 'No maximum'; $string['nopreviewavailable'] = 'No preview available for {$a}'; $string['norecords'] = 'No entries yet'; $string['notapproved'] = 'Pending approval'; $string['notapprovederror'] = 'Entry is not approved yet.'; $string['notinjectivemap'] = 'Not an injective map'; $string['notemplates'] = 'No templates yet'; $string['notopenyet'] = 'Sorry, this activity is not available until {$a}'; $string['number'] = 'Number'; $string['numberrssarticles'] = 'Entries in the RSS feed'; $string['numnotapproved'] = 'Pending'; $string['numrecords'] = '{$a} entries'; $string['ods'] = 'ODS (OpenOffice)'; $string['openafterclose'] = 'You have specified an open date after the close date'; $string['optionaldescription'] = 'Short description (optional)'; $string['optionalfilename'] = 'Filename (optional)'; $string['other'] = 'Other'; $string['otherfields'] = 'All other fields'; $string['overwrite'] = 'Overwrite'; $string['overrwritedesc'] = 'Replace existing preset with this name and overwrite its contents'; $string['overwritesettings'] = 'Overwrite current settings such as comments, ratings, etc.'; $string['page-mod-data-x'] = 'Any database activity module page'; $string['pagesize'] = 'Entries per page'; $string['participants'] = 'Participants'; $string['picture'] = 'Picture'; $string['pluginadministration'] = 'Database activity administration'; $string['pluginname'] = 'Database'; $string['portfolionotfile'] = 'Export to a portfolio rather than a file (csv and leap2a only)'; $string['presetdeleted'] = 'Preset deleted.'; $string['presetinfo'] = 'Saving as a preset will publish this template. Other users may be able to use it in their databases.'; $string['presetnotselected'] = 'No preset has been selected.'; $string['presets'] = 'Presets'; $string['presetshelp'] = 'Choose a preset to use as a starting point.'; $string['preview'] = 'Preview of {$a}'; $string['previewaction'] = 'Preview'; $string['privacy:metadata:commentpurpose'] = 'Comments on database records'; $string['privacy:metadata:data_content'] = 'The content of a field'; $string['privacy:metadata:data_content:fieldid'] = 'Field definition ID'; $string['privacy:metadata:data_content:content'] = 'Content'; $string['privacy:metadata:data_content:content1'] = 'Additional content 1'; $string['privacy:metadata:data_content:content2'] = 'Additional content 2'; $string['privacy:metadata:data_content:content3'] = 'Additional content 3'; $string['privacy:metadata:data_content:content4'] = 'Additional content 4'; $string['privacy:metadata:data_records'] = 'Records in the database activity'; $string['privacy:metadata:data_records:userid'] = 'User who created the record'; $string['privacy:metadata:data_records:groupid'] = 'Group'; $string['privacy:metadata:data_records:timecreated'] = 'Time when record was created'; $string['privacy:metadata:data_records:timemodified'] = 'Time when record was last modified'; $string['privacy:metadata:data_records:approved'] = 'Approval status'; $string['privacy:metadata:datafieldnpluginsummary'] = 'Fields for database activity module'; $string['privacy:metadata:filepurpose'] = 'File attached to the database record'; $string['privacy:metadata:tagpurpose'] = 'Tags on database records'; $string['privacy:metadata:ratingpurpose'] = 'Ratings on database records'; $string['radiobutton'] = 'Radio buttons'; $string['recordapproved'] = 'Entry approved'; $string['recorddeleted'] = 'Entry deleted'; $string['recorddisapproved'] = 'Entry unapproved'; $string['recordsnotsaved'] = 'No entry was saved. Please check the format of the uploaded file.'; $string['recordssaved'] = 'entries saved'; $string['removealldatatags'] = 'All database tags'; $string['requireapproval'] = 'Approval required'; $string['requireapproval_help'] = 'A teacher has to approve entries before they’re visible to everyone.'; $string['required'] = 'Required'; $string['requiredentries'] = 'Entries required for completion (old)'; $string['requiredentries_help'] = "If set, a message is displayed stating the number of entries required for completion. Note that this setting is not connected to activity completion. For entries required for activity completion, the new Activity completion setting 'Require entries' should be used. To remove this setting completely, set to none, then save changes. Please use the Entries required fields in the Activity completion section instead."; $string['requiredentrieswarning'] = 'This setting has been replaced by an Activity completion setting "Require entries"'; $string['requiredentriestoview'] = 'Entries required before viewing'; $string['requiredentriestoview_help'] = 'The number of entries a student is required to add before they can view entries from other students. This is not compatible with the database auto-linking filter.'; $string['requiredfield'] = 'Required field'; $string['resetalltemplates'] = 'Reset all templates'; $string['resetalltemplatesconfirmtitle'] = 'Reset all templates?'; $string['resetalltemplatesconfirm'] = 'You\'re about to remove all templates for your current preset. If you want to restore the templates later, you need to choose the preset again in the \'Presets\' tab.'; $string['resetsettings'] = 'Reset filters'; $string['resettemplate'] = 'Reset current template'; $string['resettemplateconfirmtitle'] = 'Reset template?'; $string['resettemplateconfirm'] = 'This will permanently remove the {$a} for your current preset.'; $string['resizingimages'] = 'Resizing image thumbnails...'; $string['rows'] = 'rows'; $string['rssglobaldisabled'] = 'Disabled. See site configuration variables.'; $string['rsstemplate'] = 'RSS template'; $string['rsstitletemplate'] = 'RSS title template'; $string['rsstype'] = 'RSS feed for this activity'; $string['save'] = 'Save'; $string['saveandadd'] = 'Save and add another'; $string['saveandview'] = 'Save and view'; $string['saveaspreset'] = 'Publish preset on this site'; $string['saveaspreset_help'] = 'The save as preset feature publishes the templates and fields as a preset which others on the site can then use. (You may delete it from the list of presets at any time.)'; $string['savedataaspreset'] = 'Save all fields and templates and publish as preset on this site'; $string['saveaspresetmissingcapability'] = 'The user does not have permission to save the database as a preset.'; $string['savesettings'] = 'Save settings'; $string['savesuccess'] = 'Preset saved. Preview preset'; $string['search'] = 'Search'; $string['search:activity'] = 'Database - activity information'; $string['search:entry'] = 'Database - entries'; $string['searchresults'] = 'Entries containing "{$a}"'; $string['selectedrequired'] = 'All selected required'; $string['selectexportoptions'] = 'Select export options'; $string['selectfields'] = 'Select fields'; $string['selectfordeletion'] = 'Select entry for deletion'; $string['showall'] = 'Show all entries'; $string['showmore'] = 'Show more'; $string['single'] = 'View single'; $string['singleview'] = 'Single view'; $string['singletemplate'] = 'Single view template'; $string['startbuilding'] = 'Start building your activity'; $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['tags'] = 'Tags'; $string['teachersandstudents'] = '{$a->teachers} and {$a->students}'; $string['templates'] = 'Templates'; $string['templatereset'] = 'Template reset'; $string['templateresetall'] = 'All templates reset'; $string['templatesnavigation'] = 'Templates tertiary navigation'; $string['templatesaved'] = 'Template saved'; $string['text'] = 'Text'; $string['textarea'] = 'Text area'; $string['timeadded'] = 'Time added'; $string['timemodified'] = 'Time modified'; $string['type'] = 'Field type'; $string['undefinedprocessactionmethod'] = 'No action method defined in Data_Preset to handle action "{$a}".'; $string['unsupportedfields'] = 'Unsupported fields'; $string['unsupportedfieldslist'] = 'The following fields cannot be exported:'; $string['updatefield'] = 'Update an existing field'; $string['uploadfile'] = 'Upload file'; $string['uploadrecords'] = 'Import entries'; $string['uploadrecords_help'] = 'Import entries that you have exported from another database, either via CSV or a ZIP containing a CSV file (if files are included in the export). Alternatively, to create a CSV file for importing, add one entry to the database and then export it. Edit the CSV file and add more entries.'; $string['uploadrecords_link'] = 'mod/data/import'; $string['url'] = 'URL'; $string['usedate'] = 'Include in search.'; $string['usepredefinedset'] = 'Use predefined set'; $string['usepreset'] = 'Use this preset'; $string['usestandard'] = 'Use a preset'; $string['usestandard_help'] = 'To use a preset available to the whole site, select it from the list. (If you have added a preset to the list using the save as preset feature then you have the option of deleting it.)'; $string['viewfromdate'] = 'Read only from'; $string['viewnavigation'] = 'View mode tertiary navigation'; $string['viewtemplates'] = 'View templates'; $string['viewtodate'] = 'Read only to'; $string['viewtodatevalidation'] = 'The read only to date cannot be before the read only from date.'; $string['wrongdataid'] = 'Wrong data ID provided'; // Deprecated since Moodle 4.5. $string['tagsdeleted'] = 'Database tags have been deleted'; // Deprecated since Moodle 5.0. $string['data:comment'] = 'Write comments'; $string['data:managecomments'] = 'Manage comments'; backup/moodle2/restore_data_stepslib.php000064400000016121152265517740014463 0ustar00. /** * @package mod_data * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the restore steps that will be used by the restore_data_activity_task */ /** * Structure step to restore one data activity */ class restore_data_activity_structure_step extends restore_activity_structure_step { protected function define_structure() { $paths = array(); $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('data', '/activity/data'); $paths[] = new restore_path_element('data_field', '/activity/data/fields/field'); if ($userinfo) { $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_record_tag', '/activity/data/recordstags/tag'); } // Return the paths wrapped into standard activity structure return $this->prepare_activity_structure($paths); } protected function process_data($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. $data->timeavailablefrom = $this->apply_date_offset($data->timeavailablefrom); $data->timeavailableto = $this->apply_date_offset($data->timeavailableto); $data->timeviewfrom = $this->apply_date_offset($data->timeviewfrom); $data->timeviewto = $this->apply_date_offset($data->timeviewto); $data->assesstimestart = $this->apply_date_offset($data->assesstimestart); $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish); if ($data->scale < 0) { // scale found, get mapping $data->scale = -($this->get_mappingid('scale', abs($data->scale))); } // Some old backups can arrive with data->notification = null (MDL-24470) // convert them to proper column default (zero) if (is_null($data->notification)) { $data->notification = 0; } // insert the data record $newitemid = $DB->insert_record('data', $data); $this->apply_activity_instance($newitemid); } protected function process_data_field($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->dataid = $this->get_new_parentid('data'); $data->type = clean_param($data->type, PARAM_ALPHA); // insert the data_fields record $newitemid = $DB->insert_record('data_fields', $data); $this->set_mapping('data_field', $oldid, $newitemid, false); // no files associated } protected function process_data_record($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->userid = $this->get_mappingid('user', $data->userid); $data->groupid = $this->get_mappingid('group', $data->groupid); $data->dataid = $this->get_new_parentid('data'); // insert the data_records record $newitemid = $DB->insert_record('data_records', $data); $this->set_mapping('data_record', $oldid, $newitemid, false); // no files associated } protected function process_data_content($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->fieldid = $this->get_mappingid('data_field', $data->fieldid); $data->recordid = $this->get_new_parentid('data_record'); // insert the data_content record $newitemid = $DB->insert_record('data_content', $data); $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_record_tag($data) { $data = (object)$data; if (!core_tag_tag::is_enabled('mod_data', 'data_records')) { // Tags disabled in server, nothing to process. return; } if (!$itemid = $this->get_mappingid('data_record', $data->itemid)) { // Some orphaned tag, we could not find the data record for it - ignore. return; } $tag = $data->rawname; $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; $data = (object)$data; // Cannot use ratings API, cause, it's missing the ability to specify times (modified/created) $data->contextid = $this->task->get_contextid(); $data->itemid = $this->get_new_parentid('data_record'); if ($data->scaleid < 0) { // scale found, get mapping $data->scaleid = -($this->get_mappingid('scale', abs($data->scaleid))); } $data->rating = $data->value; $data->userid = $this->get_mappingid('user', $data->userid); // We need to check that component and ratingarea are both set here. if (empty($data->component)) { $data->component = 'mod_data'; } if (empty($data->ratingarea)) { $data->ratingarea = 'entry'; } $newitemid = $DB->insert_record('rating', $data); } protected function after_execute() { global $DB; // Add data related files, no need to match by itemname (just internally handled context) $this->add_related_files('mod_data', 'intro', null); // Add content related files, matching by itemname (data_content) $this->add_related_files('mod_data', 'content', 'data_content'); // Adjust the data->defaultsort field if ($defaultsort = $DB->get_field('data', 'defaultsort', array('id' => $this->get_new_parentid('data')))) { if ($defaultsort = $this->get_mappingid('data_field', $defaultsort)) { $DB->set_field('data', 'defaultsort', $defaultsort, array('id' => $this->get_new_parentid('data'))); } } } } backup/moodle2/backup_data_activity_task.class.php000064400000011004152265517740016375 0ustar00. /** * Defines backup_data_activity_task * * @package mod_data * @category backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/data/backup/moodle2/backup_data_stepslib.php'); /** * Provides the steps to perform one complete backup of the Database instance */ class backup_data_activity_task extends backup_activity_task { /** * No specific settings for this activity */ protected function define_my_settings() { } /** * Defines a backup step to store the instance data in the data.xml file */ protected function define_my_steps() { $this->add_step(new backup_data_activity_structure_step('data_structure', 'data.xml')); } /** * Encodes URLs to the index.php and view.php scripts * * @param string $content some HTML text that eventually contains URLs to the activity instance scripts * @return string the content with the URLs encoded */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot,"/"); $baseunquoted = $CFG->wwwroot; // Link to the list of datas. $search = '/(' . $base . '\/mod\/data\/index.php\?id\=)([0-9]+)/'; $content = preg_replace($search, '$@DATAINDEX*$2@$', $content); // Link to the list of datas, urlencoded. $search = '/(' . urlencode($baseunquoted . '/mod/data/index.php?id=') . ')([0-9]+)/'; $content = preg_replace($search, '$@DATAINDEXURLENCODED*$2@$', $content); // Link to data view by moduleid. $search = '/(' . $base . '\/mod\/data\/view.php\?id\=)([0-9]+)/'; $content = preg_replace($search, '$@DATAVIEWBYID*$2@$', $content); // Link to data view by moduleid, urlencoded. $search = '/(' . urlencode($baseunquoted . '/mod/data/view.php?id=') . ')([0-9]+)/'; $content = preg_replace($search, '$@DATAVIEWBYIDURLENCODED*$2@$', $content); // Link to one "record" of the database. $search = '/(' . $base . '\/mod\/data\/view.php\?d\=)([0-9]+)\&(amp;)rid\=([0-9]+)/'; $content = preg_replace($search, '$@DATAVIEWRECORD*$2*$4@$', $content); // Link to one "record" of the database, urlencoded. $search = '/(' . urlencode($baseunquoted . '/mod/data/view.php?d=') . ')([0-9]+)%26rid%3D([0-9]+)/'; $content = preg_replace($search, '$@DATAVIEWRECORDURLENCODED*$2*$3@$', $content); // Link to database view by databaseid. $search = '/(' . $base . '\/mod\/data\/view.php\?d\=)([0-9]+)/'; $content = preg_replace($search, '$@DATAVIEWBYD*$2@$', $content); // Link to database view by databaseid, urlencoded. $search = '/(' . urlencode($baseunquoted . '/mod/data/view.php?d=') . ')([0-9]+)/'; $content = preg_replace($search, '$@DATAVIEWBYDURLENCODED*$2@$', $content); // Link to the edit page. $search = '/(' . $base . '\/mod\/data\/edit.php\?id\=)([0-9]+)/'; $content = preg_replace($search, '$@DATAEDITBYID*$2@$', $content); // Link to the edit page, urlencoded. $search = '/(' . urlencode($baseunquoted . '/mod/data/edit.php?id=') . ')([0-9]+)/'; $content = preg_replace($search, '$@DATAEDITBYIDURLENCODED*$2@$', $content); // Link to the edit page by databaseid. $search = '/(' . $base . '\/mod\/data\/edit.php\?d\=)([0-9]+)/'; $content = preg_replace($search, '$@DATAEDITBYD*$2@$', $content); // Link to the edit page by databaseid, urlencoded. $search = '/(' . urlencode($baseunquoted . '/mod/data/edit.php?d=') . ')([0-9]+)/'; $content = preg_replace($search, '$@DATAEDITBYDURLENCODED*$2@$', $content); return $content; } } backup/moodle2/backup_data_stepslib.php000064400000013575152265517740014257 0ustar00. /** * @package mod_data * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Define all the backup steps that will be used by the backup_data_activity_task */ /** * Define the complete data structure for backup, with file and id annotations */ class backup_data_activity_structure_step extends backup_activity_structure_step { protected function define_structure() { // To know if we are including userinfo $userinfo = $this->get_setting_value('userinfo'); // Define each element separated $data = new backup_nested_element('data', array('id'), array( 'name', 'intro', 'introformat', 'comments', 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto', 'requiredentries', 'requiredentriestoview', 'maxentries', 'rssarticles', 'singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'manageapproved', 'scale', 'assessed', 'assesstimestart', 'assesstimefinish', 'defaultsort', 'defaultsortdir', 'editany', 'notification', 'timemodified', 'config', 'completionentries')); $tags = new backup_nested_element('recordstags'); $tag = new backup_nested_element('tag', array('id'), array('itemid', 'rawname')); $fields = new backup_nested_element('fields'); $field = new backup_nested_element('field', array('id'), array( 'type', 'name', 'description', 'required', 'param1', 'param2', 'param3', 'param4', 'param5', 'param6', 'param7', 'param8', 'param9', 'param10')); $records = new backup_nested_element('records'); $record = new backup_nested_element('record', array('id'), array( 'userid', 'groupid', 'timecreated', 'timemodified', 'approved')); $contents = new backup_nested_element('contents'); $content = new backup_nested_element('content', array('id'), array( 'fieldid', 'content', 'content1', 'content2', 'content3', 'content4')); $ratings = new backup_nested_element('ratings'); $rating = new backup_nested_element('rating', array('id'), array( 'component', 'ratingarea', 'scaleid', 'value', 'userid', 'timecreated', 'timemodified')); // Build the tree $data->add_child($fields); $fields->add_child($field); $data->add_child($records); $records->add_child($record); $record->add_child($contents); $contents->add_child($content); $record->add_child($ratings); $ratings->add_child($rating); $data->add_child($tags); $tags->add_child($tag); // Define sources $data->set_source_table('data', array('id' => backup::VAR_ACTIVITYID)); $field->set_source_sql(' SELECT * FROM {data_fields} WHERE dataid = ?', array(backup::VAR_PARENTID)); // All the rest of elements only happen if we are including user info if ($userinfo) { $record->set_source_table('data_records', array('dataid' => backup::VAR_PARENTID)); $content->set_source_table('data_content', array('recordid' => backup::VAR_PARENTID)); $rating->set_source_table('rating', array('contextid' => backup::VAR_CONTEXTID, 'itemid' => backup::VAR_PARENTID, 'component' => backup_helper::is_sqlparam('mod_data'), 'ratingarea' => backup_helper::is_sqlparam('entry'))); $rating->set_source_alias('rating', 'value'); if (core_tag_tag::is_enabled('mod_data', 'data_records')) { $tag->set_source_sql('SELECT t.id, ti.itemid, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? AND ti.component = ? AND ti.contextid = ?', array( backup_helper::is_sqlparam('data_records'), backup_helper::is_sqlparam('mod_data'), backup::VAR_CONTEXTID)); } } // Define id annotations $data->annotate_ids('scale', 'scale'); $record->annotate_ids('user', 'userid'); $record->annotate_ids('group', 'groupid'); $rating->annotate_ids('scale', 'scaleid'); $rating->annotate_ids('user', 'userid'); // Define file annotations $data->annotate_files('mod_data', 'intro', null); // This file area hasn't itemid $content->annotate_files('mod_data', 'content', 'id'); // By content->id // Return the root element (data), wrapped into standard activity structure return $this->prepare_activity_structure($data); } } backup/moodle2/restore_data_activity_task.class.php000064400000014700152265517740016621 0ustar00. /** * @package mod_data * @subpackage backup-moodle2 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/data/backup/moodle2/restore_data_stepslib.php'); // Because it exists (must) /** * data restore task that provides all the settings and steps to perform one * complete restore of the activity */ class restore_data_activity_task extends restore_activity_task { /** * Define (add) particular settings this activity can have */ protected function define_my_settings() { // No particular settings for this activity } /** * Define (add) particular steps this activity can have */ protected function define_my_steps() { // Data only has one structure step $this->add_step(new restore_data_activity_structure_step('data_structure', 'data.xml')); } /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('data', array( 'intro', 'singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate', 'asearchtemplate'), 'data'); $contents[] = new restore_decode_content('data_fields', array( 'description', 'param1', 'param2', 'param3', 'param4', 'param5', 'param6', 'param7', 'param8', 'param9', 'param10'), 'data_field'); $contents[] = new restore_decode_content('data_content', array( 'content', 'content1', 'content2', 'content3', 'content4')); return $contents; } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { $rules = []; $rules[] = new restore_decode_rule('DATAVIEWBYID', '/mod/data/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('DATAVIEWBYIDURLENCODED', '/mod/data/view.php?id=$1', 'course_module', true); $rules[] = new restore_decode_rule('DATAVIEWBYD', '/mod/data/view.php?d=$1', 'data'); $rules[] = new restore_decode_rule('DATAVIEWBYDURLENCODED', '/mod/data/view.php?d=$1', 'data', true); $rules[] = new restore_decode_rule('DATAINDEX', '/mod/data/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('DATAINDEXURLENCODED', '/mod/data/index.php?id=$1', 'course', true); $rules[] = new restore_decode_rule('DATAVIEWRECORD', '/mod/data/view.php?d=$1&rid=$2', ['data', 'data_record']); $rules[] = new restore_decode_rule( 'DATAVIEWRECORDURLENCODED', '/mod/data/view.php?d=$1&rid=$2', ['data', 'data_record'], true ); $rules[] = new restore_decode_rule('DATAEDITBYID', '/mod/data/edit.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('DATAEDITBYIDURLENCODED', '/mod/data/edit.php?id=$1', 'course_module', true); $rules[] = new restore_decode_rule('DATAEDITBYD', '/mod/data/edit.php?d=$1', 'data'); $rules[] = new restore_decode_rule('DATAEDITBYDURLENCODED', '/mod/data/edit.php?d=$1', 'data', true); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * data logs. It must return one array * of {@link restore_log_rule} objects */ public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('data', 'add', 'view.php?d={data}&rid={data_record}', '{data}'); $rules[] = new restore_log_rule('data', 'update', 'view.php?d={data}&rid={data_record}', '{data}'); $rules[] = new restore_log_rule('data', 'view', 'view.php?id={course_module}', '{data}'); $rules[] = new restore_log_rule('data', 'record delete', 'view.php?id={course_module}', '{data}'); $rules[] = new restore_log_rule('data', 'fields add', 'field.php?d={data}&mode=display&fid={data_field}', '{data_field}'); $rules[] = new restore_log_rule('data', 'fields update', 'field.php?d={data}&mode=display&fid={data_field}', '{data_field}'); $rules[] = new restore_log_rule('data', 'fields delete', 'field.php?d={data}', '[name]'); return $rules; } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { $rules = array(); $rules[] = new restore_log_rule('data', 'view all', 'index.php?id={course}', null); return $rules; } /** * Given a commment area, return the itemname that contains the itemid mappings. * * @param string $commentarea Comment area name e.g. database_entry. * @return string name of the mapping used to determine the itemid. */ public function get_comment_mapping_itemname($commentarea) { if ($commentarea == 'database_entry') { $itemname = 'data_record'; } else { $itemname = parent::get_comment_mapping_itemname($commentarea); } return $itemname; } } backup/moodle1/lib.php000064400000014371152265517740010654 0ustar00. /** * Provides support for the conversion of moodle1 backup to the moodle2 format * Based off of a template @ http://docs.moodle.org/dev/Backup_1.9_conversion_for_developers * * @package mod_data * @copyright 2011 Aparup Banerjee * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Database conversion handler */ class moodle1_mod_data_handler extends moodle1_mod_handler { /** @var moodle1_file_manager */ protected $fileman = null; /** @var int cmid */ protected $moduleid = null; /** * Declare the paths in moodle.xml we are able to convert * * The method returns list of {@link convert_path} instances. * For each path returned, the corresponding conversion method must be * defined. * * Note that the path /MOODLE_BACKUP/COURSE/MODULES/MOD/DATA does not * actually exist in the file. The last element with the module name was * appended by the moodle1_converter class. * * @return array of {@link convert_path} instances */ public function get_paths() { return array( new convert_path('data', '/MOODLE_BACKUP/COURSE/MODULES/MOD/DATA', array( 'newfields' => array( 'introformat' => 0, 'assesstimestart' => 0, 'assesstimefinish' => 0, ) ) ), new convert_path('data_field', '/MOODLE_BACKUP/COURSE/MODULES/MOD/DATA/FIELDS/FIELD') ); } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/DATA * data available */ public function process_data($data) { global $CFG; // get the course module id and context id $instanceid = $data['id']; $cminfo = $this->get_cminfo($instanceid); $this->moduleid = $cminfo['id']; $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $this->moduleid); // replay the upgrade step 2007101512 if (!array_key_exists('asearchtemplate', $data)) { $data['asearchtemplate'] = null; } // replay the upgrade step 2007101513 if (is_null($data['notification'])) { $data['notification'] = 0; } // conditionally migrate to html format in intro if ($CFG->texteditors !== 'textarea') { $data['intro'] = text_to_html($data['intro'], false, false, true); $data['introformat'] = FORMAT_HTML; } // get a fresh new file manager for this instance $this->fileman = $this->converter->get_file_manager($contextid, 'mod_data'); // convert course files embedded into the intro $this->fileman->filearea = 'intro'; $this->fileman->itemid = 0; $data['intro'] = moodle1_converter::migrate_referenced_files($data['intro'], $this->fileman); // @todo: user data - upgrade content to new file storage // add 'export' tag to list and single template. $pattern = '/\#\#delete\#\#(\s+)\#\#approve\#\#/'; $replacement = '##delete##$1##approve##$1##export##'; $data['listtemplate'] = preg_replace($pattern, $replacement, $data['listtemplate']); $data['singletemplate'] = preg_replace($pattern, $replacement, $data['singletemplate']); //@todo: user data - move data comments to comments table //@todo: user data - move data ratings to ratings table // start writing data.xml $this->open_xml_writer("activities/data_{$this->moduleid}/data.xml"); $this->xmlwriter->begin_tag('activity', array('id' => $instanceid, 'moduleid' => $this->moduleid, 'modulename' => 'data', 'contextid' => $contextid)); $this->xmlwriter->begin_tag('data', array('id' => $instanceid)); foreach ($data as $field => $value) { if ($field <> 'id') { $this->xmlwriter->full_tag($field, $value); } } $this->xmlwriter->begin_tag('fields'); return $data; } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/DATA/FIELDS/FIELD * data available */ public function process_data_field($data) { // process database fields $this->write_xml('field', $data, array('/field/id')); } /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/DATA/RECORDS/RECORD * data available */ public function process_data_record($data) { //@todo process user data, and define the convert path in get_paths() above. //$this->write_xml('record', $data, array('/record/id')); } /** * This is executed when we reach the closing tag of our 'data' path */ public function on_data_end() { // finish writing data.xml $this->xmlwriter->end_tag('fields'); $this->xmlwriter->end_tag('data'); $this->xmlwriter->end_tag('activity'); $this->close_xml_writer(); // write inforef.xml $this->open_xml_writer("activities/data_{$this->moduleid}/inforef.xml"); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); foreach ($this->fileman->get_fileids() as $fileid) { $this->write_xml('file', array('id' => $fileid)); } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); } } view.php000064400000054360152265517740006255 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\manager; require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/data/locallib.php'); require_once($CFG->libdir . '/rsslib.php'); /// One of these is necessary! $id = optional_param('id', 0, PARAM_INT); // course module id $d = optional_param('d', 0, PARAM_INT); // database id $rid = optional_param('rid', 0, PARAM_INT); //record id $mode = optional_param('mode', '', PARAM_ALPHA); // Force the browse mode ('single') $filter = optional_param('filter', 0, PARAM_BOOL); // search filter will only be applied when $filter is true $edit = optional_param('edit', -1, PARAM_BOOL); $page = optional_param('page', 0, PARAM_INT); /// These can be added to perform an action on a record $approve = optional_param('approve', 0, PARAM_INT); //approval recordid $disapprove = optional_param('disapprove', 0, PARAM_INT); // disapproval recordid $delete = optional_param('delete', 0, PARAM_INT); //delete recordid $multidelete = optional_param_array('delcheck', null, PARAM_INT); $serialdelete = optional_param('serialdelete', null, PARAM_RAW); $confirm = optional_param('confirm', 0, PARAM_INT); $record = null; if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); } else if ($rid) { $record = $DB->get_record('data_records', ['id' => $rid], '*', MUST_EXIST); $manager = manager::create_from_data_record($record); $cm = $manager->get_coursemodule(); $course = get_course($cm->course); } else { // We must have $d. $data = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($data); $cm = $manager->get_coursemodule(); $course = get_course($cm->course); } $data = $manager->get_instance(); $context = $manager->get_context(); require_login($course, true, $cm); require_once($CFG->dirroot . '/comment/lib.php'); comment::init(); require_capability('mod/data:viewentry', $context); /// Check further parameters that set browsing preferences if (!isset($SESSION->dataprefs)) { $SESSION->dataprefs = array(); } if (!isset($SESSION->dataprefs[$data->id])) { $SESSION->dataprefs[$data->id] = array(); $SESSION->dataprefs[$data->id]['search'] = ''; $SESSION->dataprefs[$data->id]['search_array'] = array(); $SESSION->dataprefs[$data->id]['sort'] = $data->defaultsort; $SESSION->dataprefs[$data->id]['advanced'] = 0; $SESSION->dataprefs[$data->id]['order'] = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC'; } // reset advanced form if (!is_null(optional_param('resetadv', null, PARAM_RAW))) { $SESSION->dataprefs[$data->id]['search_array'] = array(); // we need the redirect to cleanup the form state properly redirect("view.php?id=$cm->id&mode=$mode&search=&advanced=1"); } $advanced = optional_param('advanced', -1, PARAM_INT); if ($advanced == -1) { $advanced = $SESSION->dataprefs[$data->id]['advanced']; } else { if (!$advanced) { // explicitly switched to normal mode - discard all advanced search settings $SESSION->dataprefs[$data->id]['search_array'] = array(); } $SESSION->dataprefs[$data->id]['advanced'] = $advanced; } $search_array = $SESSION->dataprefs[$data->id]['search_array']; if (!empty($advanced)) { $search = ''; //Added to ammend paging error. This error would occur when attempting to go from one page of advanced //search results to another. All fields were reset in the page transfer, and there was no way of determining //whether or not the user reset them. This would cause a blank search to execute whenever the user attempted //to see any page of results past the first. //This fix works as follows: //$paging flag is set to false when page 0 of the advanced search results is viewed for the first time. //Viewing any page of results after page 0 passes the false $paging flag though the URL (see line 523) and the //execution falls through to the second condition below, allowing paging to be set to true. //Paging remains true and keeps getting passed though the URL until a new search is performed //(even if page 0 is revisited). //A false $paging flag generates advanced search results based on the fields input by the user. //A true $paging flag generates davanced search results from the $SESSION global. $paging = optional_param('paging', NULL, PARAM_BOOL); if($page == 0 && !isset($paging)) { $paging = false; } else { $paging = true; } // Now build the advanced search array. list($search_array, $search) = data_build_search_array($data, $paging, $search_array); $SESSION->dataprefs[$data->id]['search_array'] = $search_array; // Make it sticky. } else { $search = optional_param('search', $SESSION->dataprefs[$data->id]['search'], PARAM_NOTAGS); //Paging variable not used for standard search. Set it to null. $paging = NULL; } // Disable search filters if $filter is not true: if (! $filter) { $search = ''; } $SESSION->dataprefs[$data->id]['search'] = $search; // Make it sticky $sort = optional_param('sort', $SESSION->dataprefs[$data->id]['sort'], PARAM_INT); $SESSION->dataprefs[$data->id]['sort'] = $sort; // Make it sticky $order = (optional_param('order', $SESSION->dataprefs[$data->id]['order'], PARAM_ALPHA) == 'ASC') ? 'ASC': 'DESC'; $SESSION->dataprefs[$data->id]['order'] = $order; // Make it sticky $oldperpage = get_user_preferences('data_perpage_'.$data->id, 10); $perpage = optional_param('perpage', $oldperpage, PARAM_INT); if ($perpage < 2) { $perpage = 2; } if ($perpage != $oldperpage) { set_user_preference('data_perpage_'.$data->id, $perpage); } // Trigger module viewed event and completion. $manager->set_module_viewed($course); $urlparams = array('d' => $data->id); if ($record) { $urlparams['rid'] = $record->id; } if ($mode) { $urlparams['mode'] = $mode; } if ($page) { $urlparams['page'] = $page; } if ($filter) { $urlparams['filter'] = $filter; } $pageurl = new moodle_url('/mod/data/view.php', $urlparams); // Initialize $PAGE, compute blocks. $PAGE->set_url($pageurl); if (($edit != -1) and $PAGE->user_allowed_editing()) { $USER->editing = $edit; } $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); /// RSS and CSS and JS meta $meta = ''; if (!empty($CFG->enablerssfeeds) && !empty($CFG->data_enablerssfeeds) && $data->rssarticles > 0) { $rsstitle = $courseshortname . ': ' . format_string($data->name); rss_add_http_header($context, 'mod_data', $data, $rsstitle); } if ($data->csstemplate) { $PAGE->requires->css('/mod/data/css.php?d='.$data->id); } if ($data->jstemplate) { $PAGE->requires->js('/mod/data/js.php?d='.$data->id, true); } /// Print the page header // Note: MDL-19010 there will be further changes to printing header and blocks. // The code will be much nicer than this eventually. if ($PAGE->user_allowed_editing() && !$PAGE->theme->haseditswitch) { // Change URL parameter and block display string value depending on whether editing is enabled or not if ($PAGE->user_is_editing()) { $urlediting = 'off'; $strediting = get_string('blockseditoff'); } else { $urlediting = 'on'; $strediting = get_string('blocksediton'); } $editurl = new moodle_url($CFG->wwwroot.'/mod/data/view.php', ['id' => $cm->id, 'edit' => $urlediting]); $PAGE->set_button($OUTPUT->single_button($editurl, $strediting)); } if ($mode == 'asearch') { $PAGE->navbar->add(get_string('search')); } $PAGE->add_body_class('limitedwidth'); $titleparts = [ format_string($data->name), format_string($course->fullname), ]; if (!empty(trim($search))) { // Indicate search results on page title when searching. array_unshift($titleparts, get_string('searchresults', 'data', s($search))); } else if (!empty($delete) && empty($confirm)) { // Displaying the delete confirmation page. array_unshift($titleparts, get_string('deleteentry', 'data')); } else if ($record !== null || $mode == 'single') { // Indicate on the page tile if the user is viewing this page on single view mode. array_unshift($titleparts, get_string('single', 'data')); } $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->set_heading($course->fullname); $PAGE->force_settings_menu(true); if ($delete && data_user_can_manage_entry($delete, $data, $context)) { $PAGE->activityheader->disable(); } // Check to see if groups are being used here. // We need the most up to date current group value. Make sure it is updated at this point. $currentgroup = groups_get_activity_group($cm, true); $groupmode = groups_get_activity_groupmode($cm); $canmanageentries = has_capability('mod/data:manageentries', $context); echo $OUTPUT->header(); if (!$manager->has_fields()) { // It's a brand-new database. There are no fields. $renderer = $manager->get_renderer(); echo $renderer->render_database_zero_state($manager); echo $OUTPUT->footer(); // Don't check the rest of the options. There is no field, there is nothing else to work with. exit; } // Detect entries not approved yet and show hint instead of not found error. if ($record and !data_can_view_record($data, $record, $currentgroup, $canmanageentries)) { throw new \moodle_exception('notapprovederror', 'data'); } // Do we need to show a link to the RSS feed for the records? //this links has been Settings (database activity administration) block /*if (!empty($CFG->enablerssfeeds) && !empty($CFG->data_enablerssfeeds) && $data->rssarticles > 0) { echo '
'; rss_print_link($context->id, $USER->id, 'mod_data', $data->id, get_string('rsstype')); echo '
'; echo '
'; }*/ if ($data->intro and empty($page) and empty($record) and $mode != 'single') { $options = new stdClass(); $options->noclean = true; } /// Delete any requested records if ($delete && data_user_can_manage_entry($delete, $data, $context)) { if ($confirm) { require_sesskey(); if (data_delete_record($delete, $data, $course->id, $cm->id)) { echo $OUTPUT->notification(get_string('recorddeleted','data'), 'notifysuccess'); } } else { // Print a confirmation page $userfieldsapi = \core_user\fields::for_userpic()->excluding('id'); $allnamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $dbparams = array($delete); if ($deleterecord = $DB->get_record_sql("SELECT dr.*, $allnamefields FROM {data_records} dr JOIN {user} u ON dr.userid = u.id WHERE dr.id = ?", $dbparams, MUST_EXIST)) { // Need to check this is valid. if ($deleterecord->dataid == $data->id) { // Must be from this database echo $OUTPUT->heading(get_string('deleteentry', 'mod_data'), 2, 'mb-4'); $deletebutton = new single_button( new moodle_url('/mod/data/view.php?d=' . $data->id . '&delete=' . $delete . '&confirm=1'), get_string('delete'), 'post', single_button::BUTTON_DANGER ); echo $OUTPUT->confirm(get_string('confirmdeleterecord','data'), $deletebutton, 'view.php?d='.$data->id); $records[] = $deleterecord; $parser = $manager->get_template('singletemplate'); echo $parser->parse_entries($records); echo $OUTPUT->footer(); exit; } } } } // Multi-delete. if ($serialdelete) { $multidelete = json_decode($serialdelete); } if ($multidelete && $canmanageentries) { if ($confirm) { require_sesskey(); foreach ($multidelete as $value) { data_delete_record($value, $data, $course->id, $cm->id); } } else { $validrecords = array(); $recordids = array(); foreach ($multidelete as $value) { $userfieldsapi = \core_user\fields::for_userpic()->excluding('id'); $allnamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $dbparams = array('id' => $value); if ($deleterecord = $DB->get_record_sql("SELECT dr.*, $allnamefields FROM {data_records} dr JOIN {user} u ON dr.userid = u.id WHERE dr.id = ?", $dbparams)) { // Need to check this is valid. if ($deleterecord->dataid == $data->id) { // Must be from this database. $validrecords[] = $deleterecord; $recordids[] = $deleterecord->id; } } } $serialiseddata = json_encode($recordids); $submitactions = array('d' => $data->id, 'sesskey' => sesskey(), 'confirm' => '1', 'serialdelete' => $serialiseddata); $action = new moodle_url('/mod/data/view.php', $submitactions); $cancelurl = new moodle_url('/mod/data/view.php', array('d' => $data->id)); $deletebutton = new single_button($action, get_string('delete'), 'post', single_button::BUTTON_DANGER); echo $OUTPUT->confirm(get_string('confirmdeleterecords', 'data'), $deletebutton, $cancelurl); $parser = $manager->get_template('listtemplate'); echo $parser->parse_entries($validrecords); echo $OUTPUT->footer(); exit; } } // If data activity closed dont let students in. // No need to display warnings because activity dates are displayed at the top of the page. list($showactivity, $warnings) = data_get_time_availability_status($data, $canmanageentries); if ($showactivity) { if ($mode == 'asearch') { $maxcount = 0; data_print_preference_form($data, $perpage, $search, $sort, $order, $search_array, $advanced, $mode); } else { // Approve or disapprove any requested records $approvecap = has_capability('mod/data:approve', $context); if (($approve || $disapprove) && $approvecap) { require_sesskey(); $newapproved = $approve ? true : false; $recordid = $newapproved ? $approve : $disapprove; if ($approverecord = $DB->get_record('data_records', array('id' => $recordid))) { // Need to check this is valid if ($approverecord->dataid == $data->id) { // Must be from this database data_approve_entry($approverecord->id, $newapproved); $msgkey = $newapproved ? 'recordapproved' : 'recorddisapproved'; echo $OUTPUT->notification(get_string($msgkey, 'data'), 'notifysuccess'); } } } $numentries = data_numentries($data); /// Check the number of entries required against the number of entries already made (doesn't apply to teachers) if ($data->entriesleft = data_get_entries_left_to_add($data, $numentries, $canmanageentries)) { $strentrieslefttoadd = get_string('entrieslefttoadd', 'data', $data); echo $OUTPUT->notification($strentrieslefttoadd); } /// Check the number of entries required before to view other participant's entries against the number of entries already made (doesn't apply to teachers) $requiredentries_allowed = true; if ($data->entrieslefttoview = data_get_entries_left_to_view($data, $numentries, $canmanageentries)) { $strentrieslefttoaddtoview = get_string('entrieslefttoaddtoview', 'data', $data); echo $OUTPUT->notification($strentrieslefttoaddtoview); $requiredentries_allowed = false; } if ($groupmode != NOGROUPS) { $returnurl = new moodle_url('/mod/data/view.php', ['d' => $data->id, 'mode' => $mode, 'search' => s($search), 'sort' => s($sort), 'order' => s($order)]); echo html_writer::div(groups_print_activity_menu($cm, $returnurl, true), 'mb-3'); } // Search for entries. list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($data, $cm, $context, $mode, $currentgroup, $search, $sort, $order, $page, $perpage, $advanced, $search_array, $record); $hasrecords = !empty($records); if ($maxcount == 0) { $renderer = $manager->get_renderer(); echo $renderer->render_empty_database($manager); echo $OUTPUT->footer(); // There is no entry, so makes no sense to check different views, pagination, etc. exit; } $actionbar = new \mod_data\output\action_bar($data->id, $pageurl); echo $actionbar->get_view_action_bar($hasrecords, $mode); // Advanced search form doesn't make sense for single (redirects list view). if ($maxcount && $mode != 'single') { data_print_preference_form($data, $perpage, $search, $sort, $order, $search_array, $advanced, $mode); } if (empty($records)) { if ($maxcount){ $a = new stdClass(); $a->max = $maxcount; $a->reseturl = "view.php?id=$cm->id&mode=$mode&search=&advanced=0"; echo $OUTPUT->box_start(); echo get_string('foundnorecords', 'data', $a); echo $OUTPUT->box_end(); } else { echo $OUTPUT->box_start(); echo get_string('norecords', 'data'); echo $OUTPUT->box_end(); } } else { // We have some records to print. $formurl = new moodle_url('/mod/data/view.php', ['d' => $data->id, 'sesskey' => sesskey()]); echo html_writer::start_tag('form', ['action' => $formurl, 'method' => 'post']); if ($maxcount != $totalcount) { $a = new stdClass(); $a->num = $totalcount; $a->max = $maxcount; $a->reseturl = "view.php?id=$cm->id&mode=$mode&search=&advanced=0"; echo $OUTPUT->box_start(); echo get_string('foundrecords', 'data', $a); echo $OUTPUT->box_end(); } if ($mode == 'single') { // Single template $baseurl = '/mod/data/view.php'; $baseurlparams = ['d' => $data->id, 'mode' => 'single']; if (!empty($search)) { $baseurlparams['filter'] = 1; } if (!empty($page)) { $baseurlparams['page'] = $page; } $baseurl = new moodle_url($baseurl, $baseurlparams); echo $OUTPUT->box_start('', 'data-singleview-content'); require_once($CFG->dirroot.'/rating/lib.php'); if ($data->assessed != RATING_AGGREGATE_NONE) { $ratingoptions = new stdClass; $ratingoptions->context = $context; $ratingoptions->component = 'mod_data'; $ratingoptions->ratingarea = 'entry'; $ratingoptions->items = $records; $ratingoptions->aggregate = $data->assessed;//the aggregation method $ratingoptions->scaleid = $data->scale; $ratingoptions->userid = $USER->id; $ratingoptions->returnurl = $baseurl->out(); $ratingoptions->assesstimestart = $data->assesstimestart; $ratingoptions->assesstimefinish = $data->assesstimefinish; $rm = new rating_manager(); $records = $rm->get_ratings($ratingoptions); } $options = [ 'search' => $search, 'page' => $page, 'baseurl' => $baseurl, ]; $parser = $manager->get_template('singletemplate', $options); echo $parser->parse_entries($records); echo $OUTPUT->box_end(); } else { // List template. $baseurl = '/mod/data/view.php'; $baseurlparams = ['d' => $data->id, 'advanced' => $advanced, 'paging' => $paging]; if (!empty($search)) { $baseurlparams['filter'] = 1; } $baseurl = new moodle_url($baseurl, $baseurlparams); echo $OUTPUT->box_start('', 'data-listview-content'); echo $data->listtemplateheader; $options = [ 'search' => $search, 'page' => $page, 'baseurl' => $baseurl, ]; $parser = $manager->get_template('listtemplate', $options); echo $parser->parse_entries($records); echo $data->listtemplatefooter; echo $OUTPUT->box_end(); } $stickyfooter = new mod_data\output\view_footer( $manager, $totalcount, $page, $nowperpage, $baseurl, $parser ); echo $OUTPUT->render($stickyfooter); echo html_writer::end_tag('form'); } } $search = trim($search); if (empty($records)) { $records = array(); } } echo $OUTPUT->footer(); rsslib.php000064400000016756152265517740006610 0ustar00. /** * This file contains all the common stuff to be used for RSS in the Database Module * * @package mod_data * @category rss * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_data\manager; defined('MOODLE_INTERNAL') || die(); /** * Generates the Database modules RSS Feed * * @param strClass $context the context the feed should be created under * @param array $args array of arguments to be used in the creation of the RSS Feed * @return NULL|string NULL if there is nothing to return, or the file path of the cached file if there is */ function data_rss_get_feed($context, $args) { global $CFG, $DB; // Check CFG->data_enablerssfeeds. if (empty($CFG->data_enablerssfeeds)) { debugging("DISABLED (module configuration)"); return null; } $dataid = clean_param($args[3], PARAM_INT); $cm = get_coursemodule_from_instance('data', $dataid, 0, false, MUST_EXIST); if ($cm) { $modcontext = context_module::instance($cm->id); //context id from db should match the submitted one if ($context->id != $modcontext->id || !has_capability('mod/data:viewentry', $modcontext)) { return null; } } $data = $DB->get_record('data', array('id' => $dataid), '*', MUST_EXIST); if (!rss_enabled_for_mod('data', $data, false, true)) { return null; } $manager = manager::create_from_instance($data); $sql = data_rss_get_sql($data); //get the cache file info $filename = rss_get_file_name($data, $sql); $cachedfilepath = rss_get_file_full_name('mod_data', $filename); //Is the cache out of date? $cachedfilelastmodified = 0; if (file_exists($cachedfilepath)) { $cachedfilelastmodified = filemtime($cachedfilepath); } //if the cache is more than 60 seconds old and there's new stuff $dontrecheckcutoff = time()-60; if ( $dontrecheckcutoff > $cachedfilelastmodified && data_rss_newstuff($data, $cachedfilelastmodified)) { require_once($CFG->dirroot . '/mod/data/lib.php'); // Get the first field in the list (a hack for now until we have a selector) if (!$firstfield = $DB->get_record_sql('SELECT id,name FROM {data_fields} WHERE dataid = ? ORDER by id', array($data->id), true)) { return null; } if (!$records = $DB->get_records_sql($sql, array(), 0, $data->rssarticles)) { return null; } $firstrecord = array_shift($records); // Get the first and put it back array_unshift($records, $firstrecord); // Now create all the articles $items = array(); foreach ($records as $record) { $recordarray = array(); array_push($recordarray, $record); $item = new stdClass(); // guess title or not if (!empty($data->rsstitletemplate)) { $parser = $manager->get_template('rsstitletemplate'); $item->title = $parser->parse_entries($recordarray); } else { // else we guess $item->title = strip_tags($DB->get_field('data_content', 'content', array('fieldid'=>$firstfield->id, 'recordid'=>$record->id))); } $parser = $manager->get_template('rsstemplate'); $item->description = $parser->parse_entries($recordarray); $item->pubdate = $record->timecreated; $item->link = $CFG->wwwroot.'/mod/data/view.php?d='.$data->id.'&rid='.$record->id; array_push($items, $item); } $course = $DB->get_record('course', array('id'=>$data->course)); $coursecontext = context_course::instance($course->id); $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext)); // First all rss feeds common headers. $header = rss_standard_header($courseshortname . ': ' . format_string($data->name, true, array('context' => $context)), $CFG->wwwroot."/mod/data/view.php?d=".$data->id, format_text($data->intro, $data->introformat, array('context' => $context))); if (!empty($header)) { $articles = rss_add_items($items); } // Now all rss feeds common footers. if (!empty($header) && !empty($articles)) { $footer = rss_standard_footer(); } // Now, if everything is ok, concatenate it. if (!empty($header) && !empty($articles) && !empty($footer)) { $rss = $header.$articles.$footer; //Save the XML contents to file. $status = rss_save_file('mod_data', $filename, $rss); } } return $cachedfilepath; } /** * Creates and returns a SQL query for the items that would be included in the RSS Feed * * @param stdClass $data database instance object * @param int $time epoch timestamp to compare time-modified to, 0 for all or a proper value * @return string SQL query string to get the items for the databse RSS Feed */ function data_rss_get_sql($data, $time=0) { //do we only want new posts? if ($time) { $time = " AND dr.timemodified > '$time'"; } else { $time = ''; } $approved = ($data->approval) ? ' AND dr.approved = 1 ' : ' '; $sql = "SELECT dr.*, u.firstname, u.lastname FROM {data_records} dr, {user} u WHERE dr.dataid = {$data->id} $approved AND dr.userid = u.id $time ORDER BY dr.timecreated DESC"; return $sql; } /** * If there is new stuff in since $time this returns true * Otherwise it returns false. * * @param stdClass $data the data activity object * @param int $time timestamp * @return bool true if there is new stuff for the RSS Feed */ function data_rss_newstuff($data, $time) { global $DB; $sql = data_rss_get_sql($data, $time); $recs = $DB->get_records_sql($sql, null, 0, 1);//limit of 1. If we get even 1 back we have new stuff return ($recs && !empty($recs)); } /** * Given a database object, deletes all cached RSS files associated with it. * * @param stdClass $data */ function data_rss_delete_file($data) { global $CFG; require_once("$CFG->libdir/rsslib.php"); rss_delete_file('mod_data', $data); } field.php000064400000043357152265517740006372 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use core\notification; use mod_data\local\importer\preset_existing_importer; use mod_data\local\importer\preset_importer; use mod_data\local\importer\preset_upload_importer; use mod_data\manager; require_once('../../config.php'); require_once('lib.php'); require_once($CFG->dirroot.'/mod/data/preset_form.php'); $id = optional_param('id', 0, PARAM_INT); // course module id $d = optional_param('d', 0, PARAM_INT); // database id $fid = optional_param('fid', 0 , PARAM_INT); // update field id $newtype = optional_param('newtype','',PARAM_ALPHA); // type of the new field $mode = optional_param('mode','',PARAM_ALPHA); $action = optional_param('action', '', PARAM_ALPHA); $fullname = optional_param('fullname', '', PARAM_PATH); // Directory the preset is in. $defaultsort = optional_param('defaultsort', 0, PARAM_INT); $defaultsortdir = optional_param('defaultsortdir', 0, PARAM_INT); $cancel = optional_param('cancel', 0, PARAM_BOOL); if ($cancel) { $mode = 'list'; } $url = new moodle_url('/mod/data/field.php'); if ($fid !== 0) { $url->param('fid', $fid); } if ($newtype !== '') { $url->param('newtype', $newtype); } if ($mode !== '') { $url->param('mode', $mode); } if ($defaultsort !== 0) { $url->param('defaultsort', $defaultsort); } if ($defaultsortdir !== 0) { $url->param('defaultsortdir', $defaultsortdir); } if ($cancel !== 0) { $url->param('cancel', $cancel); } if ($action !== '') { $url->param('action', $action); } if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); $url->param('id', $cm->id); } else { // We must have $d. $instance = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($instance); $cm = $manager->get_coursemodule(); $course = get_course($cm->course); $url->param('d', $d); } $PAGE->set_url($url); $data = $manager->get_instance(); $context = $manager->get_context(); require_login($course, true, $cm); require_capability('mod/data:managetemplates', $context); $actionbar = new \mod_data\output\action_bar($data->id, $PAGE->url); $PAGE->add_body_class('limitedwidth'); $PAGE->set_heading($course->fullname); $PAGE->activityheader->disable(); // Fill in missing properties needed for updating of instance. $data->course = $cm->course; $data->cmidnumber = $cm->idnumber; $data->instance = $cm->instance; /************************************ * Data Processing * ***********************************/ $renderer = $manager->get_renderer(); if ($action == 'finishimport' && confirm_sesskey()) { $overwritesettings = optional_param('overwritesettings', false, PARAM_BOOL); $importer = preset_importer::create_from_parameters($manager); $importer->finish_import_process($overwritesettings, $data); } switch ($mode) { case 'add': ///add a new field if (confirm_sesskey() and $fieldinput = data_submitted()){ //$fieldinput->name = data_clean_field_name($fieldinput->name); /// Only store this new field if it doesn't already exist. if (($fieldinput->name == '') or data_fieldname_exists($fieldinput->name, $data->id)) { $displaynoticebad = get_string('invalidfieldname','data'); } else { /// Check for arrays and convert to a comma-delimited string data_convert_arrays_to_strings($fieldinput); /// Create a field object to collect and store the data safely $type = required_param('type', PARAM_FILE); $field = data_get_field_new($type, $data); if (!empty($validationerrors = $field->validate($fieldinput))) { $displaynoticebad = html_writer::alist($validationerrors); $mode = 'new'; $newtype = $type; break; } $field->define_field($fieldinput); $field->insert_field(); /// Update some templates data_append_new_field_to_templates($data, $fieldinput->name); $displaynoticegood = get_string('fieldadded','data'); } } break; case 'update': ///update a field if (confirm_sesskey() and $fieldinput = data_submitted()){ //$fieldinput->name = data_clean_field_name($fieldinput->name); if (($fieldinput->name == '') or data_fieldname_exists($fieldinput->name, $data->id, $fieldinput->fid)) { $displaynoticebad = get_string('invalidfieldname','data'); } else { /// Check for arrays and convert to a comma-delimited string data_convert_arrays_to_strings($fieldinput); /// Create a field object to collect and store the data safely $field = data_get_field_from_id($fid, $data); if (!empty($validationerrors = $field->validate($fieldinput))) { $displaynoticebad = html_writer::alist($validationerrors); $mode = 'display'; break; } $oldfieldname = $field->field->name; $field->field->name = trim($fieldinput->name); $field->field->description = trim($fieldinput->description); $field->field->required = !empty($fieldinput->required) ? 1 : 0; for ($i=1; $i<=10; $i++) { if (isset($fieldinput->{'param'.$i})) { $field->field->{'param'.$i} = trim($fieldinput->{'param'.$i}); } else { $field->field->{'param'.$i} = ''; } } $field->update_field(); /// Update the templates. data_replace_field_in_templates($data, $oldfieldname, $field->field->name); $displaynoticegood = get_string('fieldupdated','data'); } } break; case 'delete': // Delete a field if (confirm_sesskey()){ if ($confirm = optional_param('confirm', 0, PARAM_INT)) { // Delete the field completely if ($field = data_get_field_from_id($fid, $data)) { $field->delete_field(); // Update the templates. data_replace_field_in_templates($data, $field->field->name, ''); // Update the default sort field if ($fid == $data->defaultsort) { $rec = new stdClass(); $rec->id = $data->id; $rec->defaultsort = 0; $rec->defaultsortdir = 0; $DB->update_record('data', $rec); } $displaynoticegood = get_string('fielddeleted', 'data'); } } else { $titleparts = [ get_string('deletefield', 'data'), format_string($data->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); data_print_header($course,$cm,$data, false); echo $OUTPUT->heading(get_string('deletefield', 'data'), 2, 'mb-4'); // Print confirmation message. $field = data_get_field_from_id($fid, $data); if ($field->type === 'unknown') { $fieldtypename = get_string('unknown', 'data'); } else { $fieldtypename = $field->name(); } echo $OUTPUT->confirm('' . $fieldtypename . ': ' . s($field->field->name) . '

' . get_string('confirmdeletefield', 'data'), 'field.php?d=' . $data->id . '&mode=delete&fid=' . $fid . '&confirm=1', 'field.php?d=' . $data->id, ['type' => single_button::BUTTON_DANGER]); echo $OUTPUT->footer(); exit; } } break; case 'sort': // Set the default sort parameters if (confirm_sesskey()) { $rec = new stdClass(); $rec->id = $data->id; $rec->defaultsort = $defaultsort; $rec->defaultsortdir = $defaultsortdir; $DB->update_record('data', $rec); redirect($CFG->wwwroot.'/mod/data/field.php?d='.$data->id, get_string('changessaved'), 2); exit; } break; case 'usepreset': $importer = preset_importer::create_from_parameters($manager); if (!$importer->needs_mapping() || $action == 'notmapping') { $backurl = new moodle_url('/mod/data/field.php', ['id' => $cm->id]); if ($importer->import(false)) { notification::success(get_string('importsuccess', 'mod_data')); } else { notification::error(get_string('cannotapplypreset', 'mod_data')); } redirect($backurl); } $PAGE->navbar->add(get_string('usestandard', 'data')); $fieldactionbar = $actionbar->get_fields_mapping_action_bar(); data_print_header($course, $cm, $data, false, $fieldactionbar); $importer = new preset_existing_importer($manager, $fullname); echo $renderer->importing_preset($data, $importer); echo $OUTPUT->footer(); exit; default: break; } /// Print the browsing interface ///get the list of possible fields (plugins) $plugins = core_component::get_plugin_list('datafield'); $menufield = array(); foreach ($plugins as $plugin=>$fulldir){ if (!is_dir($fulldir)) { continue; } $menufield[$plugin] = get_string('pluginname', 'datafield_'.$plugin); //get from language files } asort($menufield); //sort in alphabetical order $PAGE->force_settings_menu(true); $PAGE->set_pagetype('mod-data-field-' . $newtype); $titleparts = [ format_string($data->name), format_string($course->fullname), ]; if (($mode == 'new') && (!empty($newtype))) { // Adding a new field. array_unshift($titleparts, get_string('newfield', 'data')); $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); data_print_header($course, $cm, $data, 'fields'); echo $OUTPUT->heading(get_string('newfield', 'data')); $field = data_get_field_new($newtype, $data); $field->display_edit_field(); } else if ($mode == 'display' && confirm_sesskey()) { /// Display/edit existing field array_unshift($titleparts, get_string('editfield', 'data')); $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); data_print_header($course, $cm, $data, 'fields'); echo $OUTPUT->heading(get_string('editfield', 'data')); $field = data_get_field_from_id($fid, $data); $field->display_edit_field(); } else { /// Display the main listing of all fields array_unshift($titleparts, get_string('managefields', 'data')); $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $hasfields = $manager->has_fields(); // Check if it is an empty database with no fields. if (!$hasfields) { echo $OUTPUT->header(); echo $renderer->render_fields_zero_state($manager); echo $OUTPUT->footer(); // Don't check the rest of the options. There is no field, there is nothing else to work with. exit; } $fieldactionbar = $actionbar->get_fields_action_bar(true); data_print_header($course, $cm, $data, 'fields', $fieldactionbar); echo $OUTPUT->box_start(); echo get_string('fieldshelp', 'data'); echo $OUTPUT->box_end(); echo $OUTPUT->box_start('d-flex flex-row-reverse'); echo $OUTPUT->render($actionbar->get_create_fields(true)); echo $OUTPUT->box_end(); $table = new html_table(); $table->head = [ get_string('fieldname', 'data'), get_string('type', 'data'), get_string('required', 'data'), get_string('fielddescription', 'data'), ' ', ]; $table->align = ['left', 'left', 'left', 'left']; $table->wrap = [false,false,false,false]; $table->responsive = false; $fieldrecords = $manager->get_field_records(); $missingfieldtypes = []; foreach ($fieldrecords as $fieldrecord) { $field = data_get_field($fieldrecord, $data); $baseurl = new moodle_url('/mod/data/field.php', array( 'd' => $data->id, 'fid' => $field->field->id, 'sesskey' => sesskey(), )); $displayurl = new moodle_url($baseurl, array( 'mode' => 'display', )); $deleteurl = new moodle_url($baseurl, array( 'mode' => 'delete', )); $actionmenu = new action_menu(); $actionmenu->set_kebab_trigger(); $actionmenu->set_action_label(get_string('actions')); $actionmenu->set_additional_classes('fields-actions'); // It display a notification when the field type does not exist. if ($field->type === 'unknown') { $missingfieldtypes[] = $field->field->name; $fieltypedata = $field->field->type; } else { $fieltypedata = $field->image() . ' ' . $field->name(); // Edit icon, only displayed when the field type is known. $actionmenu->add(new action_menu_link_secondary( $displayurl, null, get_string('edit'), )); } // Delete. $actionmenu->add(new action_menu_link_secondary( $deleteurl, null, get_string('delete'), )); $actionmenutemplate = $actionmenu->export_for_template($OUTPUT); $table->data[] = [ s($field->field->name), $fieltypedata, $field->field->required ? get_string('yes') : get_string('no'), shorten_text($field->field->description, 30), $OUTPUT->render_from_template('core/action_menu', $actionmenutemplate) ]; } if (!empty($missingfieldtypes)) { echo $OUTPUT->notification(get_string('missingfieldtypes', 'data') . html_writer::alist($missingfieldtypes)); } echo html_writer::table($table); echo '
'; echo '
'; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; $options = array(0 => get_string('ascending', 'data'), 1 => get_string('descending', 'data')); echo html_writer::label(get_string('sortby'), 'menudefaultsortdir', false, array('class' => 'accesshide')); echo html_writer::select($options, 'defaultsortdir', $data->defaultsortdir, false, ['class' => 'form-select']); echo ''; echo '
'; echo '
'; echo '
'; } /// Finish the page echo $OUTPUT->footer(); pix/fields_zero_state.svg000064400000006223152265517740011613 0ustar00 pix/view_zero_state.svg000064400000006055152265517740011322 0ustar00 pix/templates_zero_state.svg000064400000016405152265517740012346 0ustar00 pix/field/menu.svg000064400000001043152265517740010130 0ustar00 pix/field/date.svg000064400000001037152265517740010104 0ustar00 pix/field/checkbox.svg000064400000001147152265517740010757 0ustar00 pix/field/textarea.svg000064400000001065152265517740011005 0ustar00 pix/field/text.svg000064400000001510152265517740010147 0ustar00 pix/field/url.svg000064400000002026152265517740007770 0ustar00 pix/field/picture.svg000064400000001153152265517740010641 0ustar00 pix/field/number.svg000064400000001464152265517740010463 0ustar00 pix/field/multimenu.svg000064400000001043152265517740011203 0ustar00 pix/field/latlong.svg000064400000002105152265517740010624 0ustar00 pix/field/file.svg000064400000001013152265517740010100 0ustar00 pix/field/radiobutton.svg000064400000000631152265517740011520 0ustar00 pix/noentries_zero_state.svg000064400000012215152265517740012351 0ustar00 pix/monologo.svg000064400000003671152265517740007743 0ustar00 lib.php000064400000404737152265517740006060 0ustar00. /** * @package mod_data * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_data\manager; defined('MOODLE_INTERNAL') || die(); // Some constants define ('DATA_MAX_ENTRIES', 50); define ('DATA_PERPAGE_SINGLE', 1); define ('DATA_FIRSTNAME', -1); define ('DATA_LASTNAME', -2); define ('DATA_APPROVED', -3); define ('DATA_TIMEADDED', 0); define ('DATA_TIMEMODIFIED', -4); define ('DATA_TAGS', -5); define ('DATA_CAP_EXPORT', 'mod/data:viewalluserpresets'); // Users having assigned the default role "Non-editing teacher" can export database records // Using the mod/data capability "viewalluserpresets" existing in Moodle 1.9.x. // In Moodle >= 2, new roles may be introduced and used instead. define('DATA_PRESET_COMPONENT', 'mod_data'); define('DATA_PRESET_FILEAREA', 'site_presets'); define('DATA_PRESET_CONTEXT', SYSCONTEXTID); define('DATA_EVENT_TYPE_OPEN', 'open'); define('DATA_EVENT_TYPE_CLOSE', 'close'); require_once(__DIR__ . '/deprecatedlib.php'); /** * @package mod_data * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_field_base { // Base class for Database Field Types (see field/*/field.class.php) /** @var string Subclasses must override the type with their name */ var $type = 'unknown'; /** @var object The database object that this field belongs to */ var $data = NULL; /** @var object The field object itself, if we know it */ var $field = NULL; /** @var int Width of the icon for this fieldtype */ var $iconwidth = 16; /** @var int Width of the icon for this fieldtype */ var $iconheight = 16; /** @var object course module or cmifno */ var $cm; /** @var object activity context */ var $context; /** @var priority for globalsearch indexing */ protected static $priority = self::NO_PRIORITY; /** priority value for invalid fields regarding indexing */ const NO_PRIORITY = 0; /** priority value for minimum priority */ const MIN_PRIORITY = 1; /** priority value for low priority */ const LOW_PRIORITY = 2; /** priority value for high priority */ const HIGH_PRIORITY = 3; /** priority value for maximum priority */ const MAX_PRIORITY = 4; /** @var bool whether the field is used in preview mode. */ protected $preview = false; /** * Constructor function * * @global object * @uses CONTEXT_MODULE * @param int $field * @param int $data * @param int $cm */ function __construct($field=0, $data=0, $cm=0) { // Field or data or both, each can be id or object global $DB; if (empty($field) && empty($data)) { throw new \moodle_exception('missingfield', 'data'); } if (!empty($field)) { if (is_object($field)) { $this->field = $field; // Programmer knows what they are doing, we hope } else if (!$this->field = $DB->get_record('data_fields', array('id'=>$field))) { throw new \moodle_exception('invalidfieldid', 'data'); } if (empty($data)) { if (!$this->data = $DB->get_record('data', array('id'=>$this->field->dataid))) { throw new \moodle_exception('invalidid', 'data'); } } } if (empty($this->data)) { // We need to define this properly if (!empty($data)) { if (is_object($data)) { $this->data = $data; // Programmer knows what they are doing, we hope } else if (!$this->data = $DB->get_record('data', array('id'=>$data))) { throw new \moodle_exception('invalidid', 'data'); } } else { // No way to define it! throw new \moodle_exception('missingdata', 'data'); } } if ($cm) { $this->cm = $cm; } else { $this->cm = get_coursemodule_from_instance('data', $this->data->id); } if (empty($this->field)) { // We need to define some default values $this->define_default_field(); } $this->context = context_module::instance($this->cm->id); } /** * Return the field type name. * * @return string the filed type. */ public function get_name(): string { return $this->field->name; } /** * Return if the field type supports preview. * * Fields without a preview cannot be displayed in the preset preview. * * @return bool if the plugin supports preview. */ public function supports_preview(): bool { return false; } /** * Generate a fake data_content for this field to be used in preset previews. * * Data plugins must override this method and support_preview in order to enable * preset preview for this field. * * @param int $recordid the fake record id * @return stdClass the fake record */ public function get_data_content_preview(int $recordid): stdClass { $message = get_string('nopreviewavailable', 'mod_data', $this->field->name); return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => "$message", 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } /** * Set the field to preview mode. * * @param bool $preview the new preview value */ public function set_preview(bool $preview) { $this->preview = $preview; } /** * Get the field preview value. * * @return bool */ public function get_preview(): bool { return $this->preview; } /** * This field just sets up a default field object * * @return bool */ function define_default_field() { global $OUTPUT; if (empty($this->data->id)) { echo $OUTPUT->notification('Programmer error: dataid not defined in field class'); } $this->field = new stdClass(); $this->field->id = 0; $this->field->dataid = $this->data->id; $this->field->type = $this->type; $this->field->param1 = ''; $this->field->param2 = ''; $this->field->param3 = ''; $this->field->name = ''; $this->field->description = ''; $this->field->required = false; return true; } /** * Set up the field object according to data in an object. Now is the time to clean it! * * @return bool */ function define_field($data) { $this->field->type = $this->type; $this->field->dataid = $this->data->id; $this->field->name = trim($data->name); $this->field->description = trim($data->description); $this->field->required = !empty($data->required) ? 1 : 0; if (isset($data->param1)) { $this->field->param1 = trim($data->param1); } if (isset($data->param2)) { $this->field->param2 = trim($data->param2); } if (isset($data->param3)) { $this->field->param3 = trim($data->param3); } if (isset($data->param4)) { $this->field->param4 = trim($data->param4); } if (isset($data->param5)) { $this->field->param5 = trim($data->param5); } return true; } /** * Insert a new field in the database * We assume the field object is already defined as $this->field * * @global object * @return bool */ function insert_field() { global $DB, $OUTPUT; if (empty($this->field)) { echo $OUTPUT->notification('Programmer error: Field has not been defined yet! See define_field()'); return false; } $this->field->id = $DB->insert_record('data_fields',$this->field); // Trigger an event for creating this field. $event = \mod_data\event\field_created::create(array( 'objectid' => $this->field->id, 'context' => $this->context, 'other' => array( 'fieldname' => $this->field->name, 'dataid' => $this->data->id ) )); $event->trigger(); return true; } /** * Update a field in the database * * @global object * @return bool */ function update_field() { global $DB; $DB->update_record('data_fields', $this->field); // Trigger an event for updating this field. $event = \mod_data\event\field_updated::create(array( 'objectid' => $this->field->id, 'context' => $this->context, 'other' => array( 'fieldname' => $this->field->name, 'dataid' => $this->data->id ) )); $event->trigger(); return true; } /** * Delete a field completely * * @global object * @return bool */ function delete_field() { global $DB; if (!empty($this->field->id)) { $manager = manager::create_from_instance($this->data); // Get the field before we delete it. $field = $DB->get_record('data_fields', array('id' => $this->field->id)); $this->delete_content(); $DB->delete_records('data_fields', array('id'=>$this->field->id)); // Trigger an event for deleting this field. $event = \mod_data\event\field_deleted::create(array( 'objectid' => $this->field->id, 'context' => $this->context, 'other' => array( 'fieldname' => $this->field->name, 'dataid' => $this->data->id ) )); if (!$manager->has_fields() && $manager->has_records()) { $DB->delete_records('data_records', ['dataid' => $this->data->id]); } $event->add_record_snapshot('data_fields', $field); $event->trigger(); } return true; } /** * Print the relevant form element in the ADD template for this field * * @global object * @param int $recordid * @return string */ function display_add_field($recordid=0, $formdata=null) { global $DB, $OUTPUT; if ($formdata) { $fieldname = 'field_' . $this->field->id; $content = $formdata->$fieldname; } else if ($recordid) { $content = $DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid)); } else { $content = ''; } // beware get_field returns false for new, empty records MDL-18567 if ($content===false) { $content=''; } $str = '
'; $str .= ''; $str .= '
'; return $str; } /** * Print the relevant form element to define the attributes for this field * viewable by teachers only. * * @global object * @global object * @return void Output is echo'd */ function display_edit_field() { global $CFG, $DB, $OUTPUT; if (empty($this->field)) { // No field has been defined yet, try and make one $this->define_default_field(); } // Throw an exception if field type doen't exist. Anyway user should never access to edit a field with an unknown fieldtype. if ($this->type === 'unknown') { throw new \moodle_exception(get_string('missingfieldtype', 'data', (object)['name' => $this->field->name])); } echo $OUTPUT->box_start('generalbox boxaligncenter boxwidthwide'); echo '
'."\n"; echo ''."\n"; if (empty($this->field->id)) { echo ''."\n"; } else { echo ''."\n"; echo ''."\n"; } echo ''."\n"; echo ''."\n"; echo $OUTPUT->heading($this->name(), 3); $filepath = $CFG->dirroot . '/mod/data/field/' . $this->type . '/mod.html'; $templatename = 'datafield_' . $this->type . '/' . $this->type; try { $templatefilepath = \core\output\mustache_template_finder::get_template_filepath($templatename); $templatefileexists = true; } catch (moodle_exception $e) { if (!file_exists($filepath)) { // Neither file exists. throw new \moodle_exception(get_string('missingfieldtype', 'data', (object)['name' => $this->field->name])); } $templatefileexists = false; } if ($templatefileexists) { // Give out templated Bootstrap formatted form fields. $data = $this->get_field_params(); echo $OUTPUT->render_from_template($templatename, $data); } else { // Fall back to display mod.html for backward compatibility. require_once($filepath); } $actionbuttons = html_writer::start_div(); $actionbuttons .= html_writer::tag('input', null, [ 'type' => 'submit', 'name' => 'cancel', 'value' => get_string('cancel'), 'class' => 'btn btn-secondary mx-1' ]); $actionbuttons .= html_writer::tag('input', null, [ 'type' => 'submit', 'value' => get_string('save'), 'class' => 'btn btn-primary mx-1' ]); $actionbuttons .= html_writer::end_div(); $stickyfooter = new core\output\sticky_footer($actionbuttons); echo $OUTPUT->render($stickyfooter); echo '
'; echo $OUTPUT->box_end(); } /** * Validates params of fieldinput data. Overwrite to validate fieldtype specific data. * * You are expected to return an array like ['paramname' => 'Error message for paramname param'] if there is an error, * return an empty array if everything is fine. * * @param stdClass $fieldinput The field input data to check * @return array $errors if empty validation was fine, otherwise contains one or more error messages */ public function validate(stdClass $fieldinput): array { return []; } /** * Return the data_content of the field, or generate it if it is in preview mode. * * @param int $recordid the record id * @return stdClass|bool the record data or false if none */ protected function get_data_content(int $recordid) { global $DB; if ($this->preview) { return $this->get_data_content_preview($recordid); } return $DB->get_record( 'data_content', ['fieldid' => $this->field->id, 'recordid' => $recordid] ); } /** * Display the content of the field in browse mode * * @global object * @param int $recordid * @param object $template * @return bool|string */ function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content || !isset($content->content)) { return ''; } $options = new stdClass(); if ($this->field->param1 == '1') { // We are autolinking this field, so disable linking within us. $options->filter = false; } $options->para = false; $format = !empty($content->content1) && !empty(trim($content->content1)) ? $content->content1 : null; $str = format_text($content->content, $format, $options); return $str; } /** * Update the content of one data field in the data_content table * @global object * @param int $recordid * @param mixed $value * @param string $name * @return bool */ function update_content($recordid, $value, $name=''){ global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $content->content = clean_param($value, PARAM_NOTAGS); if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } /** * Delete all content associated with the field * * @global object * @param int $recordid * @return bool */ function delete_content($recordid=0) { global $DB; if ($recordid) { $conditions = array('fieldid'=>$this->field->id, 'recordid'=>$recordid); } else { $conditions = array('fieldid'=>$this->field->id); } $rs = $DB->get_recordset('data_content', $conditions); if ($rs->valid()) { $fs = get_file_storage(); foreach ($rs as $content) { $fs->delete_area_files($this->context->id, 'mod_data', 'content', $content->id); } } $rs->close(); return $DB->delete_records('data_content', $conditions); } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { return !empty($value); } /** * Just in case a field needs to print something before the whole form */ function print_before_form() { } /** * Just in case a field needs to print something after the whole form */ function print_after_form() { } /** * Returns the sortable field for the content. By default, it's just content * but for some plugins, it could be content 1 - content4 * * @return string */ function get_sort_field() { return 'content'; } /** * Returns the SQL needed to refer to the column. Some fields may need to CAST() etc. * * @param string $fieldname * @return string $fieldname */ function get_sort_sql($fieldname) { return $fieldname; } /** * Returns the name/type of the field * * @return string */ function name() { return get_string('fieldtypelabel', "datafield_$this->type"); } /** * Prints the respective type icon * * @global object * @return string */ function image() { global $OUTPUT; return $OUTPUT->image_icon('icon', $this->type, 'datafield_' . $this->type); } /** * Per default, it is assumed that fields support text exporting. * Override this (return false) on fields not supporting text exporting. * * @return bool true */ function text_export_supported() { return true; } /** * Per default, it is assumed that fields do not support file exporting. Override this (return true) * on fields supporting file export. You will also have to implement export_file_value(). * * @return bool true if field will export a file, false otherwise */ public function file_export_supported(): bool { return false; } /** * Per default, does not return a file (just null). * Override this in fields class, if you want your field to export a file content. * In case you are exporting a file value, export_text_value() should return the corresponding file name. * * @param stdClass $record * @return null|string the file content as string or null, if no file content is being provided */ public function export_file_value(stdClass $record): null|string { return null; } /** * Per default, a field does not support the import of files. * * A field type can overwrite this function and return true. In this case it also has to implement the function * import_file_value(). * * @return false means file imports are not supported */ public function file_import_supported(): bool { return false; } /** * Returns a stored_file object for exporting a file of a given record. * * @param int $contentid content id * @param string $filecontent the content of the file as string * @param string $filename the filename the file should have */ public function import_file_value(int $contentid, string $filecontent, string $filename): void { return; } /** * Per default, return the record's text value only from the "content" field. * Override this in fields class if necessary. * * @param stdClass $record * @return string */ public function export_text_value(stdClass $record) { if ($this->text_export_supported()) { return $record->content; } return ''; } /** * @param string $relativepath * @return bool false */ function file_ok($relativepath) { return false; } /** * Returns the priority for being indexed by globalsearch * * @return int */ public static function get_priority() { return static::$priority; } /** * Returns the presentable string value for a field content. * * The returned string should be plain text. * * @param stdClass $content * @return string */ public static function get_content_value($content) { return trim($content->content, "\r\n "); } /** * Return the plugin configs for external functions, * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the field configs to null (maybe there is a private key for a service or something similar there). $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = null; } return $configs; } /** * Function to let field define their parameters. * * This method that should be overridden by the datafield plugins * when they need to define their data. * * @return array */ protected function get_field_params(): array { // Name and description of the field. $data = [ 'name' => $this->field->name, 'description' => $this->field->description, ]; // Whether the field is required. if (isset($this->field->required)) { $data['required'] = $this->field->required; } // Add all the field parameters. for ($i = 1; $i <= 10; $i++) { if (isset($this->field->{"param$i"})) { $data["param$i"] = $this->field->{"param$i"}; } } return $data; } } /** * Given a template and a dataid, generate a default case template * * @param stdClass $data the mod_data record. * @param string $template the template name * @param int $recordid the entry record * @param bool $form print a form instead of data * @param bool $update if the function update the $data object or not * @return string the template content or an empty string if no content is available (for instance, when database has no fields). */ function data_generate_default_template(&$data, $template, $recordid = 0, $form = false, $update = true) { global $DB; if (!$data || !$template) { return ''; } // These templates are empty by default (they have no content). $emptytemplates = [ 'csstemplate', 'jstemplate', 'listtemplateheader', 'listtemplatefooter', 'rsstitletemplate', ]; if (in_array($template, $emptytemplates)) { return ''; } $manager = manager::create_from_instance($data); if (empty($manager->get_fields())) { // No template will be returned if there are no fields. return ''; } $templateclass = \mod_data\template::create_default_template($manager, $template, $form); $templatecontent = $templateclass->get_template_content(); if ($update) { // Update the database instance. $newdata = new stdClass(); $newdata->id = $data->id; $newdata->{$template} = $templatecontent; $DB->update_record('data', $newdata); $data->{$template} = $templatecontent; } return $templatecontent; } /** * Build the form elements to manage tags for a record. * * @param int|bool $recordid * @param string[] $selected raw tag names * @return string */ function data_generate_tag_form($recordid = false, $selected = []) { global $CFG, $DB, $OUTPUT, $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'; $tagcollid = \core_tag_area::get_collection('mod_data', 'data_records'); $tags = []; $selectedtags = []; if ($showstandard) { $tags += $DB->get_records_menu('tag', array('isstandard' => 1, 'tagcollid' => $tagcollid), $namefield, 'id,' . $namefield . ' as fieldname'); } if ($recordid) { $selectedtags += core_tag_tag::get_item_tags_array('mod_data', 'data_records', $recordid); } if (!empty($selected)) { list($sql, $params) = $DB->get_in_or_equal($selected, SQL_PARAMS_NAMED); $params['tagcollid'] = $tagcollid; $sql = "SELECT id, $namefield FROM {tag} WHERE tagcollid = :tagcollid AND rawname $sql"; $selectedtags += $DB->get_records_sql_menu($sql, $params); } $tags += $selectedtags; $str .= ''; if (has_capability('moodle/tag:manage', context_system::instance()) && $showstandard) { $url = new moodle_url('/tag/manage.php', array('tc' => core_tag_area::get_collection('mod_data', 'data_records'))); $str .= ' ' . $OUTPUT->action_link($url, get_string('managestandardtags', 'tag')); } $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 * form templates. Set $newfieldname as '' if you want to delete the * field from the form. * * @global object * @param object $data * @param string $searchfieldname * @param string $newfieldname * @return bool */ function data_replace_field_in_templates($data, $searchfieldname, $newfieldname) { global $DB; $newdata = (object)['id' => $data->id]; $update = false; $templates = ['listtemplate', 'singletemplate', 'asearchtemplate', 'addtemplate', 'rsstemplate']; foreach ($templates as $templatename) { if (empty($data->$templatename)) { continue; } $search = [ '[[' . $searchfieldname . ']]', '[[' . $searchfieldname . '#id]]', '[[' . $searchfieldname . '#name]]', '[[' . $searchfieldname . '#description]]', ]; if (empty($newfieldname)) { $replace = ['', '', '', '']; } else { $replace = [ '[[' . $newfieldname . ']]', '[[' . $newfieldname . '#id]]', '[[' . $newfieldname . '#name]]', '[[' . $newfieldname . '#description]]', ]; } $newdata->{$templatename} = str_ireplace($search, $replace, $data->{$templatename} ?? ''); $update = true; } if (!$update) { return true; } return $DB->update_record('data', $newdata); } /** * Appends a new field at the end of the form template. * * @global object * @param object $data * @param string $newfieldname * @return bool if the field has been added or not */ function data_append_new_field_to_templates($data, $newfieldname): bool { global $DB, $OUTPUT; $newdata = (object)['id' => $data->id]; $update = false; $templates = ['singletemplate', 'addtemplate', 'rsstemplate']; foreach ($templates as $templatename) { if (empty($data->$templatename) || strpos($data->$templatename, "[[$newfieldname]]") !== false || strpos($data->$templatename, "##otherfields##") !== false ) { continue; } $newdata->$templatename = $data->$templatename; $fields = [[ 'fieldname' => '[[' . $newfieldname . '#name]]', 'fieldcontent' => '[[' . $newfieldname . ']]', ]]; $newdata->$templatename .= $OUTPUT->render_from_template( 'mod_data/fields_otherfields', ['fields' => $fields, 'classes' => 'added_field'] ); $update = true; } if (!$update) { return false; } return $DB->update_record('data', $newdata); } /** * given a field name * this function creates an instance of the particular subfield class * * @global object * @param string $name * @param object $data * @return object|bool */ function data_get_field_from_name($name, $data){ global $DB; $field = $DB->get_record('data_fields', array('name'=>$name, 'dataid'=>$data->id)); if ($field) { return data_get_field($field, $data); } else { return false; } } /** * given a field id * this function creates an instance of the particular subfield class * * @global object * @param int $fieldid * @param object $data * @return bool|object */ function data_get_field_from_id($fieldid, $data){ global $DB; $field = $DB->get_record('data_fields', array('id'=>$fieldid, 'dataid'=>$data->id)); if ($field) { return data_get_field($field, $data); } else { return false; } } /** * given a field id * this function creates an instance of the particular subfield class * * @global object * @param string $type * @param object $data * @return object */ function data_get_field_new($type, $data) { global $CFG; $type = clean_param($type, PARAM_ALPHA); $filepath = $CFG->dirroot.'/mod/data/field/'.$type.'/field.class.php'; // It should never access this method if the subfield class doesn't exist. if (!file_exists($filepath)) { throw new \moodle_exception('invalidfieldtype', 'data'); } require_once($filepath); $newfield = 'data_field_'.$type; $newfield = new $newfield(0, $data); return $newfield; } /** * returns a subclass field object given a record of the field, used to * invoke plugin methods * input: $param $field - record from db * * @global object * @param stdClass $field the field record * @param stdClass $data the data instance * @param stdClass|null $cm optional course module data * @return data_field_base the field object instance or data_field_base if unkown type */ function data_get_field(stdClass $field, stdClass $data, ?stdClass $cm=null): data_field_base { global $CFG; if (!isset($field->type)) { return new data_field_base($field); } $field->type = clean_param($field->type, PARAM_ALPHA); $filepath = $CFG->dirroot.'/mod/data/field/'.$field->type.'/field.class.php'; if (!file_exists($filepath)) { return new data_field_base($field); } require_once($filepath); $newfield = 'data_field_'.$field->type; $newfield = new $newfield($field, $data, $cm); return $newfield; } /** * Given record object (or id), returns true if the record belongs to the current user * * @global object * @global object * @param mixed $record record object or id * @return bool */ function data_isowner($record) { global $USER, $DB; if (!isloggedin()) { // perf shortcut return false; } if (!is_object($record)) { if (!$record = $DB->get_record('data_records', array('id'=>$record))) { return false; } } return ($record->userid == $USER->id); } /** * has a user reached the max number of entries? * * @param object $data * @return bool */ function data_atmaxentries($data){ if (!$data->maxentries){ return false; } else { return (data_numentries($data) >= $data->maxentries); } } /** * returns the number of entries already made by this user * * @global object * @global object * @param object $data * @return int */ function data_numentries($data, $userid=null) { global $USER, $DB; if ($userid === null) { $userid = $USER->id; } $sql = 'SELECT COUNT(*) FROM {data_records} WHERE dataid=? AND userid=?'; return $DB->count_records_sql($sql, array($data->id, $userid)); } /** * function that takes in a dataid and adds a record * this is used everytime an add template is submitted * * @global object * @global object * @param object $data * @param int $groupid * @param int $userid * @param bool $approved If specified, and the user has the capability to approve entries, then this value * will be used as the approved status of the new record * @return bool */ function data_add_record($data, $groupid = 0, $userid = null, bool $approved = true) { global $USER, $DB; $cm = get_coursemodule_from_instance('data', $data->id); $context = context_module::instance($cm->id); $record = new stdClass(); $record->userid = $userid ?? $USER->id; $record->dataid = $data->id; $record->groupid = $groupid; $record->timecreated = $record->timemodified = time(); if (has_capability('mod/data:approve', $context)) { $record->approved = $approved; } else { $record->approved = 0; } $record->id = $DB->insert_record('data_records', $record); // Trigger an event for creating this record. $event = \mod_data\event\record_created::create(array( 'objectid' => $record->id, 'context' => $context, 'other' => array( 'dataid' => $data->id ) )); $event->trigger(); $course = get_course($cm->course); data_update_completion_state($data, $course, $cm); return $record->id; } /** * check the multple existence any tag in a template * * check to see if there are 2 or more of the same tag being used. * * @global object * @param int $dataid, * @param string $template * @return bool */ function data_tags_check($dataid, $template) { global $DB, $OUTPUT; // first get all the possible tags $fields = $DB->get_records('data_fields', array('dataid'=>$dataid)); // then we generate strings to replace $tagsok = true; // let's be optimistic foreach ($fields as $field){ $pattern="/\[\[" . preg_quote($field->name, '/') . "\]\]/i"; if (preg_match_all($pattern, $template, $dummy)>1){ $tagsok = false; echo $OUTPUT->notification('[['.$field->name.']] - '.get_string('multipletags','data')); } } // else return true return $tagsok; } /** * Adds an instance of a data * * @param stdClass $data * @param mod_data_mod_form $mform * @return int intance id */ function data_add_instance($data, $mform = null) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/data/locallib.php'); if (empty($data->assessed)) { $data->assessed = 0; } if (empty($data->ratingtime) || empty($data->assessed)) { $data->assesstimestart = 0; $data->assesstimefinish = 0; } $data->timemodified = time(); $data->id = $DB->insert_record('data', $data); // Add calendar events if necessary. data_set_events($data); if (!empty($data->completionexpected)) { \core_completion\api::update_completion_date_event($data->coursemodule, 'data', $data->id, $data->completionexpected); } data_grade_item_update($data); return $data->id; } /** * updates an instance of a data * * @global object * @param object $data * @return bool */ function data_update_instance($data) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/data/locallib.php'); $data->timemodified = time(); if (!empty($data->instance)) { $data->id = $data->instance; } if (empty($data->assessed)) { $data->assessed = 0; } if (empty($data->ratingtime) or empty($data->assessed)) { $data->assesstimestart = 0; $data->assesstimefinish = 0; } if (empty($data->notification)) { $data->notification = 0; } $DB->update_record('data', $data); // Add calendar events if necessary. data_set_events($data); $completionexpected = (!empty($data->completionexpected)) ? $data->completionexpected : null; \core_completion\api::update_completion_date_event($data->coursemodule, 'data', $data->id, $completionexpected); data_grade_item_update($data); return true; } /** * deletes an instance of a data * * @global object * @param int $id * @return bool */ function data_delete_instance($id) { // takes the dataid global $DB, $CFG; if (!$data = $DB->get_record('data', array('id'=>$id))) { return false; } $cm = get_coursemodule_from_instance('data', $data->id); $context = context_module::instance($cm->id); // Delete all information related to fields. $fields = $DB->get_records('data_fields', ['dataid' => $id]); foreach ($fields as $field) { $todelete = data_get_field($field, $data, $cm); $todelete->delete_field(); } // Remove old calendar events. $events = $DB->get_records('event', array('modulename' => 'data', 'instance' => $id)); foreach ($events as $event) { $event = calendar_event::load($event); $event->delete(); } // cleanup gradebook data_grade_item_delete($data); // Delete the instance itself // We must delete the module record after we delete the grade item. $result = $DB->delete_records('data', array('id'=>$id)); return $result; } /** * returns a summary of data activity of this user * * @global object * @param object $course * @param object $user * @param object $mod * @param object $data * @return object|null */ function data_user_outline($course, $user, $mod, $data) { global $DB, $CFG; require_once("$CFG->libdir/gradelib.php"); $grades = grade_get_grades($course->id, 'mod', 'data', $data->id, $user->id); if (empty($grades->items[0]->grades)) { $grade = false; } else { $grade = reset($grades->items[0]->grades); } if ($countrecords = $DB->count_records('data_records', array('dataid'=>$data->id, 'userid'=>$user->id))) { $result = new stdClass(); $result->info = get_string('numrecords', 'data', $countrecords); $lastrecord = $DB->get_record_sql('SELECT id,timemodified FROM {data_records} WHERE dataid = ? AND userid = ? ORDER BY timemodified DESC', array($data->id, $user->id), true); $result->time = $lastrecord->timemodified; if ($grade) { if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $result->info .= ', ' . get_string('gradenoun') . ': ' . $grade->str_long_grade; } else { $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); } } return $result; } else if ($grade) { $result = (object) [ 'time' => grade_get_date_for_user_grade($grade, $user), ]; if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { $result->info = get_string('gradenoun') . ': ' . $grade->str_long_grade; } else { $result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); } return $result; } return NULL; } /** * Prints all the records uploaded by this user * * @global object * @param object $course * @param object $user * @param object $mod * @param object $data */ function data_user_complete($course, $user, $mod, $data) { global $DB, $CFG, $OUTPUT; require_once("$CFG->libdir/gradelib.php"); $grades = grade_get_grades($course->id, 'mod', 'data', $data->id, $user->id); if (!empty($grades->items[0]->grades)) { $grade = reset($grades->items[0]->grades); if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { echo $OUTPUT->container(get_string('gradenoun') . ': ' . $grade->str_long_grade); if ($grade->str_feedback) { echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); } } else { echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades')); } } $records = $DB->get_records( 'data_records', ['dataid' => $data->id, 'userid' => $user->id], 'timemodified DESC' ); if ($records) { $manager = manager::create_from_instance($data); $parser = $manager->get_template('singletemplate'); echo $parser->parse_entries($records); } } /** * Return grade for given user or all users. * * @global object * @param object $data * @param int $userid optional user id, 0 means all users * @return array array of grades, false if none */ function data_get_user_grades($data, $userid=0) { global $CFG; require_once($CFG->dirroot.'/rating/lib.php'); $ratingoptions = new stdClass; $ratingoptions->component = 'mod_data'; $ratingoptions->ratingarea = 'entry'; $ratingoptions->modulename = 'data'; $ratingoptions->moduleid = $data->id; $ratingoptions->userid = $userid; $ratingoptions->aggregationmethod = $data->assessed; $ratingoptions->scaleid = $data->scale; $ratingoptions->itemtable = 'data_records'; $ratingoptions->itemtableusercolumn = 'userid'; $rm = new rating_manager(); return $rm->get_user_grades($ratingoptions); } /** * Update activity grades * * @category grade * @param object $data * @param int $userid specific user only, 0 means all * @param bool $nullifnone */ function data_update_grades($data, $userid=0, $nullifnone=true) { global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); if (!$data->assessed) { data_grade_item_update($data); } else if ($grades = data_get_user_grades($data, $userid)) { data_grade_item_update($data, $grades); } else if ($userid and $nullifnone) { $grade = new stdClass(); $grade->userid = $userid; $grade->rawgrade = NULL; data_grade_item_update($data, $grade); } else { data_grade_item_update($data); } } /** * Update/create grade item for given data * * @category grade * @param stdClass $data A database instance with extra cmidnumber property * @param mixed $grades Optional array/object of grade(s); 'reset' means reset grades in gradebook * @return object grade_item */ function data_grade_item_update($data, $grades=NULL) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); $params = array('itemname'=>$data->name, 'idnumber'=>$data->cmidnumber); if (!$data->assessed or $data->scale == 0) { $params['gradetype'] = GRADE_TYPE_NONE; } else if ($data->scale > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; $params['grademax'] = $data->scale; $params['grademin'] = 0; } else if ($data->scale < 0) { $params['gradetype'] = GRADE_TYPE_SCALE; $params['scaleid'] = -$data->scale; } if ($grades === 'reset') { $params['reset'] = true; $grades = NULL; } return grade_update('mod/data', $data->course, 'mod', 'data', $data->id, 0, $grades, $params); } /** * Delete grade item for given data * * @category grade * @param object $data object * @return object grade_item */ function data_grade_item_delete($data) { global $CFG; require_once($CFG->libdir.'/gradelib.php'); return grade_update('mod/data', $data->course, 'mod', 'data', $data->id, 0, NULL, array('deleted'=>1)); } // junk functions /** * Return rating related permissions * * @param string $contextid the context id * @param string $component the component to get rating permissions for * @param string $ratingarea the rating area to get permissions for * @return array an associative array of the user's rating permissions */ function data_rating_permissions($contextid, $component, $ratingarea) { $context = context::instance_by_id($contextid, MUST_EXIST); if ($component != 'mod_data' || $ratingarea != 'entry') { return null; } return array( 'view' => has_capability('mod/data:viewrating',$context), 'viewany' => has_capability('mod/data:viewanyrating',$context), 'viewall' => has_capability('mod/data:viewallratings',$context), 'rate' => has_capability('mod/data:rate',$context) ); } /** * Validates a submitted rating * @param array $params submitted data * context => object the context in which the rated items exists [required] * itemid => int the ID of the object being rated * scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required] * rating => int the submitted rating * rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required] * aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required] * @return boolean true if the rating is valid. Will throw rating_exception if not */ function data_rating_validate($params) { global $DB, $USER; // Check the component is mod_data if ($params['component'] != 'mod_data') { throw new rating_exception('invalidcomponent'); } // Check the ratingarea is entry (the only rating area in data module) if ($params['ratingarea'] != 'entry') { throw new rating_exception('invalidratingarea'); } // Check the rateduserid is not the current user .. you can't rate your own entries if ($params['rateduserid'] == $USER->id) { throw new rating_exception('nopermissiontorate'); } $datasql = "SELECT d.id as dataid, d.scale, d.course, r.userid as userid, d.approval, r.approved, r.timecreated, d.assesstimestart, d.assesstimefinish, r.groupid FROM {data_records} r JOIN {data} d ON r.dataid = d.id WHERE r.id = :itemid"; $dataparams = array('itemid'=>$params['itemid']); if (!$info = $DB->get_record_sql($datasql, $dataparams)) { //item doesn't exist throw new rating_exception('invaliditemid'); } if ($info->scale != $params['scaleid']) { //the scale being submitted doesnt match the one in the database throw new rating_exception('invalidscaleid'); } //check that the submitted rating is valid for the scale // lower limit if ($params['rating'] < 0 && $params['rating'] != RATING_UNSET_RATING) { throw new rating_exception('invalidnum'); } // upper limit if ($info->scale < 0) { //its a custom scale $scalerecord = $DB->get_record('scale', array('id' => -$info->scale)); if ($scalerecord) { $scalearray = explode(',', $scalerecord->scale); if ($params['rating'] > count($scalearray)) { throw new rating_exception('invalidnum'); } } else { throw new rating_exception('invalidscaleid'); } } else if ($params['rating'] > $info->scale) { //if its numeric and submitted rating is above maximum throw new rating_exception('invalidnum'); } if ($info->approval && !$info->approved) { //database requires approval but this item isnt approved throw new rating_exception('nopermissiontorate'); } // check the item we're rating was created in the assessable time window if (!empty($info->assesstimestart) && !empty($info->assesstimefinish)) { if ($info->timecreated < $info->assesstimestart || $info->timecreated > $info->assesstimefinish) { throw new rating_exception('notavailable'); } } $course = $DB->get_record('course', array('id'=>$info->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST); $context = context_module::instance($cm->id); // if the supplied context doesnt match the item's context if ($context->id != $params['context']->id) { throw new rating_exception('invalidcontext'); } // Make sure groups allow this user to see the item they're rating $groupid = $info->groupid; if ($groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) { // Groups are being used if (!groups_group_exists($groupid)) { // Can't find group throw new rating_exception('cannotfindgroup');//something is wrong } if (!groups_is_member($groupid) and !has_capability('moodle/site:accessallgroups', $context)) { // do not allow rating of posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS throw new rating_exception('notmemberofgroup'); } } return true; } /** * Can the current user see ratings for a given itemid? * * @param array $params submitted data * contextid => int contextid [required] * component => The component for this module - should always be mod_data [required] * ratingarea => object the context in which the rated items exists [required] * itemid => int the ID of the object being rated [required] * scaleid => int scale id [optional] * @return bool * @throws coding_exception * @throws rating_exception */ function mod_data_rating_can_see_item_ratings($params) { global $DB, $USER; // Check the component is mod_data. if (!isset($params['component']) || $params['component'] != 'mod_data') { throw new rating_exception('invalidcomponent'); } // Check the ratingarea is entry (the only rating area in data). if (!isset($params['ratingarea']) || $params['ratingarea'] != 'entry') { throw new rating_exception('invalidratingarea'); } if (!isset($params['itemid'])) { throw new rating_exception('invaliditemid'); } $datasql = "SELECT d.id as dataid, d.course, r.userid, r.groupid FROM {data_records} r JOIN {data} d ON r.dataid = d.id WHERE r.id = :itemid"; $dataparams = array('itemid' => $params['itemid']); if (!$info = $DB->get_record_sql($datasql, $dataparams)) { // Item doesn't exist. throw new rating_exception('invaliditemid'); } $course = $DB->get_record('course', array('id' => $info->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST); $context = context_module::instance($cm->id); if (!empty($info->userid)) { $ratingpermissions = data_rating_permissions($context->id, 'mod_data', 'entry'); $requiredpermission = ($info->userid != $USER->id) ? 'viewall' : 'view'; if (!$ratingpermissions[$requiredpermission]) { return false; } } // User can see ratings of all participants. if ($info->groupid == 0) { return true; } // Make sure groups allow this user to see the item they're rating. return groups_group_visible($info->groupid, $course, $cm); } /** * function that takes in the current data, number of items per page, * a search string and prints a preference box in view.php * * This preference box prints a searchable advanced search template if * a) A template is defined * b) The advanced search checkbox is checked. * * @global object * @global object * @param object $data * @param int $perpage * @param string $search * @param string $sort * @param string $order * @param array $search_array * @param int $advanced * @param string $mode * @return void */ function data_print_preference_form($data, $perpage, $search, $sort='', $order='ASC', $search_array = '', $advanced = 0, $mode= ''){ global $DB, $PAGE, $OUTPUT; $cm = get_coursemodule_from_instance('data', $data->id); $context = context_module::instance($cm->id); echo '
'; echo '
'; echo '
'; echo ''; if ($mode =='asearch') { $advanced = 1; echo ''; } echo ' '; $pagesizes = array(2=>2,3=>3,4=>4,5=>5,6=>6,7=>7,8=>8,9=>9,10=>10,15=>15, 20=>20,30=>30,40=>40,50=>50,100=>100,200=>200,300=>300,400=>400,500=>500,1000=>1000); echo html_writer::select($pagesizes, 'perpage', $perpage, false, array('id' => 'pref_perpage', 'class' => 'form-select me-1')); if ($advanced) { $regsearchclass = 'search_none'; $advancedsearchclass = 'search_inline'; } else { $regsearchclass = 'search_inline'; $advancedsearchclass = 'search_none'; } echo ''; echo ' '; // foreach field, print the option echo ''; echo ''; echo ''; if ($advanced) { $checked = ' checked="checked" '; } else { $checked = ''; } $PAGE->requires->js('/mod/data/data.js'); echo ''; echo ''; echo '' . ''; echo '
'; echo ''; echo '
'; echo '
'; echo '
'; echo '
'; echo ''; // print ASC or DESC echo ''; $i = 0; // Determine if we are printing all fields for advanced search, or the template for advanced search // If a template is not defined, use the deafault template and display all fields. $asearchtemplate = $data->asearchtemplate; if (empty($asearchtemplate)) { $asearchtemplate = data_generate_default_template($data, 'asearchtemplate', 0, false, false); } static $fields = array(); static $dataid = null; if (empty($dataid)) { $dataid = $data->id; } else if ($dataid != $data->id) { $fields = array(); } if (empty($fields)) { $fieldrecords = $DB->get_records('data_fields', array('dataid'=>$data->id)); foreach ($fieldrecords as $fieldrecord) { $fields[]= data_get_field($fieldrecord, $data); } } // Replacing tags $patterns = array(); $replacement = array(); // Then we generate strings to replace for normal tags $otherfields = []; foreach ($fields as $field) { $fieldname = $field->field->name; $fieldname = preg_quote($fieldname, '/'); $searchfield = data_get_field_from_id($field->field->id, $data); if ($searchfield->type === 'unknown') { continue; } if (!empty($search_array[$field->field->id]->data)) { $searchinput = $searchfield->display_search_field($search_array[$field->field->id]->data); } else { $searchinput = $searchfield->display_search_field(); } $patterns[] = "/\[\[$fieldname\]\]/i"; $replacement[] = $searchinput; // Extra field information. $patterns[] = "/\[\[$fieldname#name\]\]/i"; $replacement[] = $field->field->name; $patterns[] = "/\[\[$fieldname#description\]\]/i"; $replacement[] = $field->field->description; // Other fields. if (strpos($asearchtemplate, "[[" . $field->field->name . "]]") === false) { $otherfields[] = [ 'fieldname' => $searchfield->field->name, 'fieldcontent' => $searchinput, ]; } } $patterns[] = "/##otherfields##/"; if (!empty($otherfields)) { $replacement[] = $OUTPUT->render_from_template( 'mod_data/fields_otherfields', ['fields' => $otherfields] ); } else { $replacement[] = ''; } $fn = !empty($search_array[DATA_FIRSTNAME]->data) ? $search_array[DATA_FIRSTNAME]->data : ''; $ln = !empty($search_array[DATA_LASTNAME]->data) ? $search_array[DATA_LASTNAME]->data : ''; $patterns[] = '/##firstname##/'; $replacement[] = '' . ''; $patterns[] = '/##lastname##/'; $replacement[] = '' . ''; if (core_tag_tag::is_enabled('mod_data', 'data_records')) { $patterns[] = "/##tags##/"; $selectedtags = isset($search_array[DATA_TAGS]->rawtagnames) ? $search_array[DATA_TAGS]->rawtagnames : []; $replacement[] = data_generate_tag_form(false, $selectedtags); } // actual replacement of the tags $options = new stdClass(); $options->para=false; $options->noclean=true; echo ''; echo ''; echo '
 
'; echo preg_replace($patterns, $replacement, format_text($asearchtemplate, FORMAT_HTML, $options)); echo '

' . '' . '' . '
'; echo '
'; echo '
'; echo '
'; echo '
'; } /** * @global object * @global object * @param object $data * @param object $record * @param bool $print if the result must be printed or returner. * @return void Output echo'd */ function data_print_ratings($data, $record, bool $print = true) { global $OUTPUT; $result = ''; if (!empty($record->rating)){ $result = $OUTPUT->render($record->rating); } if (!$print) { return $result; } echo $result; } /** * List the actions that correspond to a view of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = 'r' and edulevel = LEVEL_PARTICIPATING will * be considered as view action. * * @return array */ function data_get_view_actions() { return array('view'); } /** * List the actions that correspond to a post of this module. * This is used by the participation report. * * Note: This is not used by new logging system. Event with * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING * will be considered as post action. * * @return array */ function data_get_post_actions() { return array('add','update','record delete'); } /** * @param string $name * @param int $dataid * @param int $fieldid * @return bool */ function data_fieldname_exists($name, $dataid, $fieldid = 0) { global $DB; if (!is_numeric($name)) { $like = $DB->sql_like('df.name', ':name', false); } else { $like = "df.name = :name"; } $params = array('name'=>$name); if ($fieldid) { $params['dataid'] = $dataid; $params['fieldid1'] = $fieldid; $params['fieldid2'] = $fieldid; return $DB->record_exists_sql("SELECT * FROM {data_fields} df WHERE $like AND df.dataid = :dataid AND ((df.id < :fieldid1) OR (df.id > :fieldid2))", $params); } else { $params['dataid'] = $dataid; return $DB->record_exists_sql("SELECT * FROM {data_fields} df WHERE $like AND df.dataid = :dataid", $params); } } /** * @param array $fieldinput */ function data_convert_arrays_to_strings(&$fieldinput) { foreach ($fieldinput as $key => $val) { if (is_array($val)) { $str = ''; foreach ($val as $inner) { $str .= $inner . ','; } $str = substr($str, 0, -1); $fieldinput->$key = $str; } } } /** * Converts a database (module instance) to use the Roles System * * @global object * @global object * @uses CONTEXT_MODULE * @uses CAP_PREVENT * @uses CAP_ALLOW * @param object $data a data object with the same attributes as a record * from the data database table * @param int $datamodid the id of the data module, from the modules table * @param array $teacherroles array of roles that have archetype teacher * @param array $studentroles array of roles that have archetype student * @param array $guestroles array of roles that have archetype guest * @param int $cmid the course_module id for this data instance * @return boolean data module was converted or not */ function data_convert_to_roles($data, $teacherroles=array(), $studentroles=array(), $cmid=NULL) { global $CFG, $DB, $OUTPUT; if (!isset($data->participants) && !isset($data->assesspublic) && !isset($data->groupmode)) { // We assume that this database has already been converted to use the // Roles System. above fields get dropped the data module has been // upgraded to use Roles. return false; } if (empty($cmid)) { // We were not given the course_module id. Try to find it. if (!$cm = get_coursemodule_from_instance('data', $data->id)) { echo $OUTPUT->notification('Could not get the course module for the data'); return false; } else { $cmid = $cm->id; } } $context = context_module::instance($cmid); // $data->participants: // 1 - Only teachers can add entries // 3 - Teachers and students can add entries switch ($data->participants) { case 1: foreach ($studentroles as $studentrole) { assign_capability('mod/data:writeentry', CAP_PREVENT, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:writeentry', CAP_ALLOW, $teacherrole->id, $context->id); } break; case 3: foreach ($studentroles as $studentrole) { assign_capability('mod/data:writeentry', CAP_ALLOW, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:writeentry', CAP_ALLOW, $teacherrole->id, $context->id); } break; } // $data->assessed: // 2 - Only teachers can rate posts // 1 - Everyone can rate posts // 0 - No one can rate posts switch ($data->assessed) { case 0: foreach ($studentroles as $studentrole) { assign_capability('mod/data:rate', CAP_PREVENT, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:rate', CAP_PREVENT, $teacherrole->id, $context->id); } break; case 1: foreach ($studentroles as $studentrole) { assign_capability('mod/data:rate', CAP_ALLOW, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:rate', CAP_ALLOW, $teacherrole->id, $context->id); } break; case 2: foreach ($studentroles as $studentrole) { assign_capability('mod/data:rate', CAP_PREVENT, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:rate', CAP_ALLOW, $teacherrole->id, $context->id); } break; } // $data->assesspublic: // 0 - Students can only see their own ratings // 1 - Students can see everyone's ratings switch ($data->assesspublic) { case 0: foreach ($studentroles as $studentrole) { assign_capability('mod/data:viewrating', CAP_PREVENT, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:viewrating', CAP_ALLOW, $teacherrole->id, $context->id); } break; case 1: foreach ($studentroles as $studentrole) { assign_capability('mod/data:viewrating', CAP_ALLOW, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('mod/data:viewrating', CAP_ALLOW, $teacherrole->id, $context->id); } break; } if (empty($cm)) { $cm = $DB->get_record('course_modules', array('id'=>$cmid)); } switch ($cm->groupmode) { case NOGROUPS: break; case SEPARATEGROUPS: foreach ($studentroles as $studentrole) { assign_capability('moodle/site:accessallgroups', CAP_PREVENT, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $teacherrole->id, $context->id); } break; case VISIBLEGROUPS: foreach ($studentroles as $studentrole) { assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $studentrole->id, $context->id); } foreach ($teacherroles as $teacherrole) { assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $teacherrole->id, $context->id); } break; } return true; } /** * Prints the heads for a page * * @param stdClass $course * @param stdClass $cm * @param stdClass $data * @param string $currenttab * @param string $actionbar */ function data_print_header($course, $cm, $data, $currenttab='', string $actionbar = '') { global $CFG, $displaynoticegood, $displaynoticebad, $OUTPUT, $PAGE, $USER; echo $OUTPUT->header(); echo $actionbar; // Print any notices if (!empty($displaynoticegood)) { echo $OUTPUT->notification($displaynoticegood, 'notifysuccess'); // good (usually green) } else if (!empty($displaynoticebad)) { echo $OUTPUT->notification($displaynoticebad); // bad (usuually red) } } /** * Can user add more entries? * * @param object $data * @param mixed $currentgroup * @param int $groupmode * @param stdClass $context * @return bool */ function data_user_can_add_entry($data, $currentgroup, $groupmode, $context = null) { global $DB; // Don't let add entry to a database that has no fields. if (!$DB->record_exists('data_fields', ['dataid' => $data->id])) { return false; } if (empty($context)) { $cm = get_coursemodule_from_instance('data', $data->id, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); } if (has_capability('mod/data:manageentries', $context)) { // no entry limits apply if user can manage } else if (!has_capability('mod/data:writeentry', $context)) { return false; } else if (data_atmaxentries($data)) { return false; } else if (data_in_readonly_period($data)) { // Check whether we're in a read-only period return false; } if (!$groupmode or has_capability('moodle/site:accessallgroups', $context)) { return true; } if ($currentgroup) { return groups_is_member($currentgroup); } else { //else it might be group 0 in visible mode if ($groupmode == VISIBLEGROUPS){ return true; } else { return false; } } } /** * Check whether the current user is allowed to manage the given record considering manageentries capability, * data_in_readonly_period() result, ownership (determined by data_isowner()) and manageapproved setting. * @param mixed $record record object or id * @param object $data data object * @param object $context context object * @return bool returns true if the user is allowd to edit the entry, false otherwise */ function data_user_can_manage_entry($record, $data, $context) { global $DB; if (has_capability('mod/data:manageentries', $context)) { return true; } // Check whether this activity is read-only at present. $readonly = data_in_readonly_period($data); if (!$readonly) { // Get record object from db if just id given like in data_isowner. // ...done before calling data_isowner() to avoid querying db twice. if (!is_object($record)) { if (!$record = $DB->get_record('data_records', array('id' => $record))) { return false; } } if (data_isowner($record)) { if ($data->approval && $record->approved) { return $data->manageapproved == 1; } else { return true; } } } return false; } /** * Check whether the specified database activity is currently in a read-only period * * @param object $data * @return bool returns true if the time fields in $data indicate a read-only period; false otherwise */ function data_in_readonly_period($data) { $now = time(); if (!$data->timeviewfrom && !$data->timeviewto) { return false; } else if (($data->timeviewfrom && $now < $data->timeviewfrom) || ($data->timeviewto && $now > $data->timeviewto)) { return false; } return true; } /** * @global object * @global object * @param object $course * @param int $userid * @param string $shortname * @return string */ function data_preset_path($course, $userid, $shortname) { global $USER, $CFG; $context = context_course::instance($course->id); $userid = (int)$userid; $path = null; if ($userid > 0 && ($userid == $USER->id || has_capability('mod/data:viewalluserpresets', $context))) { $path = $CFG->dataroot.'/data/preset/'.$userid.'/'.$shortname; } else if ($userid == 0) { $path = $CFG->dirroot.'/mod/data/preset/'.$shortname; } else if ($userid < 0) { $path = $CFG->tempdir.'/data/'.-$userid.'/'.$shortname; } return $path; } /** * Implementation of the function for printing the form elements that control * whether the course reset functionality affects the data. * * @param MoodleQuickForm $mform form passed by reference */ function data_reset_course_form_definition(&$mform) { $mform->addElement('header', 'dataheader', get_string('modulenameplural', 'data')); $mform->addElement('static', 'datadelete', get_string('delete')); $mform->addElement('checkbox', 'reset_data', get_string('deleteallentries','data')); $mform->addElement('checkbox', 'reset_data_notenrolled', get_string('deletenotenrolled', 'data')); $mform->disabledIf('reset_data_notenrolled', 'reset_data', 'checked'); $mform->addElement('checkbox', 'reset_data_ratings', get_string('deleteallratings')); $mform->disabledIf('reset_data_ratings', 'reset_data', 'checked'); $mform->addElement('checkbox', 'reset_data_comments', get_string('deleteallcomments')); $mform->disabledIf('reset_data_comments', 'reset_data', 'checked'); $mform->addElement('checkbox', 'reset_data_tags', get_string('removealldatatags', 'data')); $mform->disabledIf('reset_data_tags', 'reset_data', 'checked'); } /** * Course reset form defaults. * @return array */ function data_reset_course_form_defaults($course) { return array('reset_data'=>0, 'reset_data_ratings'=>1, 'reset_data_comments'=>1, 'reset_data_notenrolled'=>0); } /** * Removes all grades from gradebook * * @global object * @global object * @param int $courseid * @param string $type optional type */ function data_reset_gradebook($courseid, $type='') { global $CFG, $DB; $sql = "SELECT d.*, cm.idnumber as cmidnumber, d.course as courseid FROM {data} d, {course_modules} cm, {modules} m WHERE m.name='data' AND m.id=cm.module AND cm.instance=d.id AND d.course=?"; if ($datas = $DB->get_records_sql($sql, array($courseid))) { foreach ($datas as $data) { data_grade_item_update($data, 'reset'); } } } /** * Actual implementation of the reset course functionality, delete all the * data responses for course $data->courseid. * * @global object * @global object * @param object $data the data submitted from the reset course. * @return array status array */ function data_reset_userdata($data) { global $CFG, $DB; require_once($CFG->libdir.'/filelib.php'); require_once($CFG->dirroot.'/rating/lib.php'); $componentstr = get_string('modulenameplural', 'data'); $status = []; $allrecordssql = "SELECT r.id FROM {data_records} r INNER JOIN {data} d ON r.dataid = d.id WHERE d.course = ?"; $alldatassql = "SELECT d.id FROM {data} d WHERE d.course=?"; $rm = new rating_manager(); $ratingdeloptions = new stdClass; $ratingdeloptions->component = 'mod_data'; $ratingdeloptions->ratingarea = 'entry'; // Set the file storage - may need it to remove files later. $fs = get_file_storage(); // Delete entries if requested. if (!empty($data->reset_data)) { $DB->delete_records_select('comments', "itemid IN ($allrecordssql) AND commentarea='database_entry'", [$data->courseid]); $DB->delete_records_select('data_content', "recordid IN ($allrecordssql)", [$data->courseid]); $DB->delete_records_select('data_records', "dataid IN ($alldatassql)", [$data->courseid]); if ($datas = $DB->get_records_sql($alldatassql, [$data->courseid])) { foreach ($datas as $dataid=>$unused) { if (!$cm = get_coursemodule_from_instance('data', $dataid)) { continue; } $datacontext = context_module::instance($cm->id); // Delete any files that may exist. $fs->delete_area_files($datacontext->id, 'mod_data', 'content'); $ratingdeloptions->contextid = $datacontext->id; $rm->delete_ratings($ratingdeloptions); core_tag_tag::delete_instances('mod_data', null, $datacontext->id); } } if (empty($data->reset_gradebook_grades)) { // Remove all grades from gradebook. data_reset_gradebook($data->courseid); } $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallentries', 'data'), 'error' => false, ]; } // Remove entries by users not enrolled into course. if (!empty($data->reset_data_notenrolled)) { $recordssql = "SELECT r.id, r.userid, r.dataid, u.id AS userexists, u.deleted AS userdeleted FROM {data_records} r JOIN {data} d ON r.dataid = d.id LEFT JOIN {user} u ON r.userid = u.id WHERE d.course = ? AND r.userid > 0"; $course_context = context_course::instance($data->courseid); $notenrolled = []; $fields = []; $rs = $DB->get_recordset_sql($recordssql, [$data->courseid]); foreach ($rs as $record) { if (array_key_exists($record->userid, $notenrolled) or !$record->userexists or $record->userdeleted or !is_enrolled($course_context, $record->userid)) { // Delete ratings. if (!$cm = get_coursemodule_from_instance('data', $record->dataid)) { continue; } $datacontext = context_module::instance($cm->id); $ratingdeloptions->contextid = $datacontext->id; $ratingdeloptions->itemid = $record->id; $rm->delete_ratings($ratingdeloptions); // Delete any files that may exist. if ($contents = $DB->get_records('data_content', ['recordid' => $record->id], '', 'id')) { foreach ($contents as $content) { $fs->delete_area_files($datacontext->id, 'mod_data', 'content', $content->id); } } $notenrolled[$record->userid] = true; core_tag_tag::remove_all_item_tags('mod_data', 'data_records', $record->id); $DB->delete_records('comments', ['itemid' => $record->id, 'commentarea' => 'database_entry']); $DB->delete_records('data_content', ['recordid' => $record->id]); $DB->delete_records('data_records', ['id' => $record->id]); } } $rs->close(); $status[] = [ 'component' => $componentstr, 'item' => get_string('deletenotenrolled', 'data'), 'error' => false, ]; } // Remove all ratings. if (!empty($data->reset_data_ratings)) { if ($datas = $DB->get_records_sql($alldatassql, [$data->courseid])) { foreach ($datas as $dataid=>$unused) { if (!$cm = get_coursemodule_from_instance('data', $dataid)) { continue; } $datacontext = context_module::instance($cm->id); $ratingdeloptions->contextid = $datacontext->id; $rm->delete_ratings($ratingdeloptions); } } if (empty($data->reset_gradebook_grades)) { // Remove all grades from gradebook. data_reset_gradebook($data->courseid); } $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallratings'), 'error' => false, ]; } // Remove all comments. if (!empty($data->reset_data_comments)) { $DB->delete_records_select('comments', "itemid IN ($allrecordssql) AND commentarea='database_entry'", [$data->courseid]); $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallcomments'), 'error' => false, ]; } // Remove all the tags. if (!empty($data->reset_data_tags)) { if ($datas = $DB->get_records_sql($alldatassql, [$data->courseid])) { foreach ($datas as $dataid => $unused) { if (!$cm = get_coursemodule_from_instance('data', $dataid)) { continue; } $context = context_module::instance($cm->id); core_tag_tag::delete_instances('mod_data', null, $context->id); } } $status[] = [ 'component' => $componentstr, 'item' => get_string('removealldatatags', 'data'), 'error' => false, ]; } // Updating dates - shift may be negative too. if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. shift_course_mod_dates( 'data', [ 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto', 'assesstimestart', 'assesstimefinish', ], $data->timeshift, $data->courseid, ); $status[] = [ 'component' => $componentstr, 'item' => get_string('date'), 'error' => false, ]; } return $status; } /** * Returns all other caps used in module * * @return array */ function data_get_extra_capabilities() { return ['moodle/rating:view', 'moodle/rating:viewany', 'moodle/rating:viewall', 'moodle/rating:rate', 'moodle/comment:view', 'moodle/comment:post', 'moodle/comment:delete']; } /** * @param string $feature FEATURE_xx constant for requested feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function data_supports($feature) { switch($feature) { case FEATURE_GROUPS: return true; case FEATURE_GROUPINGS: return true; case FEATURE_MOD_INTRO: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_COMPLETION_HAS_RULES: return true; case FEATURE_GRADE_HAS_GRADE: return true; case FEATURE_GRADE_OUTCOMES: return true; case FEATURE_RATE: return true; case FEATURE_BACKUP_MOODLE2: return true; case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_COMMENT: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_COLLABORATION; default: return null; } } //////////////////////////////////////////////////////////////////////////////// // File API // //////////////////////////////////////////////////////////////////////////////// /** * Lists all browsable file areas * * @package mod_data * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @return array */ function data_get_file_areas($course, $cm, $context) { return array('content' => get_string('areacontent', 'mod_data')); } /** * File browsing support for data module. * * @param file_browser $browser * @param array $areas * @param stdClass $course * @param cm_info $cm * @param context $context * @param string $filearea * @param int $itemid * @param string $filepath * @param string $filename * @return file_info_stored file_info_stored instance or null if not found */ function data_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { global $CFG, $DB, $USER; if ($context->contextlevel != CONTEXT_MODULE) { return null; } if (!isset($areas[$filearea])) { return null; } if (is_null($itemid)) { require_once($CFG->dirroot.'/mod/data/locallib.php'); return new data_file_info_container($browser, $course, $cm, $context, $areas, $filearea); } if (!$content = $DB->get_record('data_content', array('id'=>$itemid))) { return null; } if (!$field = $DB->get_record('data_fields', array('id'=>$content->fieldid))) { return null; } if (!$record = $DB->get_record('data_records', array('id'=>$content->recordid))) { return null; } if (!$data = $DB->get_record('data', array('id'=>$field->dataid))) { return null; } //check if approved if ($data->approval and !$record->approved and !data_isowner($record) and !has_capability('mod/data:approve', $context)) { return null; } // group access if ($record->groupid) { $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { if (!groups_is_member($record->groupid)) { return null; } } } $fieldobj = data_get_field($field, $data, $cm); $filepath = is_null($filepath) ? '/' : $filepath; $filename = is_null($filename) ? '.' : $filename; if (!$fieldobj->file_ok($filepath.$filename)) { return null; } $fs = get_file_storage(); if (!($storedfile = $fs->get_file($context->id, 'mod_data', $filearea, $itemid, $filepath, $filename))) { return null; } // Checks to see if the user can manage files or is the owner. // TODO MDL-33805 - Do not use userid here and move the capability check above. if (!has_capability('moodle/course:managefiles', $context) && $storedfile->get_userid() != $USER->id) { return null; } $urlbase = $CFG->wwwroot.'/pluginfile.php'; return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemid, true, true, false, false); } /** * Serves the data attachments. Implements needed access control ;-) * * @package mod_data * @category files * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - justsend the file */ function data_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { global $CFG, $DB; if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_course_login($course, true, $cm); if ($filearea === 'content') { $contentid = (int)array_shift($args); if (!$content = $DB->get_record('data_content', array('id'=>$contentid))) { return false; } if (!$field = $DB->get_record('data_fields', array('id'=>$content->fieldid))) { return false; } if (!$record = $DB->get_record('data_records', array('id'=>$content->recordid))) { return false; } if (!$data = $DB->get_record('data', array('id'=>$field->dataid))) { return false; } if ($data->id != $cm->instance) { // hacker attempt - context does not match the contentid return false; } //check if approved if ($data->approval and !$record->approved and !data_isowner($record) and !has_capability('mod/data:approve', $context)) { return false; } // group access if ($record->groupid) { $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { if (!groups_is_member($record->groupid)) { return false; } } } $fieldobj = data_get_field($field, $data, $cm); $relativepath = implode('/', $args); $fullpath = "/$context->id/mod_data/content/$content->id/$relativepath"; if (!$fieldobj->file_ok($relativepath)) { return false; } $fs = get_file_storage(); if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { return false; } // finally send the file send_stored_file($file, 0, 0, true, $options); // download MUST be forced - security! } return false; } function data_extend_navigation($navigation, $course, $module, $cm) { global $CFG, $OUTPUT, $USER, $DB; require_once($CFG->dirroot . '/mod/data/locallib.php'); $rid = optional_param('rid', 0, PARAM_INT); $data = $DB->get_record('data', array('id'=>$cm->instance)); $currentgroup = groups_get_activity_group($cm); $groupmode = groups_get_activity_groupmode($cm); $numentries = data_numentries($data); $canmanageentries = has_capability('mod/data:manageentries', context_module::instance($cm->id)); if ($data->entriesleft = data_get_entries_left_to_add($data, $numentries, $canmanageentries)) { $entriesnode = $navigation->add(get_string('entrieslefttoadd', 'data', $data)); $entriesnode->add_class('note'); } $navigation->add(get_string('list', 'data'), new moodle_url('/mod/data/view.php', array('d'=>$cm->instance))); if (!empty($rid)) { $navigation->add(get_string('single', 'data'), new moodle_url('/mod/data/view.php', array('d'=>$cm->instance, 'rid'=>$rid))); } else { $navigation->add(get_string('single', 'data'), new moodle_url('/mod/data/view.php', array('d'=>$cm->instance, 'mode'=>'single'))); } $navigation->add(get_string('search', 'data'), new moodle_url('/mod/data/view.php', array('d'=>$cm->instance, 'mode'=>'asearch'))); } /** * Adds module specific settings to the settings block * * @param settings_navigation $settings The settings navigation object * @param navigation_node $datanode The node to add module settings to */ function data_extend_settings_navigation(settings_navigation $settings, navigation_node $datanode) { global $DB, $CFG, $USER; $data = $DB->get_record('data', array("id" => $settings->get_page()->cm->instance)); $currentgroup = groups_get_activity_group($settings->get_page()->cm); $groupmode = groups_get_activity_groupmode($settings->get_page()->cm); // Took out participation list here! if (data_user_can_add_entry($data, $currentgroup, $groupmode, $settings->get_page()->cm->context)) { if (empty($editentry)) { //TODO: undefined $addstring = get_string('add', 'data'); } else { $addstring = get_string('editentry', 'data'); } $addentrynode = $datanode->add($addstring, new moodle_url('/mod/data/edit.php', array('d' => $settings->get_page()->cm->instance))); $addentrynode->set_show_in_secondary_navigation(false); } if (has_capability(DATA_CAP_EXPORT, $settings->get_page()->cm->context)) { // The capability required to Export database records is centrally defined in 'lib.php' // and should be weaker than those required to edit Templates, Fields and Presets. $exportentriesnode = $datanode->add(get_string('exportentries', 'data'), new moodle_url('/mod/data/export.php', array('d' => $data->id))); $exportentriesnode->set_show_in_secondary_navigation(false); } if (has_capability('mod/data:manageentries', $settings->get_page()->cm->context)) { $importentriesnode = $datanode->add(get_string('importentries', 'data'), new moodle_url('/mod/data/import.php', array('d' => $data->id))); $importentriesnode->set_show_in_secondary_navigation(false); } if (has_capability('mod/data:managetemplates', $settings->get_page()->cm->context)) { $currenttab = ''; if ($currenttab == 'list') { $defaultemplate = 'listtemplate'; } else if ($currenttab == 'add') { $defaultemplate = 'addtemplate'; } else if ($currenttab == 'asearch') { $defaultemplate = 'asearchtemplate'; } else { $defaultemplate = 'singletemplate'; } $datanode->add(get_string('presets', 'data'), new moodle_url('/mod/data/preset.php', array('d' => $data->id))); $datanode->add(get_string('fields', 'data'), new moodle_url('/mod/data/field.php', array('d' => $data->id))); $datanode->add(get_string('templates', 'data'), new moodle_url('/mod/data/templates.php', array('d' => $data->id))); } if (!empty($CFG->enablerssfeeds) && !empty($CFG->data_enablerssfeeds) && $data->rssarticles > 0) { require_once("$CFG->libdir/rsslib.php"); $string = get_string('rsstype', 'data'); $url = new moodle_url(rss_get_url($settings->get_page()->cm->context->id, $USER->id, 'mod_data', $data->id)); $datanode->add($string, $url, settings_navigation::TYPE_SETTING, null, null, new pix_icon('i/rss', '')); } } /** * Running addtional permission check on plugin, for example, plugins * may have switch to turn on/off comments option, this callback will * affect UI display, not like pluginname_comment_validate only throw * exceptions. * Capability check has been done in comment->check_permissions(), we * don't need to do it again here. * * @package mod_data * @category comment * * @param stdClass $comment_param { * context => context the context object * courseid => int course id * cm => stdClass course module object * commentarea => string comment area * itemid => int itemid * } * @return array */ function data_comment_permissions($comment_param) { global $CFG, $DB; if (!$record = $DB->get_record('data_records', array('id'=>$comment_param->itemid))) { throw new comment_exception('invalidcommentitemid'); } if (!$data = $DB->get_record('data', array('id'=>$record->dataid))) { throw new comment_exception('invalidid', 'data'); } if ($data->comments) { return array('post'=>true, 'view'=>true); } else { return array('post'=>false, 'view'=>false); } } /** * Validate comment parameter before perform other comments actions * * @package mod_data * @category comment * * @param stdClass $comment_param { * context => context the context object * courseid => int course id * cm => stdClass course module object * commentarea => string comment area * itemid => int itemid * } * @return boolean */ function data_comment_validate($comment_param) { global $DB; // validate comment area if ($comment_param->commentarea != 'database_entry') { throw new comment_exception('invalidcommentarea'); } // validate itemid if (!$record = $DB->get_record('data_records', array('id'=>$comment_param->itemid))) { throw new comment_exception('invalidcommentitemid'); } if (!$data = $DB->get_record('data', array('id'=>$record->dataid))) { throw new comment_exception('invalidid', 'data'); } if (!$course = $DB->get_record('course', array('id'=>$data->course))) { throw new comment_exception('coursemisconf'); } if (!$cm = get_coursemodule_from_instance('data', $data->id, $course->id)) { throw new comment_exception('invalidcoursemodule'); } if (!$data->comments) { throw new comment_exception('commentsoff', 'data'); } $context = context_module::instance($cm->id); //check if approved if ($data->approval and !$record->approved and !data_isowner($record) and !has_capability('mod/data:approve', $context)) { throw new comment_exception('notapprovederror', 'data'); } // group access if ($record->groupid) { $groupmode = groups_get_activity_groupmode($cm, $course); if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { if (!groups_is_member($record->groupid)) { throw new comment_exception('notmemberofgroup'); } } } // validate context id if ($context->id != $comment_param->context->id) { throw new comment_exception('invalidcontext'); } // validation for comment deletion if (!empty($comment_param->commentid)) { if ($comment = $DB->get_record('comments', array('id'=>$comment_param->commentid))) { if ($comment->commentarea != 'database_entry') { throw new comment_exception('invalidcommentarea'); } if ($comment->contextid != $comment_param->context->id) { throw new comment_exception('invalidcontext'); } if ($comment->itemid != $comment_param->itemid) { throw new comment_exception('invalidcommentitemid'); } } else { throw new comment_exception('invalidcommentid'); } } return true; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function data_page_type_list($pagetype, $parentcontext, $currentcontext) { $module_pagetype = array('mod-data-*'=>get_string('page-mod-data-x', 'data')); return $module_pagetype; } /** * Get all of the record ids from a database activity. * * @param int $dataid The dataid of the database module. * @param object $selectdata Contains an additional sql statement for the * where clause for group and approval fields. * @param array $params Parameters that coincide with the sql statement. * @return array $idarray An array of record ids */ function data_get_all_recordids($dataid, $selectdata = '', $params = null) { global $DB; $initsql = 'SELECT r.id FROM {data_records} r WHERE r.dataid = :dataid'; if ($selectdata != '') { $initsql .= $selectdata; $params = array_merge(array('dataid' => $dataid), $params); } else { $params = array('dataid' => $dataid); } $initsql .= ' GROUP BY r.id'; $initrecord = $DB->get_recordset_sql($initsql, $params); $idarray = array(); foreach ($initrecord as $data) { $idarray[] = $data->id; } // Close the record set and free up resources. $initrecord->close(); return $idarray; } /** * Get the ids of all the records that match that advanced search criteria * This goes and loops through each criterion one at a time until it either * runs out of records or returns a subset of records. * * @param array $recordids An array of record ids. * @param array $searcharray Contains information for the advanced search criteria * @param int $dataid The data id of the database. * @return array $recordids An array of record ids. */ function data_get_advance_search_ids($recordids, $searcharray, $dataid) { // Check to see if we have any record IDs. if (empty($recordids)) { // Send back an empty search. return array(); } $searchcriteria = array_keys($searcharray); // Loop through and reduce the IDs one search criteria at a time. foreach ($searchcriteria as $key) { $recordids = data_get_recordids($key, $searcharray, $dataid, $recordids); // If we don't have anymore IDs then stop. if (!$recordids) { break; } } return $recordids; } /** * Gets the record IDs given the search criteria * * @param string $alias Record alias. * @param array $searcharray Criteria for the search. * @param int $dataid Data ID for the database * @param array $recordids An array of record IDs. * @return array $nestarray An arry of record IDs */ function data_get_recordids($alias, $searcharray, $dataid, $recordids) { global $DB; $searchcriteria = $alias; // Keep the criteria. $nestsearch = $searcharray[$alias]; // searching for content outside of mdl_data_content if ($alias < 0) { $alias = ''; } list($insql, $params) = $DB->get_in_or_equal($recordids, SQL_PARAMS_NAMED); $nestselect = 'SELECT c' . $alias . '.recordid FROM {data_content} c' . $alias . ' INNER JOIN {data_fields} f ON f.id = c' . $alias . '.fieldid INNER JOIN {data_records} r ON r.id = c' . $alias . '.recordid INNER JOIN {user} u ON u.id = r.userid '; $nestwhere = 'WHERE r.dataid = :dataid AND c' . $alias .'.recordid ' . $insql . ' AND '; $params['dataid'] = $dataid; if (count($nestsearch->params) != 0) { $params = array_merge($params, $nestsearch->params); $nestsql = $nestselect . $nestwhere . $nestsearch->sql; } else if ($searchcriteria == DATA_TIMEMODIFIED) { $nestsql = $nestselect . $nestwhere . $nestsearch->field . ' >= :timemodified GROUP BY c' . $alias . '.recordid'; $params['timemodified'] = $nestsearch->data; } else if ($searchcriteria == DATA_TAGS) { if (empty($nestsearch->rawtagnames)) { return []; } $i = 0; $tagwhere = []; $tagselect = ''; foreach ($nestsearch->rawtagnames as $tagrawname) { $tagselect .= " INNER JOIN {tag_instance} ti_$i ON ti_$i.component = 'mod_data' AND ti_$i.itemtype = 'data_records' AND ti_$i.itemid = r.id INNER JOIN {tag} t_$i ON ti_$i.tagid = t_$i.id "; $tagwhere[] = " t_$i.rawname = :trawname_$i "; $params["trawname_$i"] = $tagrawname; $i++; } $nestsql = $nestselect . $tagselect . $nestwhere . implode(' AND ', $tagwhere); } else { // First name or last name. $thing = $DB->sql_like($nestsearch->field, ':search1', false); $nestsql = $nestselect . $nestwhere . $thing . ' GROUP BY c' . $alias . '.recordid'; $params['search1'] = "%$nestsearch->data%"; } $nestrecords = $DB->get_recordset_sql($nestsql, $params); $nestarray = array(); foreach ($nestrecords as $data) { $nestarray[] = $data->recordid; } // Close the record set and free up resources. $nestrecords->close(); return $nestarray; } /** * Returns an array with an sql string for advanced searches and the parameters that go with them. * * @param int $sort DATA_* * @param stdClass $data Data module object * @param array $recordids An array of record IDs. * @param string $selectdata Information for the where and select part of the sql statement. * @param string $sortorder Additional sort parameters * @return array sqlselect sqlselect['sql'] has the sql string, sqlselect['params'] contains an array of parameters. */ function data_get_advanced_search_sql($sort, $data, $recordids, $selectdata, $sortorder) { global $DB; $userfieldsapi = \core_user\fields::for_userpic()->excluding('id'); $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; if ($sort == 0) { $nestselectsql = 'SELECT r.id, r.approved, r.timecreated, r.timemodified, r.userid, ' . $namefields . ' FROM {data_content} c, {data_records} r, {user} u '; $groupsql = ' GROUP BY r.id, r.approved, r.timecreated, r.timemodified, r.userid, u.firstname, u.lastname, ' . $namefields; } else { // Sorting through 'Other' criteria if ($sort <= 0) { switch ($sort) { case DATA_LASTNAME: $sortcontentfull = "u.lastname"; break; case DATA_FIRSTNAME: $sortcontentfull = "u.firstname"; break; case DATA_APPROVED: $sortcontentfull = "r.approved"; break; case DATA_TIMEMODIFIED: $sortcontentfull = "r.timemodified"; break; case DATA_TIMEADDED: default: $sortcontentfull = "r.timecreated"; } } else { $sortfield = data_get_field_from_id($sort, $data); $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field()); $sortcontentfull = $sortfield->get_sort_sql($sortcontent); } $nestselectsql = 'SELECT r.id, r.approved, r.timecreated, r.timemodified, r.userid, ' . $namefields . ', ' . $sortcontentfull . ' AS sortorder FROM {data_content} c, {data_records} r, {user} u '; $groupsql = ' GROUP BY r.id, r.approved, r.timecreated, r.timemodified, r.userid, ' . $namefields . ', ' .$sortcontentfull; } // Default to a standard Where statement if $selectdata is empty. if ($selectdata == '') { $selectdata = 'WHERE c.recordid = r.id AND r.dataid = :dataid AND r.userid = u.id '; } // Find the field we are sorting on if ($sort > 0 or data_get_field_from_id($sort, $data)) { $selectdata .= ' AND c.fieldid = :sort AND s.recordid = r.id'; $nestselectsql .= ',{data_content} s '; } // If there are no record IDs then return an sql statment that will return no rows. if (count($recordids) != 0) { list($insql, $inparam) = $DB->get_in_or_equal($recordids, SQL_PARAMS_NAMED); } else { list($insql, $inparam) = $DB->get_in_or_equal(array('-1'), SQL_PARAMS_NAMED); } $nestfromsql = $selectdata . ' AND c.recordid ' . $insql . $groupsql; $sqlselect['sql'] = "$nestselectsql $nestfromsql $sortorder"; $sqlselect['params'] = $inparam; return $sqlselect; } /** * Delete a record entry. * * @param int $recordid The ID for the record to be deleted. * @param object $data The data object for this activity. * @param int $courseid ID for the current course (for logging). * @param int $cmid The course module ID. * @return bool True if the record deleted, false if not. */ function data_delete_record($recordid, $data, $courseid, $cmid) { global $DB, $CFG; if ($deleterecord = $DB->get_record('data_records', array('id' => $recordid))) { if ($deleterecord->dataid == $data->id) { if ($contents = $DB->get_records('data_content', array('recordid' => $deleterecord->id))) { foreach ($contents as $content) { if ($field = data_get_field_from_id($content->fieldid, $data)) { $field->delete_content($content->recordid); } } $DB->delete_records('data_content', array('recordid'=>$deleterecord->id)); $DB->delete_records('data_records', array('id'=>$deleterecord->id)); // Delete cached RSS feeds. if (!empty($CFG->enablerssfeeds)) { require_once($CFG->dirroot.'/mod/data/rsslib.php'); 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, 'context' => context_module::instance($cmid), 'courseid' => $courseid, 'other' => array( 'dataid' => $deleterecord->dataid ) )); $event->add_record_snapshot('data_records', $deleterecord); $event->trigger(); $course = get_course($courseid); $cm = get_coursemodule_from_instance('data', $data->id, 0, false, MUST_EXIST); data_update_completion_state($data, $course, $cm); return true; } } } return false; } /** * Check for required fields, and build a list of fields to be updated in a * submission. * * @param $mod stdClass The current recordid - provided as an optimisation. * @param $fields array The field data * @param $datarecord stdClass The submitted data. * @return stdClass containing: * * string[] generalnotifications Notifications for the form as a whole. * * string[] fieldnotifications Notifications for a specific field. * * bool validated Whether the field was validated successfully. * * data_field_base[] fields The field objects to be update. */ function data_process_submission(stdClass $mod, $fields, stdClass $datarecord) { $result = new stdClass(); // Empty form checking - you can't submit an empty form. $emptyform = true; $requiredfieldsfilled = true; $fieldsvalidated = true; // Store the notifications. $result->generalnotifications = array(); $result->fieldnotifications = array(); // Store the instantiated classes as an optimisation when processing the result. // This prevents the fields being re-initialised when updating. $result->fields = array(); $submitteddata = array(); foreach ($datarecord as $fieldname => $fieldvalue) { if (strpos($fieldname, '_')) { $namearray = explode('_', $fieldname, 3); $fieldid = $namearray[1]; if (!isset($submitteddata[$fieldid])) { $submitteddata[$fieldid] = array(); } if (count($namearray) === 2) { $subfieldid = 0; } else { $subfieldid = $namearray[2]; } $fielddata = new stdClass(); $fielddata->fieldname = $fieldname; $fielddata->value = $fieldvalue; $submitteddata[$fieldid][$subfieldid] = $fielddata; } } // Check all form fields which have the required are filled. foreach ($fields as $fieldrecord) { // Check whether the field has any data. $fieldhascontent = false; $field = data_get_field($fieldrecord, $mod); if (isset($submitteddata[$fieldrecord->id])) { // Field validation check. if (method_exists($field, 'field_validation')) { $errormessage = $field->field_validation($submitteddata[$fieldrecord->id]); if ($errormessage) { $result->fieldnotifications[$field->field->name][] = $errormessage; $fieldsvalidated = false; } } foreach ($submitteddata[$fieldrecord->id] as $fieldname => $value) { if ($field->notemptyfield($value->value, $value->fieldname)) { // The field has content and the form is not empty. $fieldhascontent = true; $emptyform = false; } } } // If the field is required, add a notification to that effect. if ($field->field->required && !$fieldhascontent) { if (!isset($result->fieldnotifications[$field->field->name])) { $result->fieldnotifications[$field->field->name] = array(); } $result->fieldnotifications[$field->field->name][] = get_string('errormustsupplyvalue', 'data'); $requiredfieldsfilled = false; } // Update the field. if (isset($submitteddata[$fieldrecord->id])) { foreach ($submitteddata[$fieldrecord->id] as $value) { $result->fields[$value->fieldname] = $field; } } } if ($emptyform) { // The form is empty. $result->generalnotifications[] = get_string('emptyaddform', 'data'); } $result->validated = $requiredfieldsfilled && !$emptyform && $fieldsvalidated; return $result; } /** * This standard function will check all instances of this module * and make sure there are up-to-date events created for each of them. * If courseid = 0, then every data event in the site is checked, else * only data events belonging to the course specified are checked. * This function is used, in its new format, by restore_refresh_events() * * @param int $courseid * @param int|stdClass $instance Data module instance or ID. * @param int|stdClass $cm Course module object or ID (not used in this module). * @return bool */ function data_refresh_events($courseid = 0, $instance = null, $cm = null) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/data/locallib.php'); // If we have instance information then we can just update the one event instead of updating all events. if (isset($instance)) { if (!is_object($instance)) { $instance = $DB->get_record('data', array('id' => $instance), '*', MUST_EXIST); } data_set_events($instance); return true; } if ($courseid) { if (! $data = $DB->get_records("data", array("course" => $courseid))) { return true; } } else { if (! $data = $DB->get_records("data")) { return true; } } foreach ($data as $datum) { data_set_events($datum); } return true; } /** * Fetch the configuration for this database activity. * * @param stdClass $database The object returned from the database for this instance * @param string $key The name of the key to retrieve. If none is supplied, then all configuration is returned * @param mixed $default The default value to use if no value was found for the specified key * @return mixed The returned value */ function data_get_config($database, $key = null, $default = null) { if (!empty($database->config)) { $config = json_decode($database->config); } else { $config = new stdClass(); } if ($key === null) { return $config; } if (property_exists($config, $key)) { return $config->$key; } return $default; } /** * Update the configuration for this database activity. * * @param stdClass $database The object returned from the database for this instance * @param string $key The name of the key to set * @param mixed $value The value to set for the key */ function data_set_config(&$database, $key, $value) { // Note: We must pass $database by reference because there may be subsequent calls to update_record and these should // not overwrite the configuration just set. global $DB; $config = data_get_config($database); if (!isset($config->$key) || $config->$key !== $value) { $config->$key = $value; $database->config = json_encode($config); $DB->set_field('data', 'config', $database->config, ['id' => $database->id]); } } /** * Sets the automatic completion state for this database item based on the * count of on its entries. * @since Moodle 3.3 * @param object $data The data object for this activity * @param object $course Course * @param object $cm course-module */ function data_update_completion_state($data, $course, $cm) { // If completion option is enabled, evaluate it and return true/false. $completion = new completion_info($course); if ($data->completionentries && $completion->is_enabled($cm)) { $numentries = data_numentries($data); // Check the number of entries required against the number of entries already made. if ($numentries >= $data->completionentries) { $completion->update_state($cm, COMPLETION_COMPLETE); } else { $completion->update_state($cm, COMPLETION_INCOMPLETE); } } } /** * Get the list of deprecated icons. * * @return array with the deprecated key icons. * @todo Final deprecation on Moodle 6.0 MDL-83465. */ function mod_data_get_deprecated_icons() { return [ // Deprecated since Moodle 4.6. // See MDL-82313 for refactoring. 'mod_data:field/checkbox' => 'fa-regular fa-square-check', 'mod_data:field/date' => 'fa-regular fa-calendar', 'mod_data:field/file' => 'fa-regular fa-file', 'mod_data:field/latlong' => 'fa-earth-americas', 'mod_data:field/menu' => 'fa-bars', 'mod_data:field/multimenu' => 'fa-bars', 'mod_data:field/number' => 'fa-hashtag', 'mod_data:field/picture' => 'fa-regular fa-image', 'mod_data:field/radiobutton' => 'fa-regular fa-circle-dot', 'mod_data:field/text' => 'fa-i-cursor', 'mod_data:field/textarea' => 'fa-font', 'mod_data:field/url' => 'fa-link', ]; } /* * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not * @since Moodle 3.2 */ function data_check_updates_since(cm_info $cm, $from, $filter = array()) { global $DB, $CFG; require_once($CFG->dirroot . '/mod/data/locallib.php'); $updates = course_check_module_updates_since($cm, $from, array(), $filter); // Check for new entries. $updates->entries = (object) array('updated' => false); $data = $DB->get_record('data', array('id' => $cm->instance), '*', MUST_EXIST); $searcharray = []; $searcharray[DATA_TIMEMODIFIED] = new stdClass(); $searcharray[DATA_TIMEMODIFIED]->sql = ''; $searcharray[DATA_TIMEMODIFIED]->params = array(); $searcharray[DATA_TIMEMODIFIED]->field = 'r.timemodified'; $searcharray[DATA_TIMEMODIFIED]->data = $from; $currentgroup = groups_get_activity_group($cm); // Teachers should retrieve all entries when not in separate groups. if (has_capability('mod/data:manageentries', $cm->context) && groups_get_activity_groupmode($cm) != SEPARATEGROUPS) { $currentgroup = 0; } list($entries, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($data, $cm, $cm->context, 'list', $currentgroup, '', null, null, 0, 0, true, $searcharray); if (!empty($entries)) { $updates->entries->updated = true; $updates->entries->itemids = array_keys($entries); } return $updates; } /** * This function receives a calendar event and returns the action associated with it, or null if there is none. * * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event * is not displayed on the block. * * @param calendar_event $event * @param \core_calendar\action_factory $factory * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). * @return \core_calendar\local\event\entities\action_interface|null */ function mod_data_core_calendar_provide_event_action(calendar_event $event, \core_calendar\action_factory $factory, int $userid = 0) { global $USER; if (!$userid) { $userid = $USER->id; } $cm = get_fast_modinfo($event->courseid, $userid)->instances['data'][$event->instance]; if (!$cm->uservisible) { // The module is not visible to the user for any reason. return null; } $now = time(); if (!empty($cm->customdata['timeavailableto']) && $cm->customdata['timeavailableto'] < $now) { // The module has closed so the user can no longer submit anything. return null; } // The module is actionable if we don't have a start time or the start time is // in the past. $actionable = (empty($cm->customdata['timeavailablefrom']) || $cm->customdata['timeavailablefrom'] <= $now); return $factory->create_instance( get_string('add', 'data'), new \moodle_url('/mod/data/view.php', array('id' => $cm->id)), 1, $actionable ); } /** * Add a get_coursemodule_info function in case any database type wants to add 'extra' information * for the course (see resource). * * Given a course_module object, this function returns any "extra" information that may be needed * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. * * @param stdClass $coursemodule The coursemodule object (record). * @return cached_cm_info An object on information that the courses * will know about (most noticeably, an icon). */ function data_get_coursemodule_info($coursemodule) { global $DB; $dbparams = ['id' => $coursemodule->instance]; $fields = 'id, name, intro, introformat, completionentries, timeavailablefrom, timeavailableto'; if (!$data = $DB->get_record('data', $dbparams, $fields)) { return false; } $result = new cached_cm_info(); $result->name = $data->name; if ($coursemodule->showdescription) { // Convert intro to html. Do not filter cached version, filters run at display time. $result->content = format_module_intro('data', $data, $coursemodule->id, false); } // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { $result->customdata['customcompletionrules']['completionentries'] = $data->completionentries; } // Other properties that may be used in calendar or on dashboard. if ($data->timeavailablefrom) { $result->customdata['timeavailablefrom'] = $data->timeavailablefrom; } if ($data->timeavailableto) { $result->customdata['timeavailableto'] = $data->timeavailableto; } return $result; } /** * Callback which returns human-readable strings describing the active completion custom rules for the module instance. * * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] * @return array $descriptions the array of descriptions for the custom rules. */ function mod_data_get_completion_active_rule_descriptions($cm) { // Values will be present in cm_info, and we assume these are up to date. if (empty($cm->customdata['customcompletionrules']) || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { return []; } $descriptions = []; foreach ($cm->customdata['customcompletionrules'] as $key => $val) { switch ($key) { case 'completionentries': if (!empty($val)) { $descriptions[] = get_string('completionentriesdesc', 'data', $val); } break; default: break; } } return $descriptions; } /** * This function calculates the minimum and maximum cutoff values for the timestart of * the given event. * * It will return an array with two values, the first being the minimum cutoff value and * the second being the maximum cutoff value. Either or both values can be null, which * indicates there is no minimum or maximum, respectively. * * If a cutoff is required then the function must return an array containing the cutoff * timestamp and error string to display to the user if the cutoff value is violated. * * A minimum and maximum cutoff return value will look like: * [ * [1505704373, 'The due date must be after the sbumission start date'], * [1506741172, 'The due date must be before the cutoff date'] * ] * * @param calendar_event $event The calendar event to get the time range for * @param stdClass $instance The module instance to get the range from * @return array */ function mod_data_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { $mindate = null; $maxdate = null; if ($event->eventtype == DATA_EVENT_TYPE_OPEN) { // The start time of the open event can't be equal to or after the // close time of the database activity. if (!empty($instance->timeavailableto)) { $maxdate = [ $instance->timeavailableto, get_string('openafterclose', 'data') ]; } } else if ($event->eventtype == DATA_EVENT_TYPE_CLOSE) { // The start time of the close event can't be equal to or earlier than the // open time of the database activity. if (!empty($instance->timeavailablefrom)) { $mindate = [ $instance->timeavailablefrom, get_string('closebeforeopen', 'data') ]; } } return [$mindate, $maxdate]; } /** * This function will update the data module according to the * event that has been modified. * * It will set the timeopen or timeclose value of the data instance * according to the type of event provided. * * @throws \moodle_exception * @param \calendar_event $event * @param stdClass $data The module instance to get the range from */ function mod_data_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $data) { global $DB; if (empty($event->instance) || $event->modulename != 'data') { return; } if ($event->instance != $data->id) { return; } if (!in_array($event->eventtype, [DATA_EVENT_TYPE_OPEN, DATA_EVENT_TYPE_CLOSE])) { return; } $courseid = $event->courseid; $modulename = $event->modulename; $instanceid = $event->instance; $modified = false; $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; $context = context_module::instance($coursemodule->id); // The user does not have the capability to modify this activity. if (!has_capability('moodle/course:manageactivities', $context)) { return; } if ($event->eventtype == DATA_EVENT_TYPE_OPEN) { // If the event is for the data activity opening then we should // set the start time of the data activity to be the new start // time of the event. if ($data->timeavailablefrom != $event->timestart) { $data->timeavailablefrom = $event->timestart; $data->timemodified = time(); $modified = true; } } else if ($event->eventtype == DATA_EVENT_TYPE_CLOSE) { // If the event is for the data activity closing then we should // set the end time of the data activity to be the new start // time of the event. if ($data->timeavailableto != $event->timestart) { $data->timeavailableto = $event->timestart; $modified = true; } } if ($modified) { $data->timemodified = time(); $DB->update_record('data', $data); $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); $event->trigger(); } } /** * Callback to fetch the activity event type lang string. * * @param string $eventtype The event type. * @return lang_string The event type lang string. */ function mod_data_core_calendar_get_event_action_string(string $eventtype): string { $modulename = get_string('modulename', 'data'); switch ($eventtype) { case DATA_EVENT_TYPE_OPEN: $identifier = 'calendarstart'; break; case DATA_EVENT_TYPE_CLOSE: $identifier = 'calendarend'; break; default: return get_string('requiresaction', 'calendar', $modulename); } return get_string($identifier, 'data', $modulename); } version.php000064400000002213152265517740006756 0ustar00. /** * Data activity filter version information * * @package filter * @subpackage data * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component= 'filter_data'; $plugin->dependencies = ['mod_data' => 2025040800]; export.php000064400000012256152265517740006622 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ require_once('../../config.php'); require_once('lib.php'); require_once('export_form.php'); // database ID $d = required_param('d', PARAM_INT); $exportuser = optional_param('exportuser', false, PARAM_BOOL); // Flag for exporting user details $exporttime = optional_param('exporttime', false, PARAM_BOOL); // Flag for exporting date/time information $exportapproval = optional_param('exportapproval', false, PARAM_BOOL); // Flag for exporting user details $tags = optional_param('exporttags', false, PARAM_BOOL); // Flag for exporting user details. $redirectbackto = optional_param('backto', '', PARAM_LOCALURL); // The location to redirect back to. $url = new moodle_url('/mod/data/export.php', array('d' => $d)); $PAGE->set_url($url); if (! $data = $DB->get_record('data', array('id'=>$d))) { throw new \moodle_exception('wrongdataid', 'data'); } if (! $cm = get_coursemodule_from_instance('data', $data->id, $data->course)) { throw new \moodle_exception('invalidcoursemodule'); } if(! $course = $DB->get_record('course', array('id'=>$cm->course))) { throw new \moodle_exception('invalidcourseid'); } // fill in missing properties needed for updating of instance $data->course = $cm->course; $data->cmidnumber = $cm->idnumber; $data->instance = $cm->instance; $context = context_module::instance($cm->id); require_login($course, false, $cm); require_capability(DATA_CAP_EXPORT, $context); // get fields for this database $fieldrecords = $DB->get_records('data_fields', array('dataid'=>$data->id), 'id'); if(empty($fieldrecords)) { if (has_capability('mod/data:managetemplates', $context)) { redirect($CFG->wwwroot.'/mod/data/field.php?d='.$data->id); } else { throw new \moodle_exception('nofieldindatabase', 'data'); } } // populate objets for this databases fields $fields = array(); foreach ($fieldrecords as $fieldrecord) { $fields[]= data_get_field($fieldrecord, $data); } $mform = new mod_data_export_form(new moodle_url('/mod/data/export.php', ['d' => $data->id, 'backto' => $redirectbackto]), $fields, $cm, $data); if ($mform->is_cancelled()) { $redirectbackto = !empty($redirectbackto) ? $redirectbackto : new \moodle_url('/mod/data/view.php', ['d' => $data->id]); redirect($redirectbackto); } else if ($formdata = (array) $mform->get_data()) { $selectedfields = array(); foreach ($formdata as $key => $value) { //field form elements are field_1 field_2 etc. 0 if not selected. 1 if selected. if (strpos($key, 'field_')===0 && !empty($value)) { $selectedfields[] = substr($key, 6); } } $currentgroup = groups_get_activity_group($cm); $exporter = null; switch ($formdata['exporttype']) { case 'csv': $exporter = new \mod_data\local\exporter\csv_entries_exporter(); $exporter->set_delimiter_name($formdata['delimiter_name']); break; case 'ods': $exporter = new \mod_data\local\exporter\ods_entries_exporter(); break; default: throw new coding_exception('Invalid export format has been specified. ' . 'Only "csv" and "ods" are currently supported.'); } $includefiles = !empty($formdata['includefiles']); \mod_data\local\exporter\utils::data_exportdata($data->id, $fields, $selectedfields, $exporter, $currentgroup, $context, $exportuser, $exporttime, $exportapproval, $tags, $includefiles); $count = $exporter->get_records_count(); $filename = clean_filename("{$data->name}-{$count}_record"); if ($count > 1) { $filename .= 's'; } $filename .= clean_filename('-' . gmdate("Ymd_Hi")); $exporter->set_export_file_name($filename); $exporter->send_file(); } // Build header to match the rest of the UI. $PAGE->add_body_class('limitedwidth'); $pagename = get_string('exportentries', 'data'); $titleparts = [ $pagename, format_string($data->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->set_heading($course->fullname); $PAGE->force_settings_menu(true); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); echo $OUTPUT->header(); echo $OUTPUT->heading($pagename); groups_print_activity_menu($cm, $url); $mform->display(); echo $OUTPUT->footer(); die(); preset_form.php000064400000006161152265517740007624 0ustar00. /** * Database module preset forms. * * @package mod_data * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden!'); } require_once($CFG->libdir . '/formslib.php'); class data_existing_preset_form extends moodleform { public function definition() { $this->_form->addElement('header', 'presets', get_string('usestandard', 'data')); $this->_form->addHelpButton('presets', 'usestandard', 'data'); $this->_form->addElement('hidden', 'd'); $this->_form->setType('d', PARAM_INT); $this->_form->addElement('hidden', 'action', 'confirmdelete'); $this->_form->setType('action', PARAM_ALPHANUM); $delete = get_string('delete'); foreach ($this->_customdata['presets'] as $preset) { $userid = $preset instanceof \mod_data\preset ? $preset->get_userid() : $preset->userid; $this->_form->addElement('radio', 'fullname', null, ' '.$preset->description, $userid.'/'.$preset->shortname); } $this->_form->addElement('submit', 'importexisting', get_string('choose')); } } class data_export_form extends moodleform { public function definition() { $this->_form->addElement('header', 'exportheading', get_string('exportaszip', 'data')); $this->_form->addElement('hidden', 'd'); $this->_form->setType('d', PARAM_INT); $this->_form->addElement('hidden', 'action', 'export'); $this->_form->setType('action', PARAM_ALPHANUM); $this->_form->addElement('submit', 'export', get_string('export', 'data')); } } class data_save_preset_form extends moodleform { public function definition() { $this->_form->addElement('header', 'exportheading', get_string('saveaspreset', 'data')); $this->_form->addElement('hidden', 'd'); $this->_form->setType('d', PARAM_INT); $this->_form->addElement('hidden', 'action', 'save2'); $this->_form->setType('action', PARAM_ALPHANUM); $this->_form->addElement('text', 'name', get_string('name')); $this->_form->setType('name', PARAM_FILE); $this->_form->addRule('name', null, 'required'); $this->_form->addElement('checkbox', 'overwrite', get_string('overwrite', 'data'), get_string('overrwritedesc', 'data')); $this->_form->addElement('submit', 'saveaspreset', get_string('continue')); } } tabs.php000064400000012405152265517740006226 0ustar00id); $row = array(); $row[] = new tabobject('list', new moodle_url('/mod/data/view.php', array('d' => $data->id)), get_string('list','data')); if (isset($record)) { $row[] = new tabobject('single', new moodle_url('/mod/data/view.php', array('d' => $data->id, 'rid' => $record->id)), get_string('single','data')); } else { $row[] = new tabobject('single', new moodle_url('/mod/data/view.php', array('d' => $data->id, 'mode' => 'single')), get_string('single','data')); } // Add an advanced search tab. $row[] = new tabobject('asearch', new moodle_url('/mod/data/view.php', array('d' => $data->id, 'mode' => 'asearch')), get_string('search', 'data')); if (isloggedin()) { // just a perf shortcut if (data_user_can_add_entry($data, $currentgroup, $groupmode, $context)) { // took out participation list here! $addstring = empty($editentry) ? get_string('add', 'data') : get_string('editentry', 'data'); $row[] = new tabobject('add', new moodle_url('/mod/data/edit.php', array('d' => $data->id)), $addstring); } if (has_capability(DATA_CAP_EXPORT, $context)) { // The capability required to Export database records is centrally defined in 'lib.php' // and should be weaker than those required to edit Templates, Fields and Presets. $row[] = new tabobject('export', new moodle_url('/mod/data/export.php', array('d' => $data->id)), get_string('export', 'data')); } if (has_capability('mod/data:managetemplates', $context)) { if ($currenttab == 'list') { $defaultemplate = 'listtemplate'; } else if ($currenttab == 'add') { $defaultemplate = 'addtemplate'; } else if ($currenttab == 'asearch') { $defaultemplate = 'asearchtemplate'; } else { $defaultemplate = 'singletemplate'; } $templatestab = new tabobject('templates', new moodle_url('/mod/data/templates.php', array('d' => $data->id, 'mode' => $defaultemplate)), get_string('templates','data')); $row[] = $templatestab; $row[] = new tabobject('fields', new moodle_url('/mod/data/field.php', array('d' => $data->id)), get_string('fields','data')); $row[] = new tabobject('presets', new moodle_url('/mod/data/preset.php', array('d' => $data->id)), get_string('presets', 'data')); } } if ($currenttab == 'templates' and isset($mode) && isset($templatestab)) { $templatestab->inactive = true; $templatelist = array ('listtemplate', 'singletemplate', 'asearchtemplate', 'addtemplate', 'rsstemplate', 'csstemplate', 'jstemplate'); $currenttab =''; foreach ($templatelist as $template) { $templatestab->subtree[] = new tabobject($template, new moodle_url('/mod/data/templates.php', array('d' => $data->id, 'mode' => $template)), get_string($template, 'data')); if ($template == $mode) { $currenttab = $template; } } if ($currenttab == '') { $currenttab = $mode = 'singletemplate'; } } // Print out the tabs and continue! echo $OUTPUT->tabtree($row, $currenttab); data.js000064400000004135152265517740006034 0ustar00/** * Javascript to insert the field tags into the textarea. * Used when editing a data template */ function insert_field_tags(selectlist) { var value = selectlist.options[selectlist.selectedIndex].value; var editorname = 'template'; if (typeof tinyMCE == 'undefined') { if (document.execCommand('insertText')) { document.execCommand('insertText', false, value); } else { var element = document.getElementsByName(editorname)[0]; // For inserting when in normal textareas insertAtCursor(element, value); } } else { tinyMCE.execInstanceCommand(editorname, 'mceInsertContent', false, value); } } /** * javascript for hiding/displaying advanced search form when viewing */ function showHideAdvSearch(checked) { var divs = document.getElementsByTagName('div'); for (var i = 0; i < divs.length; i++) { if(divs[i].id.match('data_adv_form')) { if(checked) { divs[i].style.display = 'inline'; } else { divs[i].style.display = 'none'; } } else if (divs[i].id.match('reg_search')) { if (!checked) { divs[i].style.display = 'inline'; } else { divs[i].style.display = 'none'; } } else if (divs[i].id.match('advsearch-save-sec')) { if (!checked) { divs[i].style.display = 'inline'; } else { divs[i].style.display = 'none'; } } } } M.data_urlpicker = {}; M.data_urlpicker.init = function(Y, options) { options.formcallback = M.data_urlpicker.callback; if (!M.core_filepicker.instances[options.client_id]) { M.core_filepicker.init(Y, options); } Y.on('click', function(e, client_id) { e.preventDefault(); M.core_filepicker.instances[client_id].show(); }, '#filepicker-button-'+options.client_id, null, options.client_id); }; M.data_urlpicker.callback = function (params) { document.getElementById('field_url_'+params.client_id).value = params.url; }; classes/external.php000064400000140015152265517740010553 0ustar00. /** * Database module external API * * @package mod_data * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . "/mod/data/locallib.php"); use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use core_external\util; use mod_data\external\database_summary_exporter; use mod_data\external\record_exporter; use mod_data\external\field_exporter; use mod_data\manager; /** * Database module external functions * * @package mod_data * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ class mod_data_external extends external_api { /** * Describes the parameters for get_databases_by_courses. * * @return external_function_parameters * @since Moodle 2.9 */ public static function get_databases_by_courses_parameters() { return new external_function_parameters ( array( 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'course id', VALUE_REQUIRED), 'Array of course ids', VALUE_DEFAULT, array() ), ) ); } /** * Returns a list of databases in a provided list of courses, * if no list is provided all databases that the user can view will be returned. * * @param array $courseids the course ids * @return array the database details * @since Moodle 2.9 */ public static function get_databases_by_courses($courseids = array()) { global $PAGE; $params = self::validate_parameters(self::get_databases_by_courses_parameters(), array('courseids' => $courseids)); $warnings = array(); $mycourses = array(); if (empty($params['courseids'])) { $mycourses = enrol_get_my_courses(); $params['courseids'] = array_keys($mycourses); } // Array to store the databases to return. $arrdatabases = array(); // Ensure there are courseids to loop through. if (!empty($params['courseids'])) { list($dbcourses, $warnings) = util::validate_courses($params['courseids'], $mycourses); // Get the databases in this course, this function checks users visibility permissions. // We can avoid then additional validate_context calls. $databases = get_all_instances_in_courses("data", $dbcourses); foreach ($databases as $database) { $context = context_module::instance($database->coursemodule); // Remove fields added by get_all_instances_in_courses. unset($database->coursemodule, $database->section, $database->visible, $database->groupmode, $database->groupingid); // This information should be only available if the user can see the database entries. if (!has_capability('mod/data:viewentry', $context)) { $fields = array('comments', 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto', 'requiredentries', 'requiredentriestoview', 'maxentries', 'rssarticles', 'singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'manageapproved', 'defaultsort', 'defaultsortdir'); foreach ($fields as $field) { unset($database->{$field}); } } // Check additional permissions for returning optional private settings. // I avoid intentionally to use can_[add|update]_moduleinfo. if (!has_capability('moodle/course:manageactivities', $context)) { $fields = array('scale', 'assessed', 'assesstimestart', 'assesstimefinish', 'editany', 'notification', 'timemodified'); foreach ($fields as $field) { unset($database->{$field}); } } $exporter = new database_summary_exporter($database, array('context' => $context)); $data = $exporter->export($PAGE->get_renderer('core')); $data->name = \core_external\util::format_string($data->name, $context); $arrdatabases[] = $data; } } $result = array(); $result['databases'] = $arrdatabases; $result['warnings'] = $warnings; return $result; } /** * Describes the get_databases_by_courses return value. * * @return external_single_structure * @since Moodle 2.9 */ public static function get_databases_by_courses_returns() { return new external_single_structure( array( 'databases' => new external_multiple_structure( database_summary_exporter::get_read_structure() ), 'warnings' => new external_warnings(), ) ); } /** * Utility function for validating a database. * * @param int $databaseid database instance id * @return array array containing the database object, course, context and course module objects * @since Moodle 3.3 */ protected static function validate_database($databaseid) { global $DB; // Request and permission validation. $database = $DB->get_record('data', array('id' => $databaseid), '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($database, 'data'); $context = context_module::instance($cm->id); self::validate_context($context); require_capability('mod/data:viewentry', $context); return array($database, $course, $cm, $context); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function view_database_parameters() { return new external_function_parameters( array( 'databaseid' => new external_value(PARAM_INT, 'data instance id') ) ); } /** * Simulate the data/view.php web interface page: trigger events, completion, etc... * * @param int $databaseid the data instance id * @return array of warnings and status result * @since Moodle 3.3 * @throws moodle_exception */ public static function view_database($databaseid) { $params = self::validate_parameters(self::view_database_parameters(), array('databaseid' => $databaseid)); $warnings = array(); list($database, $course, $cm, $context) = self::validate_database($params['databaseid']); // Call the data/lib API. $manager = manager::create_from_coursemodule($cm); $manager->set_module_viewed($course); $result = [ 'status' => true, 'warnings' => $warnings, ]; return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function view_database_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters. * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_data_access_information_parameters() { return new external_function_parameters( array( 'databaseid' => new external_value(PARAM_INT, 'Database instance id.'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group.', VALUE_DEFAULT, 0), ) ); } /** * Return access information for a given database. * * @param int $databaseid the database instance id * @param int $groupid (optional) group id, 0 means that the function will determine the user group * @return array of warnings and access information * @since Moodle 3.3 * @throws moodle_exception */ public static function get_data_access_information($databaseid, $groupid = 0) { $params = array('databaseid' => $databaseid, 'groupid' => $groupid); $params = self::validate_parameters(self::get_data_access_information_parameters(), $params); $warnings = array(); list($database, $course, $cm, $context) = self::validate_database($params['databaseid']); $result = array( 'warnings' => $warnings ); $groupmode = groups_get_activity_groupmode($cm); if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // Check to see if groups are being used here. if ($groupmode) { $groupid = groups_get_activity_group($cm); } else { $groupid = 0; } } // Group related information. $result['groupid'] = $groupid; $result['canaddentry'] = data_user_can_add_entry($database, $groupid, $groupmode, $context); // Now capabilities. $result['canmanageentries'] = has_capability('mod/data:manageentries', $context); $result['canapprove'] = has_capability('mod/data:approve', $context); // Now time access restrictions. list($result['timeavailable'], $warnings) = data_get_time_availability_status($database, $result['canmanageentries']); // Other information. $result['numentries'] = data_numentries($database); $result['entrieslefttoadd'] = data_get_entries_left_to_add($database, $result['numentries'], $result['canmanageentries']); $result['entrieslefttoview'] = data_get_entries_left_to_view($database, $result['numentries'], $result['canmanageentries']); $result['inreadonlyperiod'] = data_in_readonly_period($database); return $result; } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.3 */ public static function get_data_access_information_returns() { return new external_single_structure( array( 'groupid' => new external_value(PARAM_INT, 'User current group id (calculated)'), 'canaddentry' => new external_value(PARAM_BOOL, 'Whether the user can add entries or not.'), 'canmanageentries' => new external_value(PARAM_BOOL, 'Whether the user can manage entries or not.'), 'canapprove' => new external_value(PARAM_BOOL, 'Whether the user can approve entries or not.'), 'timeavailable' => new external_value(PARAM_BOOL, 'Whether the database is available or not by time restrictions.'), 'inreadonlyperiod' => new external_value(PARAM_BOOL, 'Whether the database is in read mode only.'), 'numentries' => new external_value(PARAM_INT, 'The number of entries the current user added.'), 'entrieslefttoadd' => new external_value(PARAM_INT, 'The number of entries left to complete the activity.'), 'entrieslefttoview' => new external_value(PARAM_INT, 'The number of entries left to view other users entries.'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_entries_parameters() { return new external_function_parameters( array( 'databaseid' => new external_value(PARAM_INT, 'data instance id'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group', VALUE_DEFAULT, 0), 'returncontents' => new external_value(PARAM_BOOL, 'Whether to return contents or not. This will return each entry raw contents and the complete list view (using the template).', VALUE_DEFAULT, false), 'sort' => new external_value(PARAM_INT, 'Sort the records by this field id, reserved ids are: 0: timeadded -1: firstname -2: lastname -3: approved -4: timemodified. Empty for using the default database setting.', VALUE_DEFAULT, null), 'order' => new external_value(PARAM_ALPHA, 'The direction of the sorting: \'ASC\' or \'DESC\'. Empty for using the default database setting.', VALUE_DEFAULT, null), 'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0), 'perpage' => new external_value(PARAM_INT, 'The number of records to return per page', VALUE_DEFAULT, 0), ) ); } /** * Return access information for a given feedback * * @param int $databaseid the data instance id * @param int $groupid (optional) group id, 0 means that the function will determine the user group * @param bool $returncontents Whether to return the entries contents or not * @param str $sort sort by this field * @param int $order the direction of the sorting * @param int $page page of records to return * @param int $perpage number of records to return per page * @return array of warnings and the entries * @since Moodle 3.3 * @throws moodle_exception */ public static function get_entries($databaseid, $groupid = 0, $returncontents = false, $sort = null, $order = null, $page = 0, $perpage = 0) { global $PAGE, $DB; $params = array('databaseid' => $databaseid, 'groupid' => $groupid, 'returncontents' => $returncontents , 'sort' => $sort, 'order' => $order, 'page' => $page, 'perpage' => $perpage); $params = self::validate_parameters(self::get_entries_parameters(), $params); $warnings = array(); if (!empty($params['order'])) { $params['order'] = strtoupper($params['order']); if ($params['order'] != 'ASC' && $params['order'] != 'DESC') { throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $params['order'] . ')'); } } list($database, $course, $cm, $context) = self::validate_database($params['databaseid']); // Check database is open in time. data_require_time_available($database, null, $context); if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // Check to see if groups are being used here. if ($groupmode = groups_get_activity_groupmode($cm)) { // We don't need to validate a possible groupid = 0 since it would be handled by data_search_entries. $groupid = groups_get_activity_group($cm); } else { $groupid = 0; } } $manager = manager::create_from_instance($database); list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($database, $cm, $context, 'list', $groupid, '', $params['sort'], $params['order'], $params['page'], $params['perpage']); $entries = []; $contentsids = []; // Store here the content ids of the records returned. foreach ($records as $record) { $user = user_picture::unalias($record, null, 'userid'); $related = array('context' => $context, 'database' => $database, 'user' => $user); $contents = $DB->get_records('data_content', array('recordid' => $record->id)); $contentsids = array_merge($contentsids, array_keys($contents)); if ($params['returncontents']) { $related['contents'] = $contents; } else { $related['contents'] = null; } $exporter = new record_exporter($record, $related); $entries[] = $exporter->export($PAGE->get_renderer('core')); } // Retrieve total files size for the records retrieved. $totalfilesize = 0; $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_data', 'content'); foreach ($files as $file) { if ($file->is_directory() || !in_array($file->get_itemid(), $contentsids)) { continue; } $totalfilesize += $file->get_filesize(); } $result = array( 'entries' => $entries, 'totalcount' => $totalcount, 'totalfilesize' => $totalfilesize, 'warnings' => $warnings ); // Check if we should return the list rendered. if ($params['returncontents']) { $parser = $manager->get_template('listtemplate', ['page' => $page]); $result['listviewcontents'] = $parser->parse_entries($records); } return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function get_entries_returns() { return new external_single_structure( array( 'entries' => new external_multiple_structure( record_exporter::get_read_structure() ), 'totalcount' => new external_value(PARAM_INT, 'Total count of records.'), 'totalfilesize' => new external_value(PARAM_INT, 'Total size (bytes) of the files included in the records.'), 'listviewcontents' => new external_value(PARAM_RAW, 'The list view contents as is rendered in the site.', VALUE_OPTIONAL), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_entry_parameters() { return new external_function_parameters( array( 'entryid' => new external_value(PARAM_INT, 'record entry id'), 'returncontents' => new external_value(PARAM_BOOL, 'Whether to return contents or not.', VALUE_DEFAULT, false), ) ); } /** * Return one entry record from the database, including contents optionally. * * @param int $entryid the record entry id id * @param bool $returncontents whether to return the entries contents or not * @return array of warnings and the entries * @since Moodle 3.3 * @throws moodle_exception */ public static function get_entry($entryid, $returncontents = false) { global $PAGE, $DB; $params = array('entryid' => $entryid, 'returncontents' => $returncontents); $params = self::validate_parameters(self::get_entry_parameters(), $params); $warnings = array(); $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST); list($database, $course, $cm, $context) = self::validate_database($record->dataid); // Check database is open in time. $canmanageentries = has_capability('mod/data:manageentries', $context); data_require_time_available($database, $canmanageentries); $manager = manager::create_from_instance($database); if ($record->groupid != 0) { if (!groups_group_visible($record->groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } // Check correct record entry. Group check was done before. if (!data_can_view_record($database, $record, $record->groupid, $canmanageentries)) { throw new moodle_exception('notapprovederror', 'data'); } $related = array('context' => $context, 'database' => $database, 'user' => null); if ($params['returncontents']) { $related['contents'] = $DB->get_records('data_content', array('recordid' => $record->id)); } else { $related['contents'] = null; } $exporter = new record_exporter($record, $related); $entry = $exporter->export($PAGE->get_renderer('core')); $result = array( 'entry' => $entry, 'ratinginfo' => \core_rating\external\util::get_rating_info($database, $context, 'mod_data', 'entry', array($record)), 'warnings' => $warnings ); // Check if we should return the entry rendered. if ($params['returncontents']) { $records = [$record]; $parser = $manager->get_template('singletemplate'); $result['entryviewcontents'] = $parser->parse_entries($records); } return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function get_entry_returns() { return new external_single_structure( array( 'entry' => record_exporter::get_read_structure(), 'entryviewcontents' => new external_value(PARAM_RAW, 'The entry as is rendered in the site.', VALUE_OPTIONAL), 'ratinginfo' => \core_rating\external\util::external_ratings_structure(), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function get_fields_parameters() { return new external_function_parameters( array( 'databaseid' => new external_value(PARAM_INT, 'Database instance id.'), ) ); } /** * Return the list of configured fields for the given database. * * @param int $databaseid the database id * @return array of warnings and the fields * @since Moodle 3.3 * @throws moodle_exception */ public static function get_fields($databaseid) { global $PAGE; $params = array('databaseid' => $databaseid); $params = self::validate_parameters(self::get_fields_parameters(), $params); $fields = $warnings = array(); list($database, $course, $cm, $context) = self::validate_database($params['databaseid']); // Check database is open in time. $canmanageentries = has_capability('mod/data:manageentries', $context); data_require_time_available($database, $canmanageentries); $fieldinstances = data_get_field_instances($database); foreach ($fieldinstances as $fieldinstance) { $record = $fieldinstance->field; // Now get the configs the user can see with his current permissions. $configs = $fieldinstance->get_config_for_external(); foreach ($configs as $name => $value) { // Overwrite. $record->{$name} = $value; } $exporter = new field_exporter($record, array('context' => $context)); $fields[] = $exporter->export($PAGE->get_renderer('core')); } $result = array( 'fields' => $fields, 'warnings' => $warnings ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function get_fields_returns() { return new external_single_structure( array( 'fields' => new external_multiple_structure( field_exporter::get_read_structure() ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function search_entries_parameters() { return new external_function_parameters( array( 'databaseid' => new external_value(PARAM_INT, 'data instance id'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group', VALUE_DEFAULT, 0), 'returncontents' => new external_value(PARAM_BOOL, 'Whether to return contents or not.', VALUE_DEFAULT, false), 'search' => new external_value(PARAM_NOTAGS, 'search string (empty when using advanced)', VALUE_DEFAULT, ''), 'advsearch' => new external_multiple_structure( new external_single_structure( array( 'name' => new external_value(PARAM_ALPHANUMEXT, 'Field key for search. Use fn or ln for first or last name'), 'value' => new external_value(PARAM_RAW, 'JSON encoded value for search'), ) ), 'Advanced search', VALUE_DEFAULT, array() ), 'sort' => new external_value(PARAM_INT, 'Sort the records by this field id, reserved ids are: 0: timeadded -1: firstname -2: lastname -3: approved -4: timemodified. Empty for using the default database setting.', VALUE_DEFAULT, null), 'order' => new external_value(PARAM_ALPHA, 'The direction of the sorting: \'ASC\' or \'DESC\'. Empty for using the default database setting.', VALUE_DEFAULT, null), 'page' => new external_value(PARAM_INT, 'The page of records to return.', VALUE_DEFAULT, 0), 'perpage' => new external_value(PARAM_INT, 'The number of records to return per page', VALUE_DEFAULT, 0), ) ); } /** * Return access information for a given feedback * * @param int $databaseid the data instance id * @param int $groupid (optional) group id, 0 means that the function will determine the user group * @param bool $returncontents whether to return contents or not * @param str $search search text * @param array $advsearch advanced search data * @param str $sort sort by this field * @param int $order the direction of the sorting * @param int $page page of records to return * @param int $perpage number of records to return per page * @return array of warnings and the entries * @since Moodle 3.3 * @throws moodle_exception */ public static function search_entries($databaseid, $groupid = 0, $returncontents = false, $search = '', $advsearch = [], $sort = null, $order = null, $page = 0, $perpage = 0) { global $PAGE, $DB; $params = array('databaseid' => $databaseid, 'groupid' => $groupid, 'returncontents' => $returncontents, 'search' => $search, 'advsearch' => $advsearch, 'sort' => $sort, 'order' => $order, 'page' => $page, 'perpage' => $perpage); $params = self::validate_parameters(self::search_entries_parameters(), $params); $warnings = array(); if (!empty($params['order'])) { $params['order'] = strtoupper($params['order']); if ($params['order'] != 'ASC' && $params['order'] != 'DESC') { throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $params['order'] . ')'); } } list($database, $course, $cm, $context) = self::validate_database($params['databaseid']); // Check database is open in time. data_require_time_available($database, null, $context); $manager = manager::create_from_instance($database); if (!empty($params['groupid'])) { $groupid = $params['groupid']; // Determine is the group is visible to user. if (!groups_group_visible($groupid, $course, $cm)) { throw new moodle_exception('notingroup'); } } else { // Check to see if groups are being used here. if ($groupmode = groups_get_activity_groupmode($cm)) { // We don't need to validate a possible groupid = 0 since it would be handled by data_search_entries. $groupid = groups_get_activity_group($cm); } else { $groupid = 0; } } if (!empty($params['advsearch'])) { $advanced = true; $defaults = []; $fn = $ln = ''; // Defaults for first and last name. // Force defaults for advanced search. foreach ($params['advsearch'] as $adv) { if ($adv['name'] == 'fn') { $fn = json_decode($adv['value']); continue; } if ($adv['name'] == 'ln') { $ln = json_decode($adv['value']); continue; } $defaults[$adv['name']] = json_decode($adv['value']); } list($searcharray, $params['search']) = data_build_search_array($database, false, [], $defaults, $fn, $ln); } else { $advanced = null; $searcharray = null; } list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($database, $cm, $context, 'list', $groupid, $params['search'], $params['sort'], $params['order'], $params['page'], $params['perpage'], $advanced, $searcharray); $entries = []; foreach ($records as $record) { $user = user_picture::unalias($record, null, 'userid'); $related = array('context' => $context, 'database' => $database, 'user' => $user); if ($params['returncontents']) { $related['contents'] = $DB->get_records('data_content', array('recordid' => $record->id)); } else { $related['contents'] = null; } $exporter = new record_exporter($record, $related); $entries[] = $exporter->export($PAGE->get_renderer('core')); } $result = array( 'entries' => $entries, 'totalcount' => $totalcount, 'maxcount' => $maxcount, 'warnings' => $warnings ); // Check if we should return the list rendered. if ($params['returncontents']) { $parser = $manager->get_template('listtemplate', ['page' => $page]); $result['listviewcontents'] = $parser->parse_entries($records); } return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function search_entries_returns() { return new external_single_structure( array( 'entries' => new external_multiple_structure( record_exporter::get_read_structure() ), 'totalcount' => new external_value(PARAM_INT, 'Total count of records returned by the search.'), 'maxcount' => new external_value(PARAM_INT, 'Total count of records that the user could see in the database (if all the search criterias were removed).', VALUE_OPTIONAL), 'listviewcontents' => new external_value(PARAM_RAW, 'The list view contents as is rendered in the site.', VALUE_OPTIONAL), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function approve_entry_parameters() { return new external_function_parameters( array( 'entryid' => new external_value(PARAM_INT, 'Record entry id.'), 'approve' => new external_value(PARAM_BOOL, 'Whether to approve (true) or unapprove the entry.', VALUE_DEFAULT, true), ) ); } /** * Approves or unapproves an entry. * * @param int $entryid the record entry id id * @param bool $approve whether to approve (true) or unapprove the entry * @return array of warnings and the entries * @since Moodle 3.3 * @throws moodle_exception */ public static function approve_entry($entryid, $approve = true) { global $PAGE, $DB; $params = array('entryid' => $entryid, 'approve' => $approve); $params = self::validate_parameters(self::approve_entry_parameters(), $params); $warnings = array(); $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST); list($database, $course, $cm, $context) = self::validate_database($record->dataid); // Check database is open in time. data_require_time_available($database, null, $context); // Check specific capabilities. require_capability('mod/data:approve', $context); data_approve_entry($record->id, $params['approve']); $result = array( 'status' => true, 'warnings' => $warnings ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function approve_entry_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function delete_entry_parameters() { return new external_function_parameters( array( 'entryid' => new external_value(PARAM_INT, 'Record entry id.'), ) ); } /** * Deletes an entry. * * @param int $entryid the record entry id * @return array of warnings success status * @since Moodle 3.3 * @throws moodle_exception */ public static function delete_entry($entryid) { global $PAGE, $DB; $params = array('entryid' => $entryid); $params = self::validate_parameters(self::delete_entry_parameters(), $params); $warnings = array(); $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST); list($database, $course, $cm, $context) = self::validate_database($record->dataid); if (data_user_can_manage_entry($record, $database, $context)) { data_delete_record($record->id, $database, $course->id, $cm->id); } else { throw new moodle_exception('noaccess', 'data'); } $result = array( 'status' => true, 'warnings' => $warnings ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function delete_entry_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'Always true. If we see this field it means that the entry was deleted.'), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function add_entry_parameters() { return new external_function_parameters( array( 'databaseid' => new external_value(PARAM_INT, 'data instance id'), 'groupid' => new external_value(PARAM_INT, 'Group id, 0 means that the function will determine the user group', VALUE_DEFAULT, 0), 'data' => new external_multiple_structure( new external_single_structure( array( 'fieldid' => new external_value(PARAM_INT, 'The field id.'), 'subfield' => new external_value(PARAM_NOTAGS, 'The subfield name (if required).', VALUE_DEFAULT, ''), 'value' => new external_value(PARAM_RAW, 'The contents for the field always JSON encoded.'), ) ), 'The fields data to be created' ), ) ); } /** * Adds a new entry to a database * * @param int $databaseid the data instance id * @param int $groupid (optional) group id, 0 means that the function will determine the user group * @param array $data the fields data to be created * @return array of warnings and status result * @since Moodle 3.3 * @throws moodle_exception */ public static function add_entry($databaseid, $groupid, $data) { global $DB; $params = array('databaseid' => $databaseid, 'groupid' => $groupid, 'data' => $data); $params = self::validate_parameters(self::add_entry_parameters(), $params); $warnings = array(); $fieldnotifications = array(); list($database, $course, $cm, $context) = self::validate_database($params['databaseid']); $fields = $DB->get_records('data_fields', ['dataid' => $database->id]); if (empty($fields)) { throw new moodle_exception('nofieldindatabase', 'data'); } // Check database is open in time. data_require_time_available($database, null, $context); $groupmode = groups_get_activity_groupmode($cm); // Determine default group. if (empty($params['groupid'])) { // Check to see if groups are being used here. if ($groupmode) { $groupid = groups_get_activity_group($cm); } else { $groupid = 0; } } // Group is validated inside the function. if (!data_user_can_add_entry($database, $groupid, $groupmode, $context)) { throw new moodle_exception('noaccess', 'data'); } // Prepare the data as is expected by the API. $datarecord = new stdClass; foreach ($params['data'] as $data) { $subfield = ($data['subfield'] !== '') ? '_' . $data['subfield'] : ''; // We ask for JSON encoded values because of multiple choice forms or checkboxes that use array parameters. $datarecord->{'field_' . $data['fieldid'] . $subfield} = json_decode($data['value']); } // Validate to ensure that enough data was submitted. $processeddata = data_process_submission($database, $fields, $datarecord); // Format notifications. if (!empty($processeddata->fieldnotifications)) { foreach ($processeddata->fieldnotifications as $field => $notififications) { foreach ($notififications as $notif) { $fieldnotifications[] = [ 'fieldname' => $field, 'notification' => $notif, ]; } } } // Create a new (empty) record. $newentryid = 0; if ($processeddata->validated && $recordid = data_add_record($database, $groupid)) { $newentryid = $recordid; // Now populate the fields contents of the new record. data_add_fields_contents_to_new_record($database, $context, $recordid, $fields, $datarecord, $processeddata); } $result = array( 'newentryid' => $newentryid, 'generalnotifications' => $processeddata->generalnotifications, 'fieldnotifications' => $fieldnotifications, ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function add_entry_returns() { return new external_single_structure( array( 'newentryid' => new external_value(PARAM_INT, 'True new created entry id. 0 if the entry was not created.'), 'generalnotifications' => new external_multiple_structure( new external_value(PARAM_RAW, 'General notifications') ), 'fieldnotifications' => new external_multiple_structure( new external_single_structure( array( 'fieldname' => new external_value(PARAM_TEXT, 'The field name.'), 'notification' => new external_value(PARAM_RAW, 'The notification for the field.'), ) ) ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.3 */ public static function update_entry_parameters() { return new external_function_parameters( array( 'entryid' => new external_value(PARAM_INT, 'The entry record id.'), 'data' => new external_multiple_structure( new external_single_structure( array( 'fieldid' => new external_value(PARAM_INT, 'The field id.'), 'subfield' => new external_value(PARAM_NOTAGS, 'The subfield name (if required).', VALUE_DEFAULT, null), 'value' => new external_value(PARAM_RAW, 'The new contents for the field always JSON encoded.'), ) ), 'The fields data to be updated' ), ) ); } /** * Updates an existing entry. * * @param int $entryid the data instance id * @param array $data the fields data to be created * @return array of warnings and status result * @since Moodle 3.3 * @throws moodle_exception */ public static function update_entry($entryid, $data) { global $DB; $params = array('entryid' => $entryid, 'data' => $data); $params = self::validate_parameters(self::update_entry_parameters(), $params); $warnings = array(); $fieldnotifications = array(); $updated = false; $record = $DB->get_record('data_records', array('id' => $params['entryid']), '*', MUST_EXIST); list($database, $course, $cm, $context) = self::validate_database($record->dataid); // Check database is open in time. data_require_time_available($database, null, $context); if (!data_user_can_manage_entry($record, $database, $context)) { throw new moodle_exception('noaccess', 'data'); } // Prepare the data as is expected by the API. $datarecord = new stdClass; foreach ($params['data'] as $data) { $subfield = ($data['subfield'] !== '') ? '_' . $data['subfield'] : ''; // We ask for JSON encoded values because of multiple choice forms or checkboxes that use array parameters. $datarecord->{'field_' . $data['fieldid'] . $subfield} = json_decode($data['value']); } // Validate to ensure that enough data was submitted. $fields = $DB->get_records('data_fields', array('dataid' => $database->id)); $processeddata = data_process_submission($database, $fields, $datarecord); // Format notifications. if (!empty($processeddata->fieldnotifications)) { foreach ($processeddata->fieldnotifications as $field => $notififications) { foreach ($notififications as $notif) { $fieldnotifications[] = [ 'fieldname' => $field, 'notification' => $notif, ]; } } } if ($processeddata->validated) { // Now update the fields contents. data_update_record_fields_contents($database, $record, $context, $datarecord, $processeddata); $updated = true; } $result = array( 'updated' => $updated, 'generalnotifications' => $processeddata->generalnotifications, 'fieldnotifications' => $fieldnotifications, 'warnings' => $warnings, ); return $result; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.3 */ public static function update_entry_returns() { return new external_single_structure( array( 'updated' => new external_value(PARAM_BOOL, 'True if the entry was successfully updated, false other wise.'), 'generalnotifications' => new external_multiple_structure( new external_value(PARAM_RAW, 'General notifications') ), 'fieldnotifications' => new external_multiple_structure( new external_single_structure( array( 'fieldname' => new external_value(PARAM_TEXT, 'The field name.'), 'notification' => new external_value(PARAM_RAW, 'The notification for the field.'), ) ) ), 'warnings' => new external_warnings() ) ); } } classes/form/import_presets.php000064400000006626152265517740012764 0ustar00. namespace mod_data\form; use context; use moodle_exception; use moodle_url; use core_form\dynamic_form; /** * Import presets form. * * @package mod_data * @copyright 2022 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_presets extends dynamic_form { /** * Process the form submission * * @return array * @throws moodle_exception */ public function process_dynamic_submission(): array { global $CFG; $filepath = $this->save_temp_file('importfile'); $context = $this->get_context_for_dynamic_submission(); $returnurl = new moodle_url('/mod/data/preset.php', [ 'id' => $context->instanceid, 'action' => 'importzip', 'filepath' => str_replace($CFG->tempdir, '', $filepath) ]); return [ 'result' => true, 'url' => $returnurl->out(false), ]; } /** * Get context * * @return context */ protected function get_context_for_dynamic_submission(): context { $cmid = $this->optional_param('cmid', null, PARAM_INT); $cm = get_coursemodule_from_id('data', $cmid); $context = \context_module::instance($cm->id); return $context; } /** * Set data * * @return void */ public function set_data_for_dynamic_submission(): void { $data = (object) [ 'cmid' => $this->optional_param('cmid', 0, PARAM_INT), ]; $this->set_data($data); } /** * Has access ? * * @return void * @throws moodle_exception */ protected function check_access_for_dynamic_submission(): void { if (!has_capability('mod/data:managetemplates', $this->get_context_for_dynamic_submission())) { throw new moodle_exception('importpresetmissingcapability', 'data'); } } /** * Get page URL * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $cmid = $this->optional_param('cmid', null, PARAM_INT); return new moodle_url('/mod/data/preset.php', ['id' => $cmid]); } /** * Form definition * * @return void */ protected function definition() { $mform = $this->_form; $mform->addElement('html', \html_writer::div(get_string('importpreset_desc', 'mod_data'), 'py-3')); $mform->addElement('hidden', 'cmid'); $mform->setType('cmid', PARAM_INT); $mform->addElement('filepicker', 'importfile', get_string('choosepreset', 'mod_data'), null, ['accepted_types' => '.zip']); $mform->addRule('importfile', null, 'required'); } } classes/form/save_as_preset.php000064400000022143152265517740012700 0ustar00. namespace mod_data\form; use context; use core\notification; use moodle_exception; use moodle_url; use core_form\dynamic_form; use mod_data\manager; use mod_data\preset; /** * Save database as preset form. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class save_as_preset extends dynamic_form { /** * Form definition */ protected function definition() { $this->_form->addElement('hidden', 'd'); $this->_form->setType('d', PARAM_INT); $this->_form->addElement('hidden', 'action', 'save'); $this->_form->setType('action', PARAM_ALPHANUM); $this->_form->addElement('hidden', 'oldpresetname', ''); $this->_form->setType('oldpresetname', PARAM_FILE); $this->_form->addElement('text', 'name', get_string('name'), ['size' => 60]); $this->_form->setType('name', PARAM_FILE); $this->_form->addRule('name', null, 'required'); // Overwrite checkbox will be hidden by default. It will only appear if there is an error when saving the preset. $this->_form->addElement('checkbox', 'overwrite', '', get_string('overrwritedesc', 'data'), ['class' => 'hidden']); $this->_form->addElement('textarea', 'description', get_string('description'), ['rows' => 5, 'cols' => 60]); $this->_form->setType('name', PARAM_TEXT); } /** * Return form context * * @return context */ protected function get_context_for_dynamic_submission(): context { global $DB; $d = $this->optional_param('d', null, PARAM_INT); $data = $DB->get_record('data', array('id' => $d), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $data->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('data', $data->id, $course->id, null, MUST_EXIST); return \context_module::instance($cm->id, MUST_EXIST); } /** * Perform some validation. * * @param array $formdata * @param array $files * @return array */ public function validation($formdata, $files): array { $errors = parent::validation($formdata, $files); $context = $this->get_context_for_dynamic_submission(); $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); $manager = manager::create_from_coursemodule($cm); if (!empty($formdata['overwrite'])) { $presets = $manager->get_available_presets(); $selectedpreset = new \stdClass(); foreach ($presets as $preset) { if ($preset->name == $formdata['name']) { $selectedpreset = $preset; break; } } if (!$selectedpreset instanceof preset || !$selectedpreset->can_manage()) { $errors['name'] = get_string('cannotoverwritepreset', 'data'); } } else if ($formdata['action'] == 'saveaspreset' || $formdata['oldpresetname'] != $formdata['name']) { // If the preset exists when a new preset is saved or name has changed, then we need to throw an error. $sitepresets = $manager->get_available_saved_presets(); $usercandelete = false; foreach ($sitepresets as $preset) { if ($formdata['name'] == $preset->name) { if ($preset->can_manage()) { $errors['name'] = get_string('errorpresetexists', 'data'); $usercandelete = true; } else { $errors['name'] = get_string('errorpresetexistsbutnotoverwrite', 'data'); } break; } } // If there are some errors, the checkbox should be displayed, to let users overwrite the preset. if (!empty($errors) && $usercandelete) { $this->_form->getElement('overwrite')->removeAttribute('class'); } } return $errors; } /** * Check if current user has access to this form, otherwise throw exception. * * @return void * @throws moodle_exception */ protected function check_access_for_dynamic_submission(): void { global $DB; if (!has_capability('mod/data:managetemplates', $this->get_context_for_dynamic_submission())) { throw new moodle_exception('saveaspresetmissingcapability', 'data'); } $action = $this->optional_param('action', '', PARAM_ALPHANUM); if ($action == 'saveaspreset') { // For saving it as a new preset, some fields need to be created; otherwise, an exception will be raised. $instanceid = $this->optional_param('d', null, PARAM_INT); $hasfields = $DB->record_exists('data_fields', ['dataid' => $instanceid]); if (!$hasfields) { throw new moodle_exception('nofieldindatabase', 'data'); } } } /** * Process the form submission, used if form was submitted via AJAX. * * @return array */ public function process_dynamic_submission(): array { global $DB, $CFG; require_once($CFG->dirroot . '/mod/data/lib.php'); $formdata = $this->get_data(); $result = false; $errors = []; $data = $DB->get_record('data', array('id' => $formdata->d), '*', MUST_EXIST); $course = $DB->get_record('course', array('id' => $data->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('data', $data->id, $course->id, null, MUST_EXIST); $context = \context_module::instance($cm->id, MUST_EXIST); try { $manager = manager::create_from_instance($data); if (!empty($formdata->overwrite)) { $presets = $manager->get_available_presets(); $selectedpreset = new \stdClass(); foreach ($presets as $preset) { if ($preset->name == $formdata->name) { $selectedpreset = $preset; break; } } if ($selectedpreset instanceof preset && $selectedpreset->can_manage()) { $selectedpreset->delete(); } } $presetname = $formdata->name; if (!empty($formdata->oldpresetname)) { $presetname = $formdata->oldpresetname; } $preset = preset::create_from_instance($manager, $presetname, $formdata->description); if (!empty($formdata->oldpresetname)) { // Update the name and the description, to save the new data. $preset->name = $formdata->name; $preset->description = $formdata->description; } $result = $preset->save(); if ($result) { // Add notification in the session to be shown when the page is reloaded on the JS side. $previewurl = new moodle_url( '/mod/data/preset.php', ['id' => $cm->id, 'fullname' => $preset->get_fullname(), 'action' => 'preview'] ); notification::success(get_string('savesuccess', 'mod_data', (object)['url' => $previewurl->out()])); } } catch (\Exception $exception) { $errors[] = $exception->getMessage(); } return [ 'result' => $result, 'errors' => $errors, ]; } /** * Load in existing data as form defaults. * * @return void */ public function set_data_for_dynamic_submission(): void { $data = (object)[ 'd' => $this->optional_param('d', 0, PARAM_INT), 'action' => $this->optional_param('action', '', PARAM_ALPHANUM), 'oldpresetname' => $this->optional_param('presetname', '', PARAM_FILE), 'name' => $this->optional_param('presetname', '', PARAM_FILE), 'description' => $this->optional_param('presetdescription', '', PARAM_TEXT), ]; $this->set_data($data); } /** * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX * * @return moodle_url */ protected function get_page_url_for_dynamic_submission(): moodle_url { $d = $this->optional_param('d', null, PARAM_INT); return new moodle_url('/user/field.php', ['d' => $d]); } } classes/external/content_exporter.php000064400000006523152265517740014162 0ustar00. /** * Class for exporting content associated to a record. * * @package mod_data * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use renderer_base; use core_external\external_files; use core_external\util as external_util; /** * Class for exporting content associated to a record. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class content_exporter extends exporter { protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'Content id.', ), 'fieldid' => array( 'type' => PARAM_INT, 'description' => 'The field type of the content.', 'default' => 0, ), 'recordid' => array( 'type' => PARAM_INT, 'description' => 'The record this content belongs to.', 'default' => 0, ), 'content' => array( 'type' => PARAM_RAW, 'description' => 'Contents.', 'null' => NULL_ALLOWED, ), 'content1' => array( 'type' => PARAM_RAW, 'description' => 'Contents.', 'null' => NULL_ALLOWED, ), 'content2' => array( 'type' => PARAM_RAW, 'description' => 'Contents.', 'null' => NULL_ALLOWED, ), 'content3' => array( 'type' => PARAM_RAW, 'description' => 'Contents.', 'null' => NULL_ALLOWED, ), 'content4' => array( 'type' => PARAM_RAW, 'description' => 'Contents.', 'null' => NULL_ALLOWED, ), ); } protected static function define_related() { return array( 'context' => 'context', ); } protected static function define_other_properties() { return array( 'files' => array( 'type' => external_files::get_properties_for_exporter(), 'multiple' => true, 'optional' => true, ), ); } protected function get_other_values(renderer_base $output) { $values = ['files' => external_util::get_area_files($this->related['context']->id, 'mod_data', 'content', $this->data->id)]; return $values; } } classes/external/database_summary_exporter.php000064400000020542152265517740016026 0ustar00. /** * Class for exporting partial database data. * * @package mod_data * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use renderer_base; use core_external\external_files; use core_external\util as external_util; /** * Class for exporting partial database data (some fields are only viewable by admins). * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class database_summary_exporter extends exporter { protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'Database id'), 'course' => array( 'type' => PARAM_INT, 'description' => 'Course id'), 'name' => array( 'type' => PARAM_RAW, 'description' => 'Database name'), 'intro' => array( 'type' => PARAM_RAW, 'description' => 'The Database intro', ), 'introformat' => array( 'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN), 'type' => PARAM_INT, 'default' => FORMAT_MOODLE ), 'lang' => array( 'type' => PARAM_LANG, 'description' => 'Forced activity language', 'null' => NULL_ALLOWED, ), 'comments' => array( 'type' => PARAM_BOOL, 'description' => 'comments enabled', ), 'timeavailablefrom' => array( 'type' => PARAM_INT, 'description' => 'timeavailablefrom field', ), 'timeavailableto' => array( 'type' => PARAM_INT, 'description' => 'timeavailableto field', ), 'timeviewfrom' => array( 'type' => PARAM_INT, 'description' => 'timeviewfrom field', ), 'timeviewto' => array( 'type' => PARAM_INT, 'description' => 'timeviewto field', ), 'requiredentries' => array( 'type' => PARAM_INT, 'description' => 'requiredentries field', ), 'requiredentriestoview' => array( 'type' => PARAM_INT, 'description' => 'requiredentriestoview field', ), 'maxentries' => array( 'type' => PARAM_INT, 'description' => 'maxentries field', ), 'rssarticles' => array( 'type' => PARAM_INT, 'description' => 'rssarticles field', ), 'singletemplate' => array( 'type' => PARAM_RAW, 'description' => 'singletemplate field', 'null' => NULL_ALLOWED, ), 'listtemplate' => array( 'type' => PARAM_RAW, 'description' => 'listtemplate field', 'null' => NULL_ALLOWED, ), 'listtemplateheader' => array( 'type' => PARAM_RAW, 'description' => 'listtemplateheader field', 'null' => NULL_ALLOWED, ), 'listtemplatefooter' => array( 'type' => PARAM_RAW, 'description' => 'listtemplatefooter field', 'null' => NULL_ALLOWED, ), 'addtemplate' => array( 'type' => PARAM_RAW, 'description' => 'addtemplate field', 'null' => NULL_ALLOWED, ), 'rsstemplate' => array( 'type' => PARAM_RAW, 'description' => 'rsstemplate field', 'null' => NULL_ALLOWED, ), 'rsstitletemplate' => array( 'type' => PARAM_RAW, 'description' => 'rsstitletemplate field', 'null' => NULL_ALLOWED, ), 'csstemplate' => array( 'type' => PARAM_RAW, 'description' => 'csstemplate field', 'null' => NULL_ALLOWED, ), 'jstemplate' => array( 'type' => PARAM_RAW, 'description' => 'jstemplate field', 'null' => NULL_ALLOWED, ), 'asearchtemplate' => array( 'type' => PARAM_RAW, 'description' => 'asearchtemplate field', 'null' => NULL_ALLOWED, ), 'approval' => array( 'type' => PARAM_BOOL, 'description' => 'approval field', ), 'manageapproved' => array( 'type' => PARAM_BOOL, 'description' => 'manageapproved field', ), 'scale' => array( 'type' => PARAM_INT, 'description' => 'scale field', 'optional' => true, ), 'assessed' => array( 'type' => PARAM_INT, 'description' => 'assessed field', 'optional' => true, ), 'assesstimestart' => array( 'type' => PARAM_INT, 'description' => 'assesstimestart field', 'optional' => true, ), 'assesstimefinish' => array( 'type' => PARAM_INT, 'description' => 'assesstimefinish field', 'optional' => true, ), 'defaultsort' => array( 'type' => PARAM_INT, 'description' => 'defaultsort field', ), 'defaultsortdir' => array( 'type' => PARAM_INT, 'description' => 'defaultsortdir field', ), 'editany' => array( 'type' => PARAM_BOOL, 'description' => 'editany field (not used any more)', 'optional' => true, ), 'notification' => array( 'type' => PARAM_INT, 'description' => 'notification field (not used any more)', 'optional' => true, ), 'timemodified' => array( 'type' => PARAM_INT, 'description' => 'Time modified', 'optional' => true, ), ); } protected static function define_related() { return array( 'context' => 'context' ); } protected static function define_other_properties() { return array( 'coursemodule' => array( 'type' => PARAM_INT ), 'introfiles' => array( 'type' => external_files::get_properties_for_exporter(), 'multiple' => true, 'optional' => true, ), ); } protected function get_other_values(renderer_base $output) { $context = $this->related['context']; $values = array( 'coursemodule' => $context->instanceid, 'introfiles' => external_util::get_area_files($context->id, 'mod_data', 'intro', false, false), ); return $values; } /** * Get the formatting parameters for the intro. * * @return array */ protected function get_format_parameters_for_intro() { return [ 'component' => 'mod_data', 'filearea' => 'intro', 'options' => array('noclean' => true), ]; } } classes/external/field_exporter.php000064400000005172152265517740013572 0ustar00. /** * Class for exporting field data. * * @package mod_data * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; /** * Class for exporting field data. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class field_exporter extends exporter { protected static function define_properties() { $properties = array( 'id' => array( 'type' => PARAM_INT, 'description' => 'Field id.', ), 'dataid' => array( 'type' => PARAM_INT, 'description' => 'The field type of the content.', 'default' => 0, ), 'type' => array( 'type' => PARAM_PLUGIN, 'description' => 'The field type.', ), 'name' => array( 'type' => PARAM_TEXT, 'description' => 'The field name.', ), 'description' => array( 'type' => PARAM_RAW, 'description' => 'The field description.', ), 'required' => array( 'type' => PARAM_BOOL, 'description' => 'Whether is a field required or not.', 'default' => 0, ), ); // Field possible parameters. for ($i = 1; $i <= 10; $i++) { $properties["param$i"] = array( 'type' => PARAM_RAW, 'description' => 'Field parameters', 'null' => NULL_ALLOWED, ); } return $properties; } protected static function define_related() { // Context is required for text formatting. return array( 'context' => 'context', ); } } classes/external/record_exporter.php000064400000011646152265517740013770 0ustar00. /** * Class for exporting record data. * * @package mod_data * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\external; defined('MOODLE_INTERNAL') || die(); use core\external\exporter; use renderer_base; use core_user; use core_tag\external\tag_item_exporter; /** * Class for exporting record data. * * @copyright 2017 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class record_exporter extends exporter { protected static function define_properties() { return array( 'id' => array( 'type' => PARAM_INT, 'description' => 'Record id.', ), 'userid' => array( 'type' => PARAM_INT, 'description' => 'The id of the user who created the record.', 'default' => 0, ), 'groupid' => array( 'type' => PARAM_INT, 'description' => 'The group id this record belongs to (0 for no groups).', 'default' => 0, ), 'dataid' => array( 'type' => PARAM_INT, 'description' => 'The database id this record belongs to.', 'default' => 0, ), 'timecreated' => array( 'type' => PARAM_INT, 'description' => 'Time the record was created.', 'default' => 0, ), 'timemodified' => array( 'type' => PARAM_INT, 'description' => 'Last time the record was modified.', 'default' => 0, ), 'approved' => array( 'type' => PARAM_BOOL, 'description' => 'Whether the entry has been approved (if the database is configured in that way).', 'default' => 0, ), ); } protected static function define_related() { return array( 'database' => 'stdClass', 'user' => 'stdClass?', 'context' => 'context', 'contents' => 'stdClass[]?', ); } protected static function define_other_properties() { return array( 'canmanageentry' => array( 'type' => PARAM_BOOL, 'description' => 'Whether the current user can manage this entry', ), 'fullname' => array( 'type' => PARAM_TEXT, 'description' => 'The user who created the entry fullname.', 'optional' => true, ), 'contents' => array( 'type' => content_exporter::read_properties_definition(), 'description' => 'The record contents.', 'multiple' => true, 'optional' => true, ), 'tags' => array( 'type' => tag_item_exporter::read_properties_definition(), 'description' => 'Tags.', 'multiple' => true, 'optional' => true, ), ); } protected function get_other_values(renderer_base $output) { global $PAGE; $values = array( 'canmanageentry' => data_user_can_manage_entry($this->data, $this->related['database'], $this->related['context']), ); if (!empty($this->related['user']) and !empty($this->related['user']->id)) { $values['fullname'] = fullname($this->related['user']); } else if ($this->data->userid) { $user = core_user::get_user($this->data->userid); $values['fullname'] = fullname($user); } if (!empty($this->related['contents'])) { $contents = []; foreach ($this->related['contents'] as $content) { $related = array('context' => $this->related['context']); $exporter = new content_exporter($content, $related); $contents[] = $exporter->export($PAGE->get_renderer('core')); } $values['contents'] = $contents; } $values['tags'] = \core_tag\external\util::get_item_tags('mod_data', 'data_records', $this->data->id); return $values; } } classes/external/get_mapping_information.php000064400000007372152265517740015462 0ustar00. namespace mod_data\external; use core\notification; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_data\local\importer\preset_importer; use mod_data\manager; /** * This is the external method for deleting a saved preset. * * @package mod_data * @since Moodle 4.1 * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_mapping_information extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'cmid' => new external_value(PARAM_INT, 'Id of the data activity', VALUE_REQUIRED), 'importedpreset' => new external_value(PARAM_TEXT, 'Preset to be imported'), ]); } /** * Get importing information for the given database course module. * * @param int $cmid Id of the course module where to import the preset. * @param string $importedpreset Plugin or saved preset to be imported. * @return array Information needed to decide whether to show the dialogue or not. */ public static function execute(int $cmid, string $importedpreset): array { $params = self::validate_parameters( self::execute_parameters(), ['cmid' => $cmid, 'importedpreset' => $importedpreset] ); try { // Let's get the manager. list($course, $cm) = get_course_and_cm_from_cmid($params['cmid'], manager::MODULE); $manager = manager::create_from_coursemodule($cm); $importer = preset_importer::create_from_plugin_or_directory($manager, $params['importedpreset']); $result['data'] = $importer->get_mapping_information(); } catch (\moodle_exception $e) { $result['warnings'][] = [ 'item' => $importedpreset, 'warningcode' => 'exception', 'message' => $e->getMessage() ]; notification::error($e->getMessage()); } return $result; } /** * Return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'data' => new external_single_structure([ 'needsmapping' => new external_value(PARAM_BOOL, 'Whether the importing needs mapping or not'), 'presetname' => new external_value(PARAM_TEXT, 'Name of the applied preset'), 'fieldstocreate' => new external_value(PARAM_TEXT, 'List of field names to create'), 'fieldstoremove' => new external_value(PARAM_TEXT, 'List of field names to remove'), ], 'Information to import if everything went fine', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ]); } } classes/external/delete_saved_preset.php000064400000011162152265517740014561 0ustar00. namespace mod_data\external; use core\notification; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_data\manager; use mod_data\preset; /** * This is the external method for deleting a saved preset. * * @package mod_data * @since Moodle 4.1 * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_saved_preset extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'dataid' => new external_value(PARAM_INT, 'Id of the data activity', VALUE_REQUIRED), 'presetnames' => new external_multiple_structure( new external_value(PARAM_TEXT, 'The preset name to delete', VALUE_REQUIRED) ) ]); } /** * Delete saved preset from the file system. * * @param int $dataid Id of the data activity to check context and permissions. * @param array $presetnames List of saved preset names to delete. * @return array True if the content has been deleted; false and the warning, otherwise. */ public static function execute(int $dataid, array $presetnames): array { global $DB; $result = false; $warnings = []; $params = self::validate_parameters(self::execute_parameters(), ['dataid' => $dataid, 'presetnames' => $presetnames]); $instance = $DB->get_record('data', ['id' => $params['dataid']], '*', MUST_EXIST); $manager = manager::create_from_instance($instance); foreach ($params['presetnames'] as $presetname) { try { $preset = preset::create_from_instance($manager, $presetname); if ($preset->can_manage()) { if ($preset->delete()) { notification::success(get_string('presetdeleted', 'mod_data')); $result = true; } else { // An error ocurred while deleting the preset. $warnings[] = [ 'item' => $presetname, 'warningcode' => 'failedpresetdelete', 'message' => get_string('failedpresetdelete', 'mod_data') ]; notification::error(get_string('failedpresetdelete', 'mod_data')); } } else { // The user has no permission to delete the preset. $warnings[] = [ 'item' => $presetname, 'warningcode' => 'cannotdeletepreset', 'message' => get_string('cannotdeletepreset', 'mod_data') ]; notification::error(get_string('cannotdeletepreset', 'mod_data')); } } catch (\moodle_exception $e) { // The saved preset has not been deleted. $warnings[] = [ 'item' => $presetname, 'warningcode' => 'exception', 'message' => $e->getMessage() ]; notification::error($e->getMessage()); } } return [ 'result' => $result, 'warnings' => $warnings ]; } /** * Return. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'result' => new external_value(PARAM_BOOL, 'The processing result'), 'warnings' => new external_warnings() ]); } } classes/privacy/provider.php000064400000002762152265517740012246 0ustar00. /** * Privacy Subsystem implementation for filter_data. * * @package filter_data * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace filter_data\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for filter_data implementing null_provider. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } classes/privacy/datafield_provider.php000064400000006347152265517740014246 0ustar00. /** * Contains interface datafield_provider * * @package mod_data * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\privacy; defined('MOODLE_INTERNAL') || die(); /** * Interface datafield_provider, all datafield plugins need to implement it * * @package mod_data * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface datafield_provider extends \core_privacy\local\request\plugin\subplugin_provider, // The data subplugins do not need to do anything themselves for the shared_userlist. // This is all handled by the parent plugin. \core_privacy\local\request\shared_userlist_provider { /** * Exports data about one record in {data_content} table. * * Datafield plugins providers should implement this method to: * - preprocess references to files in the response (examples - textarea, picture, file) * - make content more human-readable (example - replace values separators in multimenu, format date in date) * - add more information about the field itself (example - list all options for menu, multimenu, radio) * * Sample implementation (from datafield_textarea): * * $defaultvalue->content = writer::with_context($context) * ->rewrite_pluginfile_urls([$recordobj->id, $contentobj->id], 'mod_data', 'content', $contentobj->id, * $defaultvalue->content); * writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue); /** * Allows plugins to delete locally stored data. * * Usually datafield plugins do not store anything and this method will be empty. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj); } classes/dates.php000064400000004414152265517740010033 0ustar00. /** * Contains the class for fetching the important dates in mod_data for a given module instance and a user. * * @package mod_data * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_data; use core\activity_dates; /** * Class for fetching the important dates in mod_data for a given module instance and a user. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dates extends activity_dates { /** * Returns a list of important dates in mod_data * * @return array */ protected function get_dates(): array { $timeopen = $this->cm->customdata['timeavailablefrom'] ?? null; $timeclose = $this->cm->customdata['timeavailableto'] ?? null; $now = time(); $dates = []; if ($timeopen) { $openlabelid = $timeopen > $now ? 'activitydate:opens' : 'activitydate:opened'; $dates[] = [ 'dataid' => 'timeavailablefrom', 'label' => get_string($openlabelid, 'course'), 'timestamp' => (int) $timeopen, ]; } if ($timeclose) { $closelabelid = $timeclose > $now ? 'activitydate:closes' : 'activitydate:closed'; $dates[] = [ 'dataid' => 'timeavailableto', 'label' => get_string($closelabelid, 'course'), 'timestamp' => (int) $timeclose, ]; } return $dates; } } classes/local/importer/preset_upload_importer.php000064400000003261152265517740016454 0ustar00. namespace mod_data\local\importer; use mod_data\manager; /** * Data preset importer for uploaded presets * * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class preset_upload_importer extends preset_importer { /** * Constructor * * @param manager $manager * @param string $filepath */ public function __construct(manager $manager, string $filepath) { if (is_file($filepath)) { $fp = get_file_packer(); if ($fp->extract_to_pathname($filepath, $filepath.'_extracted')) { fulldelete($filepath); } $filepath .= '_extracted'; } parent::__construct($manager, $filepath); } /** * Clean uploaded files up * * @return bool Wether the preset has been successfully cleaned up. */ public function cleanup(): bool { return fulldelete($this->directory); } } classes/local/importer/entries_importer.php000064400000013606152265517740015263 0ustar00. namespace mod_data\local\importer; use coding_exception; use core_php_time_limit; use file_packer; /** * Importer class for importing data and - if needed - files as well from a zip archive. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class entries_importer { /** @var string The import file path of the file which data should be imported from. */ protected string $importfilepath; /** @var string The original name of the import file name including extension of the file which data should be imported from. */ protected string $importfilename; /** @var string $importfiletype The file type of the import file. */ protected string $importfiletype; /** @var file_packer Zip file packer to extract files from a zip archive. */ private file_packer $packer; /** @var bool Tracks state if zip archive has been extracted already. */ private bool $zipfileextracted; /** @var string Temporary directory where zip archive is being extracted to. */ private string $extracteddir; /** * Creates an entries_importer object. * * This object can be used to import data from data files (like csv) and zip archives both including a data file and files to be * stored in the course module context. * * @param string $importfilepath the complete path of the import file including filename * @param string $importfilename the import file name as uploaded by the user * @throws coding_exception if a wrong file type is being used */ public function __construct(string $importfilepath, string $importfilename) { $this->importfilepath = $importfilepath; $this->importfilename = $importfilename; $this->importfiletype = pathinfo($importfilename, PATHINFO_EXTENSION); $this->zipfileextracted = false; if ($this->importfiletype !== $this->get_import_data_file_extension() && $this->importfiletype !== 'zip') { throw new coding_exception('Only "zip" or "' . $this->get_import_data_file_extension() . '" files are ' . 'allowed.'); } } /** * Return the file extension of the import data file which is being used, for example 'csv' for a csv entries_importer. * * @return string the file extension of the export data file */ abstract public function get_import_data_file_extension(): string; /** * Returns the file content of the data file. * * Returns the content of the file directly if the entries_importer's file is a data file itself. * If the entries_importer's file is a zip archive, the content of the first found data file in the * zip archive's root will be returned. * * @return false|string the data file content as string; false, if file cannot be found/read */ public function get_data_file_content(): false|string { if ($this->importfiletype !== 'zip') { // We have no zip archive, so the file itself must be the data file. return file_get_contents($this->importfilepath); } // So we have a zip archive and need to find the right data file in the root of the zip archive. $this->extract_zip(); $datafilenames = array_filter($this->packer->list_files($this->importfilepath), fn($file) => pathinfo($file->pathname, PATHINFO_EXTENSION) === $this->get_import_data_file_extension() && !str_contains($file->pathname, '/')); if (empty($datafilenames) || count($datafilenames) > 1) { return false; } return file_get_contents($this->extracteddir . reset($datafilenames)->pathname); } /** * Returns the file content from a file which has been stored in the zip archive. * * @param string $filename * @param string $zipsubdir * @return false|string the file content as string, false if the file could not be found/read */ public function get_file_content_from_zip(string $filename, string $zipsubdir = 'files/'): false|string { if (empty($filename)) { // Nothing to return. return false; } // Just to be sure extract if not extracted yet. $this->extract_zip(); if (!str_ends_with($zipsubdir, '/')) { $zipsubdir .= '/'; } $filepathinextractedzip = $this->extracteddir . $zipsubdir . $filename; return file_exists($filepathinextractedzip) ? file_get_contents($filepathinextractedzip) : false; } /** * Extracts (if not already done and if we have a zip file to deal with) the zip file to a temporary directory. * * @return void */ private function extract_zip(): void { if ($this->zipfileextracted || $this->importfiletype !== 'zip') { return; } $this->packer = get_file_packer(); core_php_time_limit::raise(180); $this->extracteddir = make_request_directory(); if (!str_ends_with($this->extracteddir, '/')) { $this->extracteddir .= '/'; } $this->packer->extract_to_pathname($this->importfilepath, $this->extracteddir); $this->zipfileextracted = true; } } classes/local/importer/preset_existing_importer.php000064400000004666152265517740017034 0ustar00. namespace mod_data\local\importer; use mod_data\manager; /** * Data preset importer for existing presets * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class preset_existing_importer extends preset_importer { /** @var int user id. */ protected $userid; /** @var string fullname of the preset. */ private $fullname; /** * Constructor * * @param manager $manager * @param string $fullname */ public function __construct(manager $manager, string $fullname) { global $USER; list($userid, $shortname) = explode('/', $fullname, 2); $context = $manager->get_context(); if ($userid && ($userid != $USER->id) && !has_capability('mod/data:manageuserpresets', $context) && !has_capability('mod/data:viewalluserpresets', $context) ) { throw new \coding_exception('Invalid preset provided'); } $this->userid = intval($userid); $this->fullname = $fullname; $cm = $manager->get_coursemodule(); $course = $cm->get_course(); $filepath = data_preset_path($course, $userid, $shortname); parent::__construct($manager, $filepath); } /** * Returns user ID * * @return int userid */ public function get_userid(): int { return $this->userid; } /** * Returns the information we need to build the importer selector. * * @return array Value and name for the preset importer selector */ public function get_preset_selector(): array { return ['name' => 'fullname', 'value' => $this->get_userid().'/'.$this->get_directory()]; } } classes/local/importer/csv_entries_importer.php000064400000021745152265517740016141 0ustar00. namespace mod_data\local\importer; use context_module; use core_php_time_limit; use core_tag_tag; use core_user; use csv_import_reader; use moodle_exception; use stdClass; /** * CSV entries_importer class for importing data and - if needed - files as well from a zip archive. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class csv_entries_importer extends entries_importer { /** @var array Log entries for successfully added records. */ private array $addedrecordsmessages = []; /** * Declares the entries_importer to use a csv file as data file. * * @see entries_importer::get_import_data_file_extension() */ public function get_import_data_file_extension(): string { return 'csv'; } /** * Import records for a data instance from csv data. * * @param stdClass $cm Course module of the data instance. * @param stdClass $data The data instance. * @param string $encoding The encoding of csv data. * @param string $fielddelimiter The delimiter of the csv data. * * @throws moodle_exception */ public function import_csv(stdClass $cm, stdClass $data, string $encoding, string $fielddelimiter): void { global $CFG, $DB; // Large files are likely to take their time and memory. Let PHP know // that we'll take longer, and that the process should be recycled soon // to free up memory. core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); $iid = csv_import_reader::get_new_iid('moddata'); $cir = new csv_import_reader($iid, 'moddata'); $context = context_module::instance($cm->id); $readcount = $cir->load_csv_content($this->get_data_file_content(), $encoding, $fielddelimiter); if (empty($readcount)) { throw new \moodle_exception('csvfailed', 'data', "{$CFG->wwwroot}/mod/data/edit.php?d={$data->id}"); } else { if (!$fieldnames = $cir->get_columns()) { throw new \moodle_exception('cannotreadtmpfile', 'error'); } // Check the fieldnames are valid. $rawfields = $DB->get_records('data_fields', ['dataid' => $data->id], '', 'name, id, type'); $fields = []; $errorfield = ''; $usernamestring = get_string('username'); $safetoskipfields = [get_string('user'), get_string('email'), get_string('timeadded', 'data'), get_string('timemodified', 'data'), get_string('approved', 'data'), get_string('tags', 'data')]; $userfieldid = null; foreach ($fieldnames as $id => $name) { if (!isset($rawfields[$name])) { if ($name == $usernamestring) { $userfieldid = $id; } else if (!in_array($name, $safetoskipfields)) { $errorfield .= "'$name' "; } } else { // If this is the second time, a field with this name comes up, it must be a field not provided by the user... // like the username. if (isset($fields[$name])) { if ($name == $usernamestring) { $userfieldid = $id; } unset($fieldnames[$id]); // To ensure the user provided content fields remain in the array once flipped. } else { $field = $rawfields[$name]; $field->type = clean_param($field->type, PARAM_ALPHA); $filepath = "$CFG->dirroot/mod/data/field/$field->type/field.class.php"; if (!file_exists($filepath)) { $errorfield .= "'$name' "; continue; } require_once($filepath); $classname = 'data_field_' . $field->type; $fields[$name] = new $classname($field, $data, $cm); } } } if (!empty($errorfield)) { throw new \moodle_exception('fieldnotmatched', 'data', "{$CFG->wwwroot}/mod/data/edit.php?d={$data->id}", $errorfield); } $fieldnames = array_flip($fieldnames); $cir->init(); while ($record = $cir->next()) { $authorid = null; if ($userfieldid) { if (!($author = core_user::get_user_by_username($record[$userfieldid], 'id'))) { $authorid = null; } else { $authorid = $author->id; } } // Determine presence of "approved" field within the record to import. $approved = true; if (array_key_exists(get_string('approved', 'data'), $fieldnames)) { $approvedindex = $fieldnames[get_string('approved', 'data')]; $approved = !empty($record[$approvedindex]); } if ($recordid = data_add_record($data, 0, $authorid, $approved)) { // Add instance to data_record. foreach ($fields as $field) { $fieldid = $fieldnames[$field->field->name]; if (isset($record[$fieldid])) { $value = $record[$fieldid]; } else { $value = ''; } if (method_exists($field, 'update_content_import')) { $field->update_content_import($recordid, $value, 'field_' . $field->field->id); } else { $content = new stdClass(); $content->fieldid = $field->field->id; $content->content = $value; $content->recordid = $recordid; if ($field->file_import_supported() && $this->importfiletype === 'zip') { $filecontent = $this->get_file_content_from_zip($content->content); if (!$filecontent) { // No corresponding file in zip archive, so no record for this field being added at all. continue; } $contentid = $DB->insert_record('data_content', $content); $field->import_file_value($contentid, $filecontent, $content->content); } else { $DB->insert_record('data_content', $content); } } } if (core_tag_tag::is_enabled('mod_data', 'data_records') && isset($fieldnames[get_string('tags', 'data')])) { $columnindex = $fieldnames[get_string('tags', 'data')]; $rawtags = $record[$columnindex]; $tags = explode(',', $rawtags); foreach ($tags as $tag) { $tag = trim($tag); if (empty($tag)) { continue; } core_tag_tag::add_item_tag('mod_data', 'data_records', $recordid, $context, $tag); } } $this->addedrecordsmessages[] = get_string('added', 'moodle', count($this->addedrecordsmessages) + 1) . ". " . get_string('entry', 'data') . " (ID $recordid)\n"; } } $cir->close(); $cir->cleanup(true); } } /** * Getter for the array of messages for added records. * * For each successfully added record the array contains a log message. * * @return array Array of message strings: For each added record one message string */ public function get_added_records_messages(): array { return $this->addedrecordsmessages; } } classes/local/importer/preset_importer.php000064400000044144152265517740015115 0ustar00. namespace mod_data\local\importer; use core\notification; use mod_data\manager; use mod_data\preset; use stdClass; use html_writer; /** * Abstract class used for data preset importers * * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class preset_importer { /** @var manager manager instance. */ private $manager; /** @var string directory where to find the preset. */ protected $directory; /** @var array fields to remove. */ public $fieldstoremove; /** @var array fields to update. */ public $fieldstoupdate; /** @var array fields to create. */ public $fieldstocreate; /** @var array settings to be imported. */ public $settings; /** * Constructor * * @param manager $manager * @param string $directory */ public function __construct(manager $manager, string $directory) { $this->manager = $manager; $this->directory = $directory; // Read the preset and saved result. $this->settings = $this->get_preset_settings(); } /** * Returns the name of the directory the preset is located in * * @return string */ public function get_directory(): string { return basename($this->directory); } /** * Retreive the contents of a file. That file may either be in a conventional directory of the Moodle file storage * * @param \file_storage|null $filestorage . Should be null if using a conventional directory * @param \stored_file|null $fileobj the directory to look in. null if using a conventional directory * @param string|null $dir the directory to look in. null if using the Moodle file storage * @param string $filename the name of the file we want * @return string|null the contents of the file or null if the file doesn't exist. */ public function get_file_contents( ?\file_storage &$filestorage, ?\stored_file &$fileobj, ?string $dir, string $filename ): ?string { if (empty($filestorage) || empty($fileobj)) { if (substr($dir, -1) != '/') { $dir .= '/'; } if (file_exists($dir.$filename)) { return file_get_contents($dir.$filename); } else { return null; } } else { if ($filestorage->file_exists( DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $fileobj->get_filepath(), $filename) ) { $file = $filestorage->get_file( DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $fileobj->get_filepath(), $filename ); return $file->get_content(); } else { return null; } } } /** * Gets the preset settings * * @return stdClass Settings to be imported. */ public function get_preset_settings(): stdClass { global $CFG; require_once($CFG->libdir.'/xmlize.php'); $fs = null; $fileobj = null; if (!preset::is_directory_a_preset($this->directory)) { // Maybe the user requested a preset stored in the Moodle file storage. $fs = get_file_storage(); $files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA); // Preset name to find will be the final element of the directory. $explodeddirectory = explode('/', $this->directory); $presettofind = end($explodeddirectory); // Now go through the available files available and see if we can find it. foreach ($files as $file) { if (($file->is_directory() && $file->get_filepath() == '/') || !$file->is_directory()) { continue; } $presetname = trim($file->get_filepath(), '/'); if ($presetname == $presettofind) { $this->directory = $presetname; $fileobj = $file; } } if (empty($fileobj)) { throw new \moodle_exception('invalidpreset', 'data', '', $this->directory); } } $allowedsettings = [ 'intro', 'comments', 'requiredentries', 'requiredentriestoview', 'maxentries', 'rssarticles', 'approval', 'defaultsortdir', 'defaultsort' ]; $module = $this->manager->get_instance(); $result = new stdClass; $result->settings = new stdClass; $result->importfields = []; $result->currentfields = $this->manager->get_field_records(); // Grab XML. $presetxml = $this->get_file_contents($fs, $fileobj, $this->directory, 'preset.xml'); $parsedxml = xmlize($presetxml, 0); // First, do settings. Put in user friendly array. $settingsarray = $parsedxml['preset']['#']['settings'][0]['#']; $result->settings = new StdClass(); foreach ($settingsarray as $setting => $value) { if (!is_array($value) || !in_array($setting, $allowedsettings)) { // Unsupported setting. continue; } $result->settings->$setting = $value[0]['#']; } // Now work out fields to user friendly array. if ( array_key_exists('preset', $parsedxml) && array_key_exists('#', $parsedxml['preset']) && array_key_exists('field', $parsedxml['preset']['#'])) { $fieldsarray = $parsedxml['preset']['#']['field']; foreach ($fieldsarray as $field) { if (!is_array($field)) { continue; } $fieldstoimport = new StdClass(); foreach ($field['#'] as $param => $value) { if (!is_array($value)) { continue; } $fieldstoimport->$param = $value[0]['#']; } $fieldstoimport->dataid = $module->id; $fieldstoimport->type = clean_param($fieldstoimport->type, PARAM_ALPHA); $result->importfields[] = $fieldstoimport; } } // Calculate default mapping. if (is_null($this->fieldstoremove) && is_null($this->fieldstocreate) && is_null($this->fieldstoupdate)) { $this->set_affected_fields($result->importfields, $result->currentfields); } // Now add the HTML templates to the settings array so we can update d. foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { $result->settings->$templatename = $this->get_file_contents( $fs, $fileobj, $this->directory, $templatefile ); } $result->settings->instance = $module->id; return $result; } /** * Import the preset into the given database module * * @param bool $overwritesettings Whether to overwrite activity settings or not. * @return bool Wether the importing has been successful. */ public function import(bool $overwritesettings): bool { global $DB, $OUTPUT, $CFG; $settings = $this->settings->settings; $currentfields = $this->settings->currentfields; $missingfieldtypes = []; $module = $this->manager->get_instance(); foreach ($this->fieldstoupdate as $currentid => $updatable) { if ($currentid != -1 && isset($currentfields[$currentid])) { $fieldobject = data_get_field_from_id($currentfields[$currentid]->id, $module); $toupdate = false; foreach ($updatable as $param => $value) { if ($param != "id" && $fieldobject->field->$param !== $value) { $fieldobject->field->$param = $value; } } unset($fieldobject->field->similarfield); $fieldobject->update_field(); unset($fieldobject); } } foreach ($this->fieldstocreate as $newfield) { /* Make a new field */ $filepath = $CFG->dirroot."/mod/data/field/$newfield->type/field.class.php"; if (!file_exists($filepath)) { $missingfieldtypes[] = $newfield->name; continue; } include_once($filepath); if (!isset($newfield->description)) { $newfield->description = ''; } $classname = 'data_field_' . $newfield->type; $fieldclass = new $classname($newfield, $module); $fieldclass->insert_field(); unset($fieldclass); } if (!empty($missingfieldtypes)) { echo $OUTPUT->notification(get_string('missingfieldtypeimport', 'data') . html_writer::alist($missingfieldtypes)); } // Get rid of all old unused data. foreach ($currentfields as $cid => $currentfield) { if (!array_key_exists($cid, $this->fieldstoupdate)) { // Delete all information related to fields. $todelete = data_get_field_from_id($currentfield->id, $module); $todelete->delete_field(); } } // Handle special settings here. if (!empty($settings->defaultsort)) { if (is_numeric($settings->defaultsort)) { // Old broken value. $settings->defaultsort = 0; } else { $settings->defaultsort = (int)$DB->get_field( 'data_fields', 'id', ['dataid' => $module->id, 'name' => $settings->defaultsort] ); } } else { $settings->defaultsort = 0; } // Do we want to overwrite all current database settings? if ($overwritesettings) { // All supported settings. $overwrite = array_keys((array)$settings); } else { // Only templates and sorting. $overwrite = ['singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate', 'jstemplate', 'asearchtemplate', 'defaultsortdir', 'defaultsort']; } // Now overwrite current data settings. foreach ($module as $prop => $unused) { if (in_array($prop, $overwrite)) { $module->$prop = $settings->$prop; } } data_update_instance($module); return $this->cleanup(); } /** * Returns information about the fields needs to be removed, updated or created. * * @param array $newfields Array of new fields to be applied. * @param array $currentfields Array of current fields on database activity. * @return void */ public function set_affected_fields(array $newfields = [], array $currentfields = []): void { $fieldstoremove = []; $fieldstocreate = []; $preservedfields = []; // Maps fields and makes new ones. if (!empty($newfields)) { // We require an injective mapping, and need to know what to protect. foreach ($newfields as $newid => $newfield) { $preservedfieldid = optional_param("field_$newid", -1, PARAM_INT); if (array_key_exists($preservedfieldid, $preservedfields)) { throw new \moodle_exception('notinjectivemap', 'data'); } if ($preservedfieldid == -1) { // Let's check if there is any field with same type and name that we could map to. foreach ($currentfields as $currentid => $currentfield) { if (($currentfield->type == $newfield->type) && ($currentfield->name == $newfield->name) && !array_key_exists($currentid, $preservedfields)) { // We found a possible default map. $preservedfieldid = $currentid; $preservedfields[$currentid] = $newfield; } } } if ($preservedfieldid == -1) { // We need to create a new field. $fieldstocreate[] = $newfield; } else { $preservedfields[$preservedfieldid] = $newfield; } } } foreach ($currentfields as $currentid => $currentfield) { if (!array_key_exists($currentid, $preservedfields)) { $fieldstoremove[] = $currentfield; } } $this->fieldstocreate = $fieldstocreate; $this->fieldstoremove = $fieldstoremove; $this->fieldstoupdate = $preservedfields; } /** * Any clean up routines should go here * * @return bool Wether the preset has been successfully cleaned up. */ public function cleanup(): bool { return true; } /** * Check if the importing process needs fields mapping. * * @return bool True if the current database needs to map the fields imported. */ public function needs_mapping(): bool { if (!$this->manager->has_fields()) { return false; } return (!empty($this->fieldstocreate) || !empty($this->fieldstoremove)); } /** * Returns the information we need to build the importer selector. * * @return array Value and name for the preset importer selector */ public function get_preset_selector(): array { return ['name' => 'directory', 'value' => $this->get_directory()]; } /** * Helper function to finish up the import routine. * * Called from fields and presets pages. * * @param bool $overwritesettings Whether to overwrite activity settings or not. * @param stdClass $instance database instance object * @return void */ public function finish_import_process(bool $overwritesettings, stdClass $instance): void { $result = $this->import($overwritesettings); if ($result) { notification::success(get_string('importsuccess', 'mod_data')); } else { notification::error(get_string('cannotapplypreset', 'mod_data')); } $backurl = new \moodle_url('/mod/data/field.php', ['d' => $instance->id]); redirect($backurl); } /** * Get the right importer instance from the provided parameters (POST or GET) * * @param manager $manager the current database manager * @return preset_importer the relevant preset_importer instance * @throws \moodle_exception when the file provided as parameter (POST or GET) does not exist */ public static function create_from_parameters(manager $manager): preset_importer { $fullname = optional_param('fullname', '', PARAM_PATH); // Directory the preset is in. if (!$fullname) { $fullname = required_param('directory', PARAM_FILE); } return self::create_from_plugin_or_directory($manager, $fullname); } /** * Get the right importer instance from the provided parameters (POST or GET) * * @param manager $manager the current database manager * @param string $pluginordirectory The plugin name or directory to create the importer from. * @return preset_importer the relevant preset_importer instance */ public static function create_from_plugin_or_directory(manager $manager, string $pluginordirectory): preset_importer { global $CFG; if (!$pluginordirectory) { throw new \moodle_exception('emptypresetname', 'mod_data'); } try { $presetdir = $CFG->tempdir . '/forms/' . $pluginordirectory; if (file_exists($presetdir) && is_dir($presetdir)) { return new preset_upload_importer($manager, $presetdir); } else { return new preset_existing_importer($manager, $pluginordirectory); } } catch (\moodle_exception $e) { throw new \moodle_exception('errorpresetnotfound', 'mod_data', '', $pluginordirectory); } } /** * Get the information needed to decide the modal * * @return array An array with all the information to decide the mapping */ public function get_mapping_information(): array { return [ 'needsmapping' => $this->needs_mapping(), 'presetname' => preset::get_name_from_plugin($this->get_directory()), 'fieldstocreate' => $this->get_field_names($this->fieldstocreate), 'fieldstoremove' => $this->get_field_names($this->fieldstoremove), ]; } /** * Returns a list of the fields * * @param array $fields Array of fields to get name from. * @return string A string listing the names of the fields. */ public function get_field_names(array $fields): string { $fieldnames = array_map(function($field) { return $field->name; }, $fields); return implode(', ', $fieldnames); } } classes/local/exporter/ods_entries_exporter.php000064400000004113152265517740016137 0ustar00. namespace mod_data\local\exporter; use MoodleODSWorkbook; use MoodleODSWriter; /** * ODS entries exporter for mod_data. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class ods_entries_exporter extends entries_exporter { /** * Returns the file extension of this entries exporter. * * @see \mod_data\local\exporter\entries_exporter::get_export_data_file_extension() */ public function get_export_data_file_extension(): string { return 'ods'; } /** * Returns the ods data exported by the ODS library for further handling. * * @see \mod_data\local\exporter\entries_exporter::get_data_file_content() */ public function get_data_file_content(): string { global $CFG; require_once("$CFG->libdir/odslib.class.php"); $filearg = '-'; $workbook = new MoodleODSWorkbook($filearg); $worksheet = []; $worksheet[0] = $workbook->add_worksheet(''); $rowno = 0; foreach ($this->exportdata as $row) { $colno = 0; foreach ($row as $col) { $worksheet[0]->write($rowno, $colno, $col); $colno++; } $rowno++; } $writer = new MoodleODSWriter($worksheet); return $writer->get_file_content(); } } classes/local/exporter/csv_entries_exporter.php000064400000005621152265517740016152 0ustar00. namespace mod_data\local\exporter; use coding_exception; use csv_export_writer; /** * CSV entries exporter for mod_data. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class csv_entries_exporter extends entries_exporter { /** @var string[] Possible delimiter names. Only used internally to check if a valid delimiter name * has been specified. */ private const POSSIBLE_DELIMITER_NAMES = ['comma', 'tab', 'semicolon', 'colon', 'cfg']; /** * @var string name of the delimiter to use for the csv export. Possible values: * 'comma', 'tab', 'semicolon', 'colon' or 'cfg'. */ private string $delimitername = 'comma'; /** * Returns the csv data exported by the csv_export_writer for further handling. * * @see \mod_data\local\exporter\entries_exporter::get_data_file_content() */ public function get_data_file_content(): string { global $CFG; require_once($CFG->libdir . '/csvlib.class.php'); return csv_export_writer::print_array($this->exportdata, $this->delimitername, '"', true); } /** * Returns the file extension of this entries exporter. * * @see \mod_data\local\exporter\entries_exporter::get_export_data_file_extension() */ public function get_export_data_file_extension(): string { return 'csv'; } /** * Setter for the delimiter name which should be used in this csv_entries_exporter object. * * Calling this setter is optional, the delimiter name defaults to 'comma'. * * @param string $delimitername one of 'comma', 'tab', 'semicolon', 'colon' or 'cfg' * @return void * @throws coding_exception if a wrong delimiter name has been specified */ public function set_delimiter_name(string $delimitername): void { if (!in_array($delimitername, self::POSSIBLE_DELIMITER_NAMES)) { throw new coding_exception('Wrong delimiter type', 'Please choose on of the following delimiters: ' . '\"comma\", \"tab\", \"semicolon\", \"colon\", \"cfg\"'); } $this->delimitername = $delimitername; } } classes/local/exporter/entries_exporter.php000064400000024327152265517740015303 0ustar00. namespace mod_data\local\exporter; use file_serving_exception; use moodle_exception; use zip_archive; /** * Exporter class for exporting data and - if needed - files as well in a zip archive. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class entries_exporter { /** @var int Tracks the currently edited row of the export data file. */ private int $currentrow; /** * @var array The data structure containing the data for exporting. It's a 2-dimensional array of * rows and columns. */ protected array $exportdata; /** @var string Name of the export file name without extension. */ protected string $exportfilename; /** @var zip_archive The zip archive object we store all the files in, if we need to export files as well. */ private zip_archive $ziparchive; /** @var bool Tracks the state if the zip archive already has been closed. */ private bool $isziparchiveclosed; /** @var string full path of the zip archive. */ private string $zipfilepath; /** @var array Array to store all filenames in the zip archive for export. */ private array $filenamesinzip; /** * Creates an entries_exporter object. * * This object can be used to export data to different formats including files. If files are added, * everything will be bundled up in a zip archive. */ public function __construct() { $this->currentrow = 0; $this->exportdata = []; $this->exportfilename = 'Exportfile'; $this->filenamesinzip = []; $this->isziparchiveclosed = true; } /** * Adds a row (array of strings) to the export data. * * @param array $row the row to add, $row has to be a plain array of strings * @return void */ public function add_row(array $row): void { $this->exportdata[] = $row; $this->currentrow++; } /** * Adds a data string (so the content for a "cell") to the current row. * * @param string $cellcontent the content to add to the current row * @return void */ public function add_to_current_row(string $cellcontent): void { $this->exportdata[$this->currentrow][] = $cellcontent; } /** * Signal the entries_exporter to finish the current row and jump to the next row. * * @return void */ public function next_row(): void { $this->currentrow++; } /** * Sets the name of the export file. * * Only use the basename without path and without extension here. * * @param string $exportfilename name of the file without path and extension * @return void */ public function set_export_file_name(string $exportfilename): void { $this->exportfilename = $exportfilename; } /** * The entries_exporter will prepare a data file from the rows and columns being added. * Overwrite this method to generate the data file as string. * * @return string the data file as a string */ abstract protected function get_data_file_content(): string; /** * Overwrite the method to return the file extension your data file will have, for example * return 'csv'; for a csv file entries_exporter. * * @return string the file extension of the data file your entries_exporter is using */ abstract protected function get_export_data_file_extension(): string; /** * Returns the count of currently stored records (rows excluding header row). * * @return int the count of records/rows */ public function get_records_count(): int { // The attribute $this->exportdata also contains a header. If only one row is present, this // usually is the header, so record count should be 0. if (count($this->exportdata) <= 1) { return 0; } return count($this->exportdata) - 1; } /** * Use this method to add a file which should be exported to the entries_exporter. * * @param string $filename the name of the file which should be added * @param string $filecontent the content of the file as a string * @param string $zipsubdir the subdirectory in the zip archive. Defaults to 'files/'. * @return void * @throws moodle_exception if there is an error adding the file to the zip archive */ public function add_file_from_string(string $filename, string $filecontent, string $zipsubdir = 'files/'): void { if (empty($this->filenamesinzip)) { // No files added yet, so we need to create a zip archive. $this->create_zip_archive(); } if (!str_ends_with($zipsubdir, '/')) { $zipsubdir .= '/'; } $zipfilename = $zipsubdir . $filename; $this->filenamesinzip[] = $zipfilename; $this->ziparchive->add_file_from_string($zipfilename, $filecontent); } /** * Sends the generated export file. * * Care: By default this function finishes the current PHP request and directly serves the file to the user as download. * * @param bool $sendtouser true if the file should be sent directly to the user, if false the file content will be returned * as string * @return string|null file content as string if $sendtouser is true * @throws moodle_exception if there is an issue adding the data file * @throws file_serving_exception if the file could not be served properly */ public function send_file(bool $sendtouser = true): null|string { if (empty($this->filenamesinzip)) { if ($sendtouser) { send_file($this->get_data_file_content(), $this->exportfilename . '.' . $this->get_export_data_file_extension(), null, 0, true, true); return null; } else { return $this->get_data_file_content(); } } $this->add_file_from_string($this->exportfilename . '.' . $this->get_export_data_file_extension(), $this->get_data_file_content(), '/'); $this->finish_zip_archive(); if ($this->isziparchiveclosed) { if ($sendtouser) { send_file($this->zipfilepath, $this->exportfilename . '.zip', null, 0, false, true); return null; } else { return file_get_contents($this->zipfilepath); } } else { throw new file_serving_exception('Could not serve zip file, it could not be closed properly.'); } } /** * Checks if a file with the given name has already been added to the file export bundle. * * Care: Filenames are compared to all files in the specified zip subdirectory which * defaults to 'files/'. * * @param string $filename the filename containing the zip path of the file to check * @param string $zipsubdir The subdirectory in which the filename should be looked for, * defaults to 'files/' * @return bool true if file with the given name already exists, false otherwise */ public function file_exists(string $filename, string $zipsubdir = 'files/'): bool { if (!str_ends_with($zipsubdir, '/')) { $zipsubdir .= '/'; } if (empty($filename)) { return false; } return in_array($zipsubdir . $filename, $this->filenamesinzip, true); } /** * Creates a unique filename based on the given filename. * * This method adds "_1", "_2", ... to the given file name until the newly generated filename * is not equal to any of the already saved ones in the export file bundle. * * @param string $filename the filename based on which a unique filename should be generated * @return string the unique filename */ public function create_unique_filename(string $filename): string { if (!$this->file_exists($filename)) { return $filename; } $extension = pathinfo($filename, PATHINFO_EXTENSION); $filenamewithoutextension = empty($extension) ? $filename : substr($filename, 0,strlen($filename) - strlen($extension) - 1); $filenamewithoutextension = $filenamewithoutextension . '_1'; $i = 1; $filename = empty($extension) ? $filenamewithoutextension : $filenamewithoutextension . '.' . $extension; while ($this->file_exists($filename)) { // In case we have already a file ending with '_XX' where XX is an ascending number, we have to // remove '_XX' first before adding '_YY' again where YY is the successor of XX. $filenamewithoutextension = preg_replace('/_' . $i . '$/', '_' . ($i + 1), $filenamewithoutextension); $filename = empty($extension) ? $filenamewithoutextension : $filenamewithoutextension . '.' . $extension; $i++; } return $filename; } /** * Prepares the zip archive. * * @return void */ private function create_zip_archive(): void { $tmpdir = make_request_directory(); $this->zipfilepath = $tmpdir . '/' . $this->exportfilename . '.zip'; $this->ziparchive = new zip_archive(); $this->isziparchiveclosed = !$this->ziparchive->open($this->zipfilepath); } /** * Closes the zip archive. * * @return void */ private function finish_zip_archive(): void { if (!$this->isziparchiveclosed) { $this->isziparchiveclosed = $this->ziparchive->close(); } } } classes/local/exporter/utils.php000064400000015026152265517740013036 0ustar00. namespace mod_data\local\exporter; use context; use context_system; /** * Utility class for exporting data from a mod_data instance. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class utils { /** * Exports the data of the mod_data instance to an entries_exporter object which then can export it to a file format. * * @param int $dataid * @param array $fields * @param array $selectedfields * @param entries_exporter $exporter the entries_exporter object used * @param int $currentgroup group ID of the current group. This is used for * exporting data while maintaining group divisions. * @param context|null $context the context in which the operation is performed (for capability checks) * @param bool $userdetails whether to include the details of the record author * @param bool $time whether to include time created/modified * @param bool $approval whether to include approval status * @param bool $tags whether to include tags * @param bool $includefiles whether files should be exported as well * @return void */ public static function data_exportdata(int $dataid, array $fields, array $selectedfields, entries_exporter $exporter, int $currentgroup = 0, ?context $context = null, bool $userdetails = false, bool $time = false, bool $approval = false, bool $tags = false, bool $includefiles = true): void { global $DB; if (is_null($context)) { $context = context_system::instance(); } // Exporting user data needs special permission. $userdetails = $userdetails && has_capability('mod/data:exportuserinfo', $context); // Populate the header in first row of export. $header = []; foreach ($fields as $key => $field) { if (!in_array($field->field->id, $selectedfields)) { // Ignore values we aren't exporting. unset($fields[$key]); } else { $header[] = $field->field->name; } } if ($tags) { $header[] = get_string('tags', 'data'); } if ($userdetails) { $header[] = get_string('user'); $header[] = get_string('username'); $header[] = get_string('email'); } if ($time) { $header[] = get_string('timeadded', 'data'); $header[] = get_string('timemodified', 'data'); } if ($approval) { $header[] = get_string('approved', 'data'); } $exporter->add_row($header); $datarecords = $DB->get_records('data_records', array('dataid' => $dataid)); ksort($datarecords); $line = 1; foreach ($datarecords as $record) { // Get content indexed by fieldid. if ($currentgroup) { $select = 'SELECT c.fieldid, c.content, c.content1, c.content2, c.content3, c.content4 FROM {data_content} c, ' . '{data_records} r WHERE c.recordid = ? AND r.id = c.recordid AND r.groupid = ?'; $where = array($record->id, $currentgroup); } else { $select = 'SELECT fieldid, content, content1, content2, content3, content4 FROM {data_content} WHERE recordid = ?'; $where = array($record->id); } if ($content = $DB->get_records_sql($select, $where)) { foreach ($fields as $field) { $contents = ''; if (isset($content[$field->field->id])) { $contents = $field->export_text_value($content[$field->field->id]); if (!empty($contents) && $field->file_export_supported() && $includefiles && !is_null($field->export_file_value($record))) { // For exporting overwrite the content of the column with a unique // filename, even it is not exactly the name of the file in the // mod_data instance content. But it's more important to match the name // of the exported file. $contents = $exporter->create_unique_filename($contents); $exporter->add_file_from_string($contents, $field->export_file_value($record)); } } // Just be double sure. $contents = !empty($contents) ? $contents : ''; $exporter->add_to_current_row($contents); } if ($tags) { $itemtags = \core_tag_tag::get_item_tags_array('mod_data', 'data_records', $record->id); $exporter->add_to_current_row(implode(', ', $itemtags)); } if ($userdetails) { // Add user details to the export data. $userdata = get_complete_user_data('id', $record->userid); // In case of an already deleted user, we do not receive any userdata and export an empty string instead. $exporter->add_to_current_row($userdata ? fullname($userdata) : ''); $exporter->add_to_current_row($userdata ? $userdata->username : ''); $exporter->add_to_current_row($userdata ? $userdata->email : ''); } if ($time) { // Add time added / modified. $exporter->add_to_current_row(userdate($record->timecreated)); $exporter->add_to_current_row(userdate($record->timemodified)); } if ($approval) { // Add approval status. $exporter->add_to_current_row((int) $record->approved); } } $exporter->next_row(); } } } classes/manager.php000064400000040442152265517740010346 0ustar00. namespace mod_data; use cm_info; use context_module; use completion_info; use data_field_base; use mod_data_renderer; use mod_data\event\course_module_viewed; use mod_data\event\template_viewed; use mod_data\event\template_updated; use moodle_page; use core_component; use stdClass; /** * Class manager for database activity * * @package mod_data * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** Module name. */ const MODULE = 'data'; /** The plugin name. */ const PLUGINNAME = 'mod_data'; /** Template list with their files required to save the information of a preset. */ const TEMPLATES_LIST = [ 'listtemplate' => 'listtemplate.html', 'singletemplate' => 'singletemplate.html', 'asearchtemplate' => 'asearchtemplate.html', 'addtemplate' => 'addtemplate.html', 'rsstemplate' => 'rsstemplate.html', 'csstemplate' => 'csstemplate.css', 'jstemplate' => 'jstemplate.js', 'listtemplateheader' => 'listtemplateheader.html', 'listtemplatefooter' => 'listtemplatefooter.html', 'rsstitletemplate' => 'rsstitletemplate.html', ]; /** @var string plugin path. */ public $path; /** @var stdClass course_module record. */ private $instance; /** @var context_module the current context. */ private $context; /** @var cm_info course_modules record. */ private $cm; /** @var array the current data_fields records. * Do not access this attribute directly, use $this->get_field_records instead */ private $_fieldrecords = null; /** * Class constructor. * * @param cm_info $cm course module info object * @param stdClass $instance activity instance object. */ public function __construct(cm_info $cm, stdClass $instance) { global $CFG; $this->cm = $cm; $this->instance = $instance; $this->context = context_module::instance($cm->id); $this->instance->cmidnumber = $cm->idnumber; $this->path = $CFG->dirroot . '/mod/' . self::MODULE; } /** * Create a manager instance from an instance record. * * @param stdClass $instance an activity record * @return manager */ public static function create_from_instance(stdClass $instance): self { $cm = get_coursemodule_from_instance(self::MODULE, $instance->id); // Ensure that $this->cm is a cm_info object. $cm = cm_info::create($cm); return new self($cm, $instance); } /** * Create a manager instance from a course_modules record. * * @param stdClass|cm_info $cm an activity record * @return manager */ public static function create_from_coursemodule($cm): self { global $DB; // Ensure that $this->cm is a cm_info object. $cm = cm_info::create($cm); $instance = $DB->get_record(self::MODULE, ['id' => $cm->instance], '*', MUST_EXIST); return new self($cm, $instance); } /** * Create a manager instance from a data_record entry. * * @param stdClass $record the data_record record * @return manager */ public static function create_from_data_record($record): self { global $DB; $instance = $DB->get_record(self::MODULE, ['id' => $record->dataid], '*', MUST_EXIST); $cm = get_coursemodule_from_instance(self::MODULE, $instance->id); $cm = cm_info::create($cm); return new self($cm, $instance); } /** * Return the current context. * * @return context_module */ public function get_context(): context_module { return $this->context; } /** * Return the current instance. * * @return stdClass the instance record */ public function get_instance(): stdClass { return $this->instance; } /** * Return the current cm_info. * * @return cm_info the course module */ public function get_coursemodule(): cm_info { return $this->cm; } /** * Return the current module renderer. * * @param moodle_page|null $page the current page * @return mod_data_renderer the module renderer */ public function get_renderer(?moodle_page $page = null): mod_data_renderer { global $PAGE; $page = $page ?? $PAGE; return $page->get_renderer(self::PLUGINNAME); } /** * Trigger module viewed event and set the module viewed for completion. * * @param stdClass $course course object */ public function set_module_viewed(stdClass $course) { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Trigger module viewed event. $event = course_module_viewed::create([ 'objectid' => $this->instance->id, 'context' => $this->context, ]); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('course_modules', $this->cm); $event->add_record_snapshot(self::MODULE, $this->instance); $event->trigger(); // Completion. $completion = new completion_info($course); $completion->set_module_viewed($this->cm); } /** * Trigger module template viewed event. */ public function set_template_viewed() { // Trigger an event for viewing templates. $event = template_viewed::create([ 'context' => $this->context, 'courseid' => $this->cm->course, 'other' => [ 'dataid' => $this->instance->id, ], ]); $event->add_record_snapshot(self::MODULE, $this->instance); $event->trigger(); } /** * Return if the database has records. * * @return bool true if the database has records */ public function has_records(): bool { global $DB; return $DB->record_exists('data_records', ['dataid' => $this->instance->id]); } /** * Return if the database has fields. * * @return bool true if the database has fields */ public function has_fields(): bool { global $DB; if ($this->_fieldrecords === null) { return $DB->record_exists('data_fields', ['dataid' => $this->instance->id]); } return !empty($this->_fieldrecords); } /** * Return the database fields. * * @return data_field_base[] the field instances. */ public function get_fields(): array { $result = []; $fieldrecords = $this->get_field_records(); foreach ($fieldrecords as $fieldrecord) { $result[$fieldrecord->id] = $this->get_field($fieldrecord); } return $result; } /** * Return the field records (the current data_fields records). * * @return stdClass[] an array of records */ public function get_field_records() { global $DB; if ($this->_fieldrecords === null) { $this->_fieldrecords = $DB->get_records('data_fields', ['dataid' => $this->instance->id], 'id'); } return $this->_fieldrecords; } /** * Return a specific field instance from a field record. * * @param stdClass $fieldrecord the fieldrecord to convert * @return data_field_base the data field class instance */ public function get_field(stdClass $fieldrecord): data_field_base { global $CFG; // Some old field plugins require $CFG to be in the scope. $filepath = "{$this->path}/field/{$fieldrecord->type}/field.class.php"; $classname = "data_field_{$fieldrecord->type}"; if (!file_exists($filepath)) { return new data_field_base($fieldrecord, $this->instance, $this->cm); } require_once($filepath); if (!class_exists($classname)) { return new data_field_base($fieldrecord, $this->instance, $this->cm); } $newfield = new $classname($fieldrecord, $this->instance, $this->cm); return $newfield; } /** * Return a specific template. * * NOTE: this method returns a default template if the module template is empty. * However, it won't update the template database field. * * Some possible options: * - search: string with the current searching text. * - page: integer repesenting the current pagination page numbre (if any) * - baseurl: a moodle_url object to the current page. * * @param string $templatename * @param array $options extra display options array * @return template the template instance */ public function get_template(string $templatename, array $options = []): template { if ($templatename === 'single') { $templatename = 'singletemplate'; } $instance = $this->instance; $templatecontent = $instance->{$templatename} ?? ''; if (empty($templatecontent)) { $templatecontent = data_generate_default_template($instance, $templatename, 0, false, false); } $options['templatename'] = $templatename; // Some templates have extra options. $options = array_merge($options, template::get_default_display_options($templatename)); return new template($this, $templatecontent, $options); } /** Check if the user can manage templates on the current context. * * @param int $userid the user id to check ($USER->id if null). * @return bool if the user can manage templates on current context. */ public function can_manage_templates(?int $userid = null): bool { global $USER; if (!$userid) { $userid = $USER->id; } return has_capability('mod/data:managetemplates', $this->context, $userid); } /** Check if the user can export entries on the current context. * * @param int $userid the user id to check ($USER->id if null). * @return bool if the user can export entries on current context. */ public function can_export_entries(?int $userid = null): bool { global $USER, $DB; if (!$userid) { $userid = $USER->id; } // Exportallentries and exportentry are basically the same capability. return has_capability('mod/data:exportallentries', $this->context) || has_capability('mod/data:exportentry', $this->context) || (has_capability('mod/data:exportownentry', $this->context) && $DB->record_exists('data_records', ['userid' => $userid, 'dataid' => $this->instance->id])); } /** * Update the database templates. * * @param stdClass $newtemplates an object with all the new templates * @return bool if updated successfully. */ public function update_templates(stdClass $newtemplates): bool { global $DB; $record = (object)[ 'id' => $this->instance->id, ]; foreach (self::TEMPLATES_LIST as $templatename => $templatefile) { if (!isset($newtemplates->{$templatename})) { continue; } $record->{$templatename} = $newtemplates->{$templatename}; } // The add entry form cannot repeat tags. if (isset($record->addtemplate) && !data_tags_check($this->instance->id, $record->addtemplate)) { return false; } $DB->update_record(self::MODULE, $record); $this->instance = $DB->get_record(self::MODULE, ['id' => $this->cm->instance], '*', MUST_EXIST); // Trigger an event for saving the templates. $event = template_updated::create(array( 'context' => $this->context, 'courseid' => $this->cm->course, 'other' => array( 'dataid' => $this->instance->id, ) )); $event->trigger(); return true; } /** * Reset all templates. * * @return bool if the reset is done or not */ public function reset_all_templates(): bool { $newtemplates = new stdClass(); foreach (self::TEMPLATES_LIST as $templatename => $templatefile) { $newtemplates->{$templatename} = ''; } return $this->update_templates($newtemplates); } /** * Reset all templates related to a specific template. * * @param string $templatename the template name * @return bool if the reset is done or not */ public function reset_template(string $templatename): bool { $newtemplates = new stdClass(); // Reset the template to default. $newtemplates->{$templatename} = ''; if ($templatename == 'listtemplate') { $newtemplates->listtemplateheader = ''; $newtemplates->listtemplatefooter = ''; } if ($templatename == 'rsstemplate') { $newtemplates->rsstitletemplate = ''; } return $this->update_templates($newtemplates); } /** Check if the user can view a specific preset. * * @param preset $preset the preset instance. * @param int $userid the user id to check ($USER->id if null). * @return bool if the user can view the preset. */ public function can_view_preset(preset $preset, ?int $userid = null): bool { global $USER; if (!$userid) { $userid = $USER->id; } $presetuserid = $preset->get_userid(); if ($presetuserid && $presetuserid != $userid) { return has_capability('mod/data:viewalluserpresets', $this->context, $userid); } return true; } /** * Returns an array of all the available presets. * * @return array A list with the datapreset plugins and the presets saved by users. */ public function get_available_presets(): array { // First load the datapreset plugins that exist within the modules preset dir. $pluginpresets = static::get_available_plugin_presets(); // Then find the presets that people have saved. $savedpresets = static::get_available_saved_presets(); return array_merge($pluginpresets, $savedpresets); } /** * Returns an array of all the presets that users have saved to the site. * * @return array A list with the preset saved by the users. */ public function get_available_saved_presets(): array { global $USER; $presets = []; $fs = get_file_storage(); $files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA); if (empty($files)) { return $presets; } $canviewall = has_capability('mod/data:viewalluserpresets', $this->get_context()); foreach ($files as $file) { $isnotdirectory = ($file->is_directory() && $file->get_filepath() == '/') || !$file->is_directory(); $userid = $file->get_userid(); $cannotviewfile = !$canviewall && $userid != $USER->id; if ($isnotdirectory || $cannotviewfile) { continue; } $preset = preset::create_from_storedfile($this, $file); $presets[] = $preset; } return $presets; } /** * Returns an array of all the available plugin presets. * * @return array A list with the datapreset plugins. */ public static function get_available_plugin_presets(): array { $presets = []; $dirs = core_component::get_plugin_list('datapreset'); foreach ($dirs as $dir => $fulldir) { if (preset::is_directory_a_preset($fulldir)) { $preset = preset::create_from_plugin(null, $dir); $presets[] = $preset; } } return $presets; } } classes/plugininfo/datafield.php000064400000002205152265517740013016 0ustar00. /** * Subplugin info class. * * @package mod_data * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); class datafield extends base { public function is_uninstall_allowed() { global $DB; return !$DB->record_exists('data_fields', array('type'=>$this->name)); } } classes/plugininfo/datapreset.php000064400000002070152265517740013235 0ustar00. /** * Subplugin info class. * * @package mod_data * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\plugininfo; use core\plugininfo\base; defined('MOODLE_INTERNAL') || die(); class datapreset extends base { public function is_uninstall_allowed() { return true; } } classes/search/activity.php000064400000002605152265517740012034 0ustar00. /** * Search area for mod_data activities. * * @package mod_data * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\search; defined('MOODLE_INTERNAL') || die(); /** * Search area for mod_data activities. * * @package mod_data * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } classes/search/entry.php000064400000032340152265517740011340 0ustar00. /** * Search area for mod_data activity entries. * * @package mod_data * @copyright 2016 Devang Gaur * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\search; use mod_data\manager; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->dirroot . '/lib/grouplib.php'); /** * Search area for mod_data activity entries. * * @package mod_data * @copyright 2016 Devang Gaur * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class entry extends \core_search\base_mod { /** * @var array Internal quick static cache. */ protected $entriesdata = array(); /** * Returns recordset containing required data for indexing database entries. * * @param int $modifiedfrom timestamp * @param \context|null $context Optional context to restrict scope of returned results * @return moodle_recordset|null Recordset (or null if no results) */ public function get_document_recordset($modifiedfrom = 0, ?\context $context = null) { global $DB; list ($contextjoin, $contextparams) = $this->get_context_restriction_sql( $context, 'data', 'd', SQL_PARAMS_NAMED); if ($contextjoin === null) { return null; } $sql = "SELECT dr.*, d.course FROM {data_records} dr JOIN {data} d ON d.id = dr.dataid $contextjoin WHERE dr.timemodified >= :timemodified"; return $DB->get_recordset_sql($sql, array_merge($contextparams, ['timemodified' => $modifiedfrom])); } /** * Returns the documents associated with this glossary entry id. * * @param stdClass $entry glossary entry. * @param array $options * @return \core_search\document */ public function get_document($entry, $options = array()) { try { $cm = $this->get_cm('data', $entry->dataid, $entry->course); $context = \context_module::instance($cm->id); } catch (\dml_missing_record_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving mod_data ' . $entry->id . ' document, not all required data is available: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } catch (\dml_exception $ex) { // Notify it as we run here as admin, we should see everything. debugging('Error retrieving mod_data' . $entry->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER); return false; } // Prepare associative array with data from DB. $doc = \core_search\document_factory::instance($entry->id, $this->componentname, $this->areaname); $doc->set('contextid', $context->id); $doc->set('courseid', $entry->course); $doc->set('userid', $entry->userid); if ($entry->groupid > 0) { $doc->set('groupid', $entry->groupid); } $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID); $doc->set('modified', $entry->timemodified); $indexfields = $this->get_fields_for_entries($entry); if (count($indexfields) < 2) { return false; } // All fields should be already returned as plain text by data_field_base::get_content_value. $doc->set('title', $indexfields[0]); $doc->set('content', $indexfields[1]); if (isset($indexfields[2])) { $doc->set('description1', $indexfields[2]); } if (isset($indexfields[3])) { $doc->set('description2', $indexfields[3]); } return $doc; } /** * Whether the user can access the document or not. * * @throws \dml_missing_record_exception * @throws \dml_exception * @param int $id Glossary entry id * @return bool */ public function check_access($id) { global $DB, $USER; if (isguestuser()) { return \core_search\manager::ACCESS_DENIED; } $now = time(); $sql = "SELECT dr.*, d.* FROM {data_records} dr JOIN {data} d ON d.id = dr.dataid WHERE dr.id = ?"; $entry = $DB->get_record_sql($sql, array( $id ), IGNORE_MISSING); if (!$entry) { return \core_search\manager::ACCESS_DELETED; } if (($entry->timeviewfrom && $now < $entry->timeviewfrom) || ($entry->timeviewto && $now > $entry->timeviewto)) { return \core_search\manager::ACCESS_DENIED; } $cm = $this->get_cm('data', $entry->dataid, $entry->course); $context = \context_module::instance($cm->id); $canmanageentries = has_capability('mod/data:manageentries', $context); if (!has_capability('mod/data:viewentry', $context)) { return \core_search\manager::ACCESS_DENIED; } $numberofentriesindb = $DB->count_records('data_records', array('dataid' => $entry->dataid)); $requiredentriestoview = $entry->requiredentriestoview; if ($requiredentriestoview && ($requiredentriestoview > $numberofentriesindb) && ($USER->id != $entry->userid) && !$canmanageentries) { return \core_search\manager::ACCESS_DENIED; } if ($entry->approval && !$entry->approved && ($entry->userid != $USER->id) && !$canmanageentries) { return \core_search\manager::ACCESS_DENIED; } $currentgroup = groups_get_activity_group($cm, true); $groupmode = groups_get_activity_groupmode($cm); if (($groupmode == 1) && ($entry->groupid != $currentgroup) && !$canmanageentries) { return \core_search\manager::ACCESS_DENIED; } return \core_search\manager::ACCESS_GRANTED; } /** * Link to database entry. * * @param \core_search\document $doc * @return \moodle_url */ public function get_doc_url(\core_search\document $doc) { $entry = $this->get_entry($doc->get('itemid')); return new \moodle_url('/mod/data/view.php', array( 'd' => $entry->dataid, 'rid' => $entry->id )); } /** * Link to the database activity. * * @param \core_search\document $doc * @return \moodle_url */ public function get_context_url(\core_search\document $doc) { $entry = $this->get_entry($doc->get('itemid')); return new \moodle_url('/mod/data/view.php', array('d' => $entry->dataid)); } /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } /** * Add the database entries attachments. * * @param \core_search\document $doc * @return void */ public function attach_files($doc) { global $DB; $entryid = $doc->get('itemid'); try { $entry = $this->get_entry($entryid); } catch (\dml_missing_record_exception $e) { debugging('Could not get record to attach files to '.$doc->get('id'), DEBUG_DEVELOPER); return; } $cm = $this->get_cm('data', $entry->dataid, $doc->get('courseid')); $context = \context_module::instance($cm->id); // Get all content fields which have files in them. $contentssql = " SELECT con.* FROM {data_content} con JOIN {files} fil ON fil.component = :component AND fil.filearea = :filearea AND fil.itemid = con.id WHERE con.recordid = :recordid "; $contents = $DB->get_recordset_sql($contentssql, [ 'recordid' => $entryid, 'component' => 'mod_data', 'filearea' => 'content', ]); foreach ($contents as $content) { // Get the files and attach them. $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_data', 'content', $content->id, 'filename', false); foreach ($files as $file) { $doc->add_stored_file($file); } } $contents->close(); } /** * Get database entry data * * @throws \dml_exception * @param int $entryid * @return stdClass */ protected function get_entry($entryid) { global $DB; if (empty($this->entriesdata[$entryid])) { $this->entriesdata[$entryid] = $DB->get_record('data_records', array( 'id' => $entryid ), '*', MUST_EXIST); } return $this->entriesdata[$entryid]; } /** * get_fields_for_entries * * @param StdClass $entry * @return array */ protected function get_fields_for_entries($entry) { global $DB; $indexfields = array(); $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url'); $sql = "SELECT dc.*, df.name AS fldname, df.type AS fieldtype, df.required FROM {data_content} dc, {data_fields} df WHERE dc.fieldid = df.id AND dc.recordid = :recordid"; $contents = $DB->get_records_sql($sql, ['recordid' => $entry->id]); $filteredcontents = []; $data = $DB->get_record('data', ['id' => $entry->dataid]); $manager = manager::create_from_instance($data); $template = $manager->get_template('addtemplate'); $template = $template->get_template_content(); // Filtering out the data_content records having invalid fieldtypes. foreach ($contents as $content) { if (in_array($content->fieldtype, $validfieldtypes)) { $filteredcontents[] = $content; } } foreach ($filteredcontents as $content) { $classname = $this->get_field_class_name($content->fieldtype); if (!$classname) { $content->addtemplateposition = -1; continue; } $content->priority = $classname::get_priority(); $content->addtemplateposition = strpos($template ?? '', '[['.$content->fldname.']]'); } $orderqueue = new \SPLPriorityQueue(); // Filtering out contents which belong to fields that aren't present in the addtemplate of the database activity instance. foreach ($filteredcontents as $content) { if ($content->addtemplateposition >= 0) { $orderqueue->insert($content, $content->addtemplateposition); } } $filteredcontents = array(); while ($orderqueue->valid()) { $filteredcontents[] = $orderqueue->extract(); } // SPLPriorityQueue sorts according to descending order of the priority (here, addtemplateposition). $filteredcontents = array_reverse($filteredcontents); // Using a CUSTOM SPLPriorityQueure instance to sort out the filtered contents according to these rules : // 1. Priorities in $fieldtypepriorities // 2. Compulsory fieldtypes are to be given the top priority. $contentqueue = new sortedcontentqueue($filteredcontents); foreach ($filteredcontents as $key => $content) { $contentqueue->insert($content, $key); } while ($contentqueue->valid()) { $content = $contentqueue->extract(); $classname = $this->get_field_class_name($content->fieldtype); $indexfields[] = $classname::get_content_value($content); } // Limited to 4 fields as a document only has 4 content fields. if (count($indexfields) > 4) { $indexfields[3] = implode(' ', array_slice($indexfields, 3)); } return $indexfields; } /** * Returns the class name for the given field type and includes it. * * @param string $fieldtype * @return string|null It will return the class name or null if the field type is not available. */ protected function get_field_class_name(string $fieldtype): ?string { global $CFG; $fieldtype = trim($fieldtype); $fieldpath = $CFG->dirroot . '/mod/data/field/' . $fieldtype . '/field.class.php'; if (!file_exists($fieldpath)) { return null; } require_once($fieldpath); return 'data_field_' . $fieldtype; } /** * Confirms that data entries support group restrictions. * * @return bool True */ public function supports_group_restriction() { return true; } } classes/search/sortedcontentqueue.php000064400000004531152265517740014140 0ustar00. /** * Priority Queue class to sort out db entry contents. * * @package mod_data * @copyright 2016 Devang Gaur * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\search; defined('MOODLE_INTERNAL') || die(); /** * Priority Queue class to sort out db entry contents. * * @package mod_data * @copyright 2016 Devang Gaur * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sortedcontentqueue extends \SPLPriorityQueue { /** * @var array All contents that will be sorted. */ private $contents; /** * contructor * * @param array $contents * @return void */ public function __construct($contents) { $this->contents = $contents; } /** * comparator function overriden for sorting the records * ...as per 'required' and 'priotirity' field values * * @param int $key1 * @param int $key2 * @return int */ public function compare($key1 , $key2): int { $record1 = $this->contents[$key1]; $record2 = $this->contents[$key2]; // If a content's fieldtype is compulsory in the database than it would have priority than any other noncompulsory content. if ( ($record1->required && $record2->required) || (!$record1->required && !$record2->required)) { if ($record1->priority === $record2->priority) { return $key1 < $key2 ? 1 : -1; } return $record1->priority < $record2->priority ? -1 : 1; } else if ($record1->required && !$record2->required) { return 1; } else { return -1; } } }classes/output/fields_action_bar.php000064400000005073152265517740013724 0ustar00. namespace mod_data\output; use templatable; use renderable; /** * Renderable class for the action bar elements in the field pages in the database activity. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fields_action_bar implements templatable, renderable { /** @var int $id The database module id. */ private $id; /** * The class constructor. * * @param int $id The database module id * @param null $unused1 This parameter has been deprecated since 4.1 and should not be used anymore. * @param null $unused2 This parameter has been deprecated since 4.1 and should not be used anymore. * @param null $unused3 This parameter has been deprecated since 4.1 and should not be used anymore. * @param null $unused4 This parameter has been deprecated since 4.1 and should not be used anymore. * @param \action_menu|null $unused5 This parameter has been deprecated since 4.2 and should not be used anymore. */ public function __construct(int $id, $unused1 = null, $unused2 = null, $unused3 = null, $unused4 = null, ?\action_menu $unused5 = null) { if ($unused1 !== null || $unused2 !== null || $unused3 !== null || $unused4 !== null || $unused5 !== null) { debugging('Deprecated argument passed to fields_action_bar constructor', DEBUG_DEVELOPER); } $this->id = $id; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { $data = [ 'd' => $this->id, 'title' => get_string('managefields', 'mod_data'), ]; return $data; } } classes/output/add_entries_action.php000064400000004543152265517740014114 0ustar00. namespace mod_data\output; use moodle_url; use templatable; use renderable; /** * Renderable class for the Add entries button in the database activity. * * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class add_entries_action implements templatable, renderable { /** @var int $id The database module id. */ private $id; /** * The class constructor. * * @param int $id The database module id. * @param bool $hasentries Whether entries exist. */ public function __construct(int $id) { $this->id = $id; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the add entries button. * @return \stdClass or null if the user has no permission to add new entries. */ public function export_for_template(\renderer_base $output): ?\stdClass { global $PAGE, $DB; $database = $DB->get_record('data', ['id' => $this->id]); $cm = get_coursemodule_from_instance('data', $this->id); $currentgroup = groups_get_activity_group($cm); $groupmode = groups_get_activity_groupmode($cm); if (data_user_can_add_entry($database, $currentgroup, $groupmode, $PAGE->context)) { $addentrylink = new moodle_url('/mod/data/edit.php', ['d' => $this->id, 'backto' => $PAGE->url->out(false)]); $button = new \single_button($addentrylink, get_string('add', 'mod_data'), 'get', \single_button::BUTTON_PRIMARY); return $button->export_for_template($output); } return null; } } classes/output/view_action_bar.php000064400000012570152265517740013430 0ustar00. namespace mod_data\output; use data_portfolio_caller; use mod_data\manager; use moodle_url; use portfolio_add_button; use templatable; use renderable; /** * Renderable class for the action bar elements in the view pages in the database activity. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class view_action_bar implements templatable, renderable { /** @var int $id The database module id. */ private $id; /** @var \url_select $urlselect The URL selector object. */ private $urlselect; /** @var bool $hasentries Whether entries exist. */ private $hasentries; /** @var bool $mode The current view mode (list, view...). */ private $mode; /** * The class constructor. * * @param int $id The database module id. * @param \url_select $urlselect The URL selector object. * @param bool $hasentries Whether entries exist. * @param string $mode The current view mode (list, view...). */ public function __construct(int $id, \url_select $urlselect, bool $hasentries, string $mode) { $this->id = $id; $this->urlselect = $urlselect; $this->hasentries = $hasentries; $this->mode = $mode; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { global $PAGE, $DB, $CFG; $data = [ 'urlselect' => $this->urlselect->export_for_template($output), ]; $activity = $DB->get_record('data', ['id' => $this->id], '*', MUST_EXIST); $manager = manager::create_from_instance($activity); $actionsselect = null; // Import entries. if (has_capability('mod/data:manageentries', $manager->get_context())) { $actionsselect = new \action_menu(); $actionsselect->set_menu_trigger(get_string('actions'), 'btn btn-secondary'); $importentrieslink = new moodle_url('/mod/data/import.php', ['d' => $this->id, 'backto' => $PAGE->url->out(false)]); $actionsselect->add(new \action_menu_link( $importentrieslink, null, get_string('importentries', 'mod_data'), false )); } // Export entries. if (has_capability(DATA_CAP_EXPORT, $manager->get_context()) && $this->hasentries) { if (!$actionsselect) { $actionsselect = new \action_menu(); $actionsselect->set_menu_trigger(get_string('actions'), 'btn btn-secondary'); } $exportentrieslink = new moodle_url('/mod/data/export.php', ['d' => $this->id, 'backto' => $PAGE->url->out(false)]); $actionsselect->add(new \action_menu_link( $exportentrieslink, null, get_string('exportentries', 'mod_data'), false )); } // Export to portfolio. This is for exporting all records, not just the ones in the search. if ($this->mode == '' && !empty($CFG->enableportfolios) && $this->hasentries) { if ($manager->can_export_entries()) { // Add the portfolio export button. require_once($CFG->libdir . '/portfoliolib.php'); $cm = $manager->get_coursemodule(); $button = new portfolio_add_button(); $button->set_callback_options( 'data_portfolio_caller', ['id' => $cm->id], 'mod_data' ); if (data_portfolio_caller::has_files($activity)) { // No plain HTML. $button->set_formats([PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_LEAP2A]); } $exporturl = $button->to_html(PORTFOLIO_ADD_MOODLE_URL); if (!is_null($exporturl)) { if (!$actionsselect) { $actionsselect = new \action_menu(); $actionsselect->set_menu_trigger(get_string('actions'), 'btn btn-secondary'); } $actionsselect->add(new \action_menu_link( $exporturl, null, get_string('addtoportfolio', 'portfolio'), false )); } } } if ($actionsselect) { $data['actionsselect'] = $actionsselect->export_for_template($output); } return $data; } } classes/output/zero_state_action_bar.php000064400000005731152265517740014636 0ustar00. namespace mod_data\output; use mod_data\manager; use moodle_url; use templatable; use renderable; /** * Renderable class for the action bar elements in the zero state (no fields created) pages in the database activity. * * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class zero_state_action_bar implements templatable, renderable { /** @var manager The manager instance. */ protected $manager; /** * The class constructor. * * @param manager $manager The manager instance. */ public function __construct(manager $manager) { $this->manager = $manager; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { global $PAGE; $data = []; if ($this->manager->can_manage_templates()) { $cm = $this->manager->get_coursemodule(); $instance = $this->manager->get_instance(); $params = ['id' => $cm->id, 'backto' => $PAGE->url->out(false)]; $usepresetlink = new moodle_url('/mod/data/preset.php', $params); $usepresetbutton = new \single_button($usepresetlink, get_string('usestandard', 'mod_data'), 'get', \single_button::BUTTON_PRIMARY); $data['usepresetbutton'] = $usepresetbutton->export_for_template($output); $actionbar = new \mod_data\output\action_bar($instance->id, $PAGE->url); $createfieldbutton = $actionbar->get_create_fields(); $data['createfieldbutton'] = $createfieldbutton->export_for_template($output); $importpresetlink = new moodle_url('/mod/data/preset.php', $params); $importpresetbutton = new \single_button($importpresetlink, get_string('importapreset', 'mod_data'), 'get', \single_button::BUTTON_SECONDARY, [ 'data-action' => 'importpresets', 'data-dataid' => $cm->id, ]); $data['importpresetbutton'] = $importpresetbutton->export_for_template($output); } return $data; } } classes/output/presets.php000064400000021053152265517740011756 0ustar00. namespace mod_data\output; use action_menu; use action_menu_link_secondary; use mod_data\manager; use mod_data\preset; use moodle_url; use templatable; use renderable; use renderer_base; use stdClass; /** * Renderable class for the presets table in the database activity. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class presets implements templatable, renderable { /** @var array $presets The array containing the existing presets. */ private $presets; /** @var moodle_url $formactionurl The action url for the form. */ private $formactionurl; /** @var bool $manage Whether the manage preset options should be displayed. */ private $manage; /** @var int $id instance id */ private $id; /** @var int $cmid course module id */ private $cmid; /** * The class constructor. * * @param manager $manager The database manager * @param array $presets The array containing the existing presets * @param moodle_url $formactionurl The action url for the form * @param bool $manage Whether the manage preset options should be displayed */ public function __construct(manager $manager, array $presets, moodle_url $formactionurl, bool $manage = false) { $this->id = $manager->get_instance()->id; $this->cmid = $manager->get_coursemodule()->id; $this->presets = $presets; $this->formactionurl = $formactionurl; $this->manage = $manage; } /** * Export the data for the mustache template. * * @param renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(renderer_base $output): array { $presets = $this->get_presets($output); return [ 'id' => $this->cmid, 'formactionurl' => $this->formactionurl->out(), 'showmanage' => $this->manage, 'presets' => $presets, ]; } /** * Returns the presets list with the information required to display them. * * @param renderer_base $output The renderer to be used to render the action bar elements. * @return array Presets list. */ private function get_presets(renderer_base $output): array { $presets = []; foreach ($this->presets as $index => $preset) { $presetname = $preset->name; $userid = $preset instanceof preset ? $preset->get_userid() : $preset->userid; if (!empty($userid)) { // If the preset has the userid field, the full name of creator it will be added to the end of the name. $userfieldsapi = \core_user\fields::for_name(); $namefields = $userfieldsapi->get_sql('', false, '', '', false)->selects; $fields = 'id, ' . $namefields; $presetuser = \core_user::get_user($userid, $fields, MUST_EXIST); $username = fullname($presetuser, true); $presetname = "{$presetname} ({$username})"; } $actions = $this->get_preset_action_menu($output, $preset, $userid); $fullname = $preset->get_fullname(); $previewurl = new moodle_url( '/mod/data/preset.php', ['d' => $this->id, 'fullname' => $fullname, 'action' => 'preview'] ); $presets[] = [ 'id' => $this->id, 'cmid' => $this->cmid, 'name' => $preset->name, 'url' => $previewurl->out(), 'shortname' => $preset->shortname, 'fullname' => $presetname, 'description' => $preset->description, 'userid' => $userid, 'actions' => $actions, 'presetindex' => $index, ]; } return $presets; } /** * Return the preset action menu data. * * @param renderer_base $output The renderer to be used to render the action bar elements. * @param preset|stdClass $preset the preset object * @param int|null $userid the user id (null for plugin presets) * @return stdClass the resulting action menu */ private function get_preset_action_menu(renderer_base $output, $preset, ?int $userid): stdClass { $actions = new stdClass(); // If we cannot manage then return an empty menu. if (!$this->manage) { return $actions; } $actionmenu = new action_menu(); $actionmenu->set_kebab_trigger(); $actionmenu->set_action_label(get_string('actions')); $actionmenu->set_additional_classes('presets-actions'); $canmanage = $preset->can_manage(); $usepreseturl = new moodle_url('/mod/data/preset.php', [ 'action' => 'usepreset', 'cmid' => $this->cmid, ]); $this->add_action_menu($actionmenu, get_string('usepreset', 'mod_data'), $usepreseturl, [ 'data-action' => 'selectpreset', 'data-presetname' => $preset->get_fullname(), 'data-cmid' => $this->cmid, ] ); // Attention: the id here is the cm->id, not d->id. $previewpreseturl = new moodle_url('/mod/data/preset.php', [ 'fullname' => $preset->get_fullname(), 'action' => 'preview', 'id' => $this->cmid, ]); $this->add_action_menu($actionmenu, get_string('previewaction', 'mod_data'), $previewpreseturl, [ 'data-action' => 'preview', ] ); // Presets saved by users can be edited or removed. if (!$preset->isplugin) { // Edit. if ($canmanage) { $editactionurl = new moodle_url('/mod/data/preset.php', [ 'action' => 'edit', 'd' => $this->id, ]); $this->add_action_menu($actionmenu, get_string('edit'), $editactionurl, [ 'data-action' => 'editpreset', 'data-presetname' => $preset->name, 'data-presetdescription' => $preset->description, ]); } // Export. $exporturl = new moodle_url('/mod/data/preset.php', [ 'presetname' => $preset->name, 'action' => 'export', 'd' => $this->id, ]); $this->add_action_menu($actionmenu, get_string('export', 'mod_data'), $exporturl, [ 'data-action' => 'exportpreset', 'data-presetname' => $preset->name, 'data-presetdescription' => $preset->description, ]); // Delete. if ($canmanage) { $deleteactionurl = new moodle_url('/mod/data/preset.php', [ 'action' => 'delete', 'd' => $this->id, ]); $this->add_action_menu($actionmenu, get_string('delete'), $deleteactionurl, [ 'data-action' => 'deletepreset', 'data-presetname' => $preset->name, ]); } } $actions = $actionmenu->export_for_template($output); return $actions; } /** * Add action to the action menu * * @param action_menu $actionmenu * @param string $actionlabel * @param moodle_url $actionurl * @param array $otherattributes * @return void */ private function add_action_menu(action_menu &$actionmenu, string $actionlabel, moodle_url $actionurl, array $otherattributes) { $attributes = [ 'data-dataid' => $this->id, ]; $actionmenu->add(new action_menu_link_secondary( $actionurl, null, $actionlabel, array_merge($attributes, $otherattributes), )); } } classes/output/view_footer.php000064400000012722152265517740012624 0ustar00. namespace mod_data\output; use action_link; use core\output\sticky_footer; use html_writer; use mod_data\manager; use mod_data\template; use moodle_url; use renderer_base; /** * Renderable class for sticky footer in the view pages of the database activity. * * @package mod_data * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class view_footer extends sticky_footer { /** @var int $totalcount the total records count. */ private $totalcount; /** @var int $currentpage the current page */ private $currentpage; /** @var int $nowperpage the number of elements per page */ private $nowperpage; /** @var moodle_url $baseurl the page base url */ private $baseurl; /** @var template $parser the template name */ private $parser; /** @var manager $manager if the user can manage capabilities or not */ private $manager; /** * The class constructor. * * @param manager $manager the activity manager * @param int $totalcount the total records count * @param int $currentpage the current page * @param int $nowperpage the number of elements per page * @param moodle_url $baseurl the page base url * @param template $parser the current template name */ public function __construct( manager $manager, int $totalcount, int $currentpage, int $nowperpage, moodle_url $baseurl, template $parser ) { $this->manager = $manager; $this->totalcount = $totalcount; $this->currentpage = $currentpage; $this->nowperpage = $nowperpage; $this->baseurl = $baseurl; $this->parser = $parser; } /** * Export this data so it can be used as the context for a mustache template (core/inplace_editable). * * @param renderer_base $output typically, the renderer that's calling this function * @return array data context for a mustache template */ public function export_for_template(renderer_base $output) { $this->set_content( $this->get_footer_output($output) ); return parent::export_for_template($output); } /** * Generate the pre-rendered footer content. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return string the rendered content */ public function get_footer_output(renderer_base $output): string { $data = []; $cm = $this->manager->get_coursemodule(); $instance = $this->manager->get_instance(); $currentgroup = groups_get_activity_group($cm); $groupmode = groups_get_activity_groupmode($cm); $context = $this->manager->get_context(); $canmanageentries = has_capability('mod/data:manageentries', $context); $parser = $this->parser; // Sticky footer content. $data['pagination'] = $output->paging_bar( $this->totalcount, $this->currentpage, $this->nowperpage, $this->baseurl ); if ($parser->get_template_name() != 'singletemplate' && $parser->has_tag('delcheck') && $canmanageentries) { // Build the select/deselect all control. $selectallid = 'selectall-listview-entries'; $togglegroup = 'listview-entries'; $mastercheckbox = new \core\output\checkbox_toggleall($togglegroup, true, [ 'id' => $selectallid, 'name' => $selectallid, 'value' => 1, 'label' => get_string('selectall'), 'classes' => 'btn-secondary mx-1', ], true); $data['selectall'] = $output->render($mastercheckbox); $data['deleteselected'] = html_writer::empty_tag('input', [ 'class' => 'btn btn-secondary mx-1', 'type' => 'submit', 'value' => get_string('deleteselected'), 'disabled' => true, 'data-action' => 'toggle', 'data-togglegroup' => $togglegroup, 'data-toggle' => 'action', ]); } if (data_user_can_add_entry($instance, $currentgroup, $groupmode, $context)) { $addentrylink = new moodle_url( '/mod/data/edit.php', ['id' => $cm->id, 'backto' => $this->baseurl] ); $addentrybutton = new action_link( $addentrylink, get_string('add', 'mod_data'), null, ['class' => 'btn btn-primary mx-1', 'role' => 'button'] ); $data['addentrybutton'] = $addentrybutton->export_for_template($output); } return $output->render_from_template('mod_data/view_footer', $data); } } classes/output/fields_mappings_action_bar.php000064400000003505152265517740015620 0ustar00. namespace mod_data\output; use templatable; use renderable; /** * Renderable class for the action bar elements in the fields mapping page in the database activity. * * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fields_mappings_action_bar implements templatable, renderable { /** @var int $id The database module id. */ private $id; /** * The class constructor. * * @param int $id The database module id */ public function __construct(int $id) { $this->id = $id; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { return [ 'tertiarytitle' => get_string('fieldmappings', 'mod_data'), 'hasback' => true, 'backtitle' => get_string('back'), 'backurl' => new \moodle_url('/mod/data/preset.php', ['d' => $this->id]), ]; } } classes/output/template_editor_tools.php000064400000016460152265517740014700 0ustar00. namespace mod_data\output; use templatable; use renderable; use core_tag_tag; use mod_data\manager; /** * Renderable class for template editor tools. * * @package mod_data * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_editor_tools implements templatable, renderable { /** @var manager manager instance. */ private $manager; /** @var string the template name. */ private $templatename; /** * The class constructor. * * @param manager $manager the activity instance manager * @param string $templatename the template to edit */ public function __construct(manager $manager, string $templatename) { $this->manager = $manager; $this->templatename = $templatename; } /** * Export the data for the mustache template. * * @param \renderer_base $output renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { $tools = [ $this->get_field_tags($this->templatename), $this->get_field_info_tags($this->templatename), $this->get_action_tags($this->templatename), $this->get_other_tags($this->templatename), ]; $tools = array_filter($tools, static function ($value) { return !empty($value['tags']); }); return [ 'toolshelp' => $output->help_icon('availabletags', 'data'), 'hastools' => !empty($tools), 'tools' => array_values($tools), ]; } /** * Return the field template tags. * * @param string $templatename the template name * @return array|null array of tags. */ protected function get_field_tags(string $templatename): array { $name = get_string('fields', 'data'); if ($templatename == 'csstemplate' || $templatename == 'jstemplate') { return $this->get_optgroup_data($name, []); } $taglist = []; $fields = $this->manager->get_fields(); foreach ($fields as $field) { if ($field->type === 'unknown') { continue; } $fieldname = $field->get_name(); $taglist["[[$fieldname]]"] = $fieldname; } $taglist['##otherfields##'] = get_string('otherfields', 'data'); return $this->get_optgroup_data($name, $taglist); } /** * Return the field information template tags. * * @param string $templatename the template name * @return array|null array of tags. */ protected function get_field_info_tags(string $templatename): array { $name = get_string('fieldsinformationtags', 'data'); $taglist = []; $fields = $this->manager->get_fields(); foreach ($fields as $field) { if ($field->type === 'unknown') { continue; } $fieldname = $field->get_name(); if ($templatename == 'addtemplate') { $taglist["[[$fieldname#id]]"] = get_string('fieldtagid', 'mod_data', $fieldname); } $taglist["[[$fieldname#name]]"] = get_string('fieldtagname', 'mod_data', $fieldname); $taglist["[[$fieldname#description]]"] = get_string('fieldtagdescription', 'mod_data', $fieldname); } return $this->get_optgroup_data($name, $taglist); } /** * Return the field action tags. * * @param string $templatename the template name * @return array|null array of tags. */ protected function get_action_tags(string $templatename): array { $name = get_string('actions'); if ($templatename == 'addtemplate' || $templatename == 'asearchtemplate') { return $this->get_optgroup_data($name, []); } $taglist = [ '##actionsmenu##' => get_string('actionsmenu', 'data'), '##edit##' => get_string('edit', 'data'), '##delete##' => get_string('delete', 'data'), '##approve##' => get_string('approve', 'data'), '##disapprove##' => get_string('disapprove', 'data'), ]; if ($templatename != 'rsstemplate') { $taglist['##export##'] = get_string('export', 'data'); } if ($templatename != 'singletemplate') { $taglist['##more##'] = get_string('more', 'data'); $taglist['##moreurl##'] = get_string('moreurl', 'data'); $taglist['##delcheck##'] = get_string('delcheck', 'data'); } return $this->get_optgroup_data($name, $taglist); } /** * Return the available other tags * * @param string $templatename the template name * @return array associative array of tags => tag name */ protected function get_other_tags(string $templatename): array { $name = get_string('other', 'data'); $taglist = []; if ($templatename == 'asearchtemplate') { $taglist['##firstname##'] = get_string('firstname'); $taglist['##lastname##'] = get_string('lastname'); return $this->get_optgroup_data($name, $taglist); } if (core_tag_tag::is_enabled('mod_data', 'data_records')) { $taglist['##tags##'] = get_string('tags'); } if ($templatename == 'addtemplate') { return $this->get_optgroup_data($name, $taglist); } $taglist['##timeadded##'] = get_string('timeadded', 'data'); $taglist['##timemodified##'] = get_string('timemodified', 'data'); $taglist['##user##'] = get_string('user'); $taglist['##userpicture##'] = get_string('userpic'); $taglist['##approvalstatus##'] = get_string('approvalstatus', 'data'); $taglist['##id##'] = get_string('id', 'data'); if ($templatename == 'singletemplate') { return $this->get_optgroup_data($name, $taglist); } $taglist['##comments##'] = get_string('comments', 'data'); return $this->get_optgroup_data($name, $taglist); } /** * Generate a valid optgroup data. * * @param string $name the optgroup name * @param array $taglist the indexed array of taglists ($tag => $tagname) * @return array of optgroup data */ protected function get_optgroup_data(string $name, array $taglist): array { $tags = []; foreach ($taglist as $tag => $tagname) { $tags[] = [ 'tag' => "$tag", 'tagname' => $tagname . ' - ' . $tag, ]; } return [ 'name' => $name, 'tags' => $tags, ]; } } classes/output/template_editor.php000064400000013762152265517740013462 0ustar00. namespace mod_data\output; use templatable; use renderable; use mod_data\manager; use moodle_url; use texteditor; /** * Renderable class for template editor. * * @package mod_data * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_editor implements templatable, renderable { /** @var manager manager instance. */ private $manager; /** @var string the template name. */ private $templatename; /** * The class constructor. * * @param manager $manager the activity instance manager * @param string $templatename the template to edit */ public function __construct(manager $manager, string $templatename) { $this->manager = $manager; $this->templatename = $templatename; } /** * Export the data for the mustache template. * * @param \renderer_base $output renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { $instance = $this->manager->get_instance(); $cm = $this->manager->get_coursemodule(); $data = [ 'title' => get_string('header' . $this->templatename, 'data'), 'sesskey' => sesskey(), 'disableeditor' => true, 'url' => new moodle_url('/mod/data/templates.php', ['id' => $cm->id, 'mode' => $this->templatename]), ]; // Determine whether to use HTML editors. $usehtmleditor = false; $disableeditor = false; if (($this->templatename !== 'csstemplate') && ($this->templatename !== 'jstemplate')) { $usehtmleditor = data_get_config($instance, "editor_{$this->templatename}", true); $disableeditor = true; } $data['usehtmleditor'] = $usehtmleditor; // Some templates, like CSS, cannot enable the wysiwyg editor. $data['disableeditor'] = $disableeditor; $tools = new template_editor_tools($this->manager, $this->templatename); $data['toolbar'] = $tools->export_for_template($output); $data['editors'] = $this->get_editors_data($usehtmleditor); return $data; } /** * Get the editors data. * * @param bool $usehtmleditor if the user wants wysiwyg editor or not * @return array editors data */ private function get_editors_data(bool $usehtmleditor): array { global $PAGE; $result = []; $manager = $this->manager; $instance = $manager->get_instance(); // Setup editor. editors_head_setup(); $PAGE->requires->js_call_amd( 'mod_data/templateseditor', 'init', ['d' => $instance->id, 'mode' => $this->templatename] ); $format = FORMAT_PLAIN; if ($usehtmleditor) { $format = FORMAT_HTML; } $editor = editors_get_preferred_editor($format); // Add editors. if ($this->templatename === 'listtemplate') { $template = $manager->get_template('listtemplateheader'); $result[] = $this->generate_editor_data( $editor, 'header', 'listtemplateheader', $template->get_template_content() ); $maineditorname = 'multientry'; } else { $maineditorname = $this->templatename; } $template = $manager->get_template($this->templatename); $result[] = $this->generate_editor_data( $editor, $maineditorname, $this->templatename, $template->get_template_content() ); if ($this->templatename === 'listtemplate') { $template = $manager->get_template('listtemplatefooter'); $result[] = $this->generate_editor_data( $editor, 'footer', 'listtemplatefooter', $template->get_template_content() ); } if ($this->templatename === 'rsstemplate') { $template = $manager->get_template('rsstitletemplate'); $result[] = $this->generate_editor_data( $editor, 'rsstitletemplate', 'rsstitletemplate', $template->get_template_content() ); } return $result; } /** * Generate a single editor data. * * @param texteditor $editor the editor object * @param string $name the editor name * @param string $fieldname the field name * @param string|null $value the current value * @return array the editor data */ private function generate_editor_data( texteditor $editor, string $name, string $fieldname, ?string $value ): array { $options = [ 'trusttext' => false, 'forcehttps' => false, 'subdirs' => false, 'maxfiles' => 0, 'maxbytes' => 0, 'changeformat' => 0, 'noclean' => false, ]; $result = [ 'name' => get_string($name, 'data'), 'fieldname' => $fieldname, 'value' => $value, ]; $editor->set_text($value); $editor->use_editor($fieldname, $options); return $result; } } classes/output/presets_action_bar.php000064400000004044152265517740014140 0ustar00. namespace mod_data\output; use templatable; use renderable; /** * Renderable class for the action bar elements in the presets page in the database activity. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class presets_action_bar implements templatable, renderable { /** @var int $id The database module id. */ private $cmid; /** @var \action_menu $actionsselect The presets actions selector object. */ private $actionsselect; /** * The class constructor. * * @param int $cmid The database module id * @param \action_menu|null $actionsselect The presets actions selector object. */ public function __construct(int $cmid, ?\action_menu $actionsselect) { $this->cmid = $cmid; $this->actionsselect = $actionsselect; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { $data = [ 'id' => $this->cmid, ]; if ($this->actionsselect) { $data['actionsselect'] = $this->actionsselect->export_for_template($output); } return $data; } } classes/output/defaulttemplate.php000064400000006507152265517740013460 0ustar00. namespace mod_data\output; use core_tag_tag; use mod_data\manager; use templatable; use renderable; /** * Renderable class for the default templates in the database activity. * * @package mod_data * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class defaulttemplate implements templatable, renderable { /** @var array $fields The array containing the existing fields. */ private $fields; /** @var string $templatename The template name (addtemplate, listtemplate...). */ private $templatename; /** @var bool $isform Whether a form should be displayed instead of data. */ private $isform; /** * The class constructor. * * @param array $fields The array containing the existing fields. * @param string $templatename The template name (addtemplate, listtemplate...). * @param bool $isform Whether a form should be displayed instead of data. */ public function __construct(array $fields, string $templatename, bool $isform) { $this->fields = $fields; $this->templatename = $templatename; $this->isform = $isform; } /** * Obtains the mustache template name for this database template. * * @return string the file mustache path for this template. */ public function get_templatename(): string { return 'mod_data/defaulttemplate_' . $this->templatename; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array The data to display. */ public function export_for_template(\renderer_base $output): array { $result = []; $exportedfields = []; foreach ($this->fields as $field) { $fieldname = $field->field->name; if ($this->isform) { $fieldcontent = $field->display_add_field(); } else { $fieldcontent = '[[' . $fieldname . ']]'; } $exportedfields[] = [ 'fieldname' => $fieldname, 'fieldcontent' => $fieldcontent, ]; } if (!empty($exportedfields)) { $result['fields'] = $exportedfields; } if (core_tag_tag::is_enabled(manager::PLUGINNAME, 'data_records')) { // Add tags information only if they are enabled. if ($this->isform) { $tags = data_generate_tag_form(); } else { $tags = '##tags##'; } $result['tags'] = $tags; } return $result; } } classes/output/empty_database_action_bar.php000064400000004501152265517740015433 0ustar00. namespace mod_data\output; use mod_data\manager; use moodle_url; use templatable; use renderable; /** * Renderable class for the action bar elements for an empty database activity. * * @package mod_data * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class empty_database_action_bar implements templatable, renderable { /** @var manager The manager instance. */ protected $manager; /** * The class constructor. * * @param int $id The database module id. */ public function __construct(manager $manager) { $this->manager = $manager; } /** * Export the data for the mustache template. * * @param \renderer_base $output The renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { global $PAGE; $instance = $this->manager->get_instance(); $addentrybutton = new add_entries_action($instance->id); $data = ['addentrybutton' => $addentrybutton->export_for_template($output)]; if (has_capability('mod/data:manageentries', $PAGE->context)) { $params = ['d' => $instance->id, 'backto' => $PAGE->url->out(false)]; $importentrieslink = new moodle_url('/mod/data/import.php', $params); $importentriesbutton = new \single_button($importentrieslink, get_string('importentries', 'mod_data'), 'get'); $data['importentriesbutton'] = $importentriesbutton->export_for_template($output); } return $data; } } classes/output/preset_preview.php000064400000010164152265517740013335 0ustar00. namespace mod_data\output; use templatable; use renderable; use mod_data\manager; use mod_data\preset; use mod_data\template; use moodle_page; use moodle_url; /** * Preset preview output class. * * @package mod_data * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class preset_preview implements templatable, renderable { /** @var manager manager instance. */ private $manager; /** @var preset the preset. */ private $preset; /** @var string the template to preview. */ private $templatename; /** * The class constructor. * * @param manager $manager the activity instance manager * @param preset $preset the preset * @param string $templatename the templatename */ public function __construct(manager $manager, preset $preset, string $templatename) { $this->manager = $manager; $this->preset = $preset; $this->templatename = $templatename; } /** * Add the preset CSS and JS to the page. * * @param moodle_page $page the current page instance */ public function prepare_page(moodle_page $page) { $instance = $this->manager->get_instance(); $preset = $this->preset; // Add CSS and JS. $csscontent = $preset->get_template_content('csstemplate'); if (!empty($csscontent)) { $url = new moodle_url('/mod/data/css.php', ['d' => $instance->id, 'preset' => $preset->get_fullname()]); $page->requires->css($url); } $jscontent = $preset->get_template_content('jstemplate'); if (!empty($jscontent)) { $url = new moodle_url('/mod/data/js.php', ['d' => $instance->id, 'preset' => $preset->get_fullname()]); $page->requires->js($url); } } /** * Export the data for the mustache template. * * @param \renderer_base $output renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { $coursemodule = $this->manager->get_coursemodule(); $preset = $this->preset; // Get fields for preview. $count = ($this->templatename == 'listtemplate') ? 2 : 1; $fields = $preset->get_fields(true); $entries = $preset->get_sample_entries($count); $templatecontent = $preset->get_template_content($this->templatename); $useurl = new moodle_url('/mod/data/field.php'); // Generate preview content. $options = ['templatename' => $this->templatename]; if ($this->templatename == 'listtemplate') { $options['showmore'] = true; } $parser = new template($this->manager, $templatecontent, $options, $fields); $content = $parser->parse_entries($entries); if ($this->templatename == 'listtemplate') { $listtemplateheader = $preset->get_template_content('listtemplateheader'); $listtemplatefooter = $preset->get_template_content('listtemplatefooter'); $content = $listtemplateheader . $content . $listtemplatefooter; } return [ 'cmid' => $coursemodule->id, 'description' => $preset->description ?? '', 'preview' => $content, 'formactionurl' => $useurl->out(), 'userid' => $preset->get_userid() ?? 0, 'shortname' => $preset->shortname, ]; } } classes/output/action_bar.php000064400000032532152265517740012376 0ustar00. namespace mod_data\output; use mod_data\manager; use mod_data\preset; use moodle_url; use url_select; /** * Class responsible for generating the action bar elements in the database module pages. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_bar { /** @var int $id The database module id. */ private $id; /** @var int $cmid The database course module id. */ private $cmid; /** @var moodle_url $currenturl The URL of the current page. */ private $currenturl; /** * The class constructor. * * @param int $id The database module id. * @param moodle_url $pageurl The URL of the current page. */ public function __construct(int $id, moodle_url $pageurl) { $this->id = $id; [$course, $cm] = get_course_and_cm_from_instance($this->id, 'data'); $this->cmid = $cm->id; $this->currenturl = $pageurl; } /** * Generate the output for the action bar in the field page. * * @param bool $hasfieldselect Whether the field selector element should be rendered. * @param null $unused1 This parameter has been deprecated since 4.1 and should not be used anymore. * @param null $unused2 This parameter has been deprecated since 4.1 and should not be used anymore. * @return string The HTML code for the action bar. */ public function get_fields_action_bar( bool $hasfieldselect = false, ?bool $unused1 = null, ?bool $unused2 = null ): string { global $PAGE; if ($unused1 !== null || $unused2 !== null) { debugging('Deprecated argument passed to get_fields_action_bar method', DEBUG_DEVELOPER); } $renderer = $PAGE->get_renderer('mod_data'); $fieldsactionbar = new fields_action_bar($this->id); return $renderer->render_fields_action_bar($fieldsactionbar); } /** * Generate the output for the action bar in the field mappings page. * * @return string The HTML code for the action bar. */ public function get_fields_mapping_action_bar(): string { global $PAGE; $renderer = $PAGE->get_renderer('mod_data'); $fieldsactionbar = new fields_mappings_action_bar($this->id); $data = $fieldsactionbar->export_for_template($renderer); return $renderer->render_from_template('mod_data/fields_action_bar', $data); } /** * Generate the output for the create a new field action menu. * * @param bool $isprimarybutton is the action trigger a primary or secondary button? * @return \action_menu Action menu to create a new field */ public function get_create_fields(bool $isprimarybutton = false): \action_menu { // Get the list of possible fields (plugins). $plugins = \core_component::get_plugin_list('datafield'); $menufield = []; foreach ($plugins as $plugin => $fulldir) { $menufield[$plugin] = get_string('pluginname', "datafield_{$plugin}"); } asort($menufield); $fieldselect = new \action_menu(); $triggerclasses = ['btn']; $triggerclasses[] = $isprimarybutton ? 'btn-primary' : 'btn-secondary'; $fieldselect->set_menu_trigger(get_string('newfield', 'mod_data'), join(' ', $triggerclasses)); $fieldselectparams = ['id' => $this->cmid, 'mode' => 'new']; foreach ($menufield as $fieldtype => $fieldname) { $fieldselectparams['newtype'] = $fieldtype; $fieldselect->add(new \action_menu_link( new moodle_url('/mod/data/field.php', $fieldselectparams), new \image_icon('icon', $fieldtype, 'datafield_' . $fieldtype), $fieldname, false )); } $fieldselect->set_additional_classes('singlebutton'); return $fieldselect; } /** * Generate the output for the action selector in the view page. * * @param bool $hasentries Whether entries exist. * @param string $mode The current view mode (list, view...). * @return string The HTML code for the action selector. */ public function get_view_action_bar(bool $hasentries, string $mode): string { global $PAGE; $viewlistlink = new moodle_url('/mod/data/view.php', ['d' => $this->id]); $viewsinglelink = new moodle_url('/mod/data/view.php', ['d' => $this->id, 'mode' => 'single']); $menu = [ $viewlistlink->out(false) => get_string('listview', 'mod_data'), $viewsinglelink->out(false) => get_string('singleview', 'mod_data'), ]; $activeurl = $this->currenturl; if ($this->currenturl->get_param('rid') || $this->currenturl->get_param('mode') == 'single') { $activeurl = $viewsinglelink; } $urlselect = new url_select($menu, $activeurl->out(false), null, 'viewactionselect'); $urlselect->set_label(get_string('viewnavigation', 'mod_data'), ['class' => 'visually-hidden']); $renderer = $PAGE->get_renderer('mod_data'); $viewactionbar = new view_action_bar($this->id, $urlselect, $hasentries, $mode); return $renderer->render_view_action_bar($viewactionbar); } /** * Generate the output for the action selector in the templates page. * * @return string The HTML code for the action selector. */ public function get_templates_action_bar(): string { global $PAGE; $listtemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'listtemplate']); $singletemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'singletemplate']); $advancedsearchtemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'asearchtemplate']); $addtemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'addtemplate']); $rsstemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'rsstemplate']); $csstemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'csstemplate']); $jstemplatelink = new moodle_url('/mod/data/templates.php', ['d' => $this->id, 'mode' => 'jstemplate']); $menu = [ $addtemplatelink->out(false) => get_string('addtemplate', 'mod_data'), $singletemplatelink->out(false) => get_string('singletemplate', 'mod_data'), $listtemplatelink->out(false) => get_string('listtemplate', 'mod_data'), $advancedsearchtemplatelink->out(false) => get_string('asearchtemplate', 'mod_data'), $csstemplatelink->out(false) => get_string('csstemplate', 'mod_data'), $jstemplatelink->out(false) => get_string('jstemplate', 'mod_data'), $rsstemplatelink->out(false) => get_string('rsstemplate', 'mod_data'), ]; $selectmenu = new \core\output\select_menu('presetsactions', $menu, $this->currenturl->out(false)); $selectmenu->set_label(get_string('templatesnavigation', 'mod_data'), ['class' => 'visually-hidden']); $renderer = $PAGE->get_renderer('mod_data'); $presetsactions = $this->get_presets_actions_select(false); // Reset single template action. $resetcurrrent = new moodle_url($this->currenturl); $resetcurrrent->param('action', 'resettemplate'); $presetsactions->add(new \action_menu_link( $resetcurrrent, null, get_string('resettemplate', 'mod_data'), false, ['data-action' => 'resettemplate', 'data-dataid' => $this->id] )); // Reset all templates action. $resetallurl = new moodle_url($this->currenturl); $resetallurl->params([ 'action' => 'resetalltemplates', 'sesskey' => sesskey(), ]); $presetsactions->add(new \action_menu_link( $resetallurl, null, get_string('resetalltemplates', 'mod_data'), false, ['data-action' => 'resetalltemplates', 'data-dataid' => $this->id] )); $templatesactionbar = new templates_action_bar($this->id, $selectmenu, null, null, $presetsactions); return $renderer->render_templates_action_bar($templatesactionbar); } /** * Generate the output for the action selector in the presets page. * * @return string The HTML code for the action selector. */ public function get_presets_action_bar(): string { global $PAGE; $renderer = $PAGE->get_renderer('mod_data'); $presetsactionbar = new presets_action_bar($this->cmid, $this->get_presets_actions_select(true)); return $renderer->render_presets_action_bar($presetsactionbar); } /** * Generate the output for the action selector in the presets preview page. * * @param manager $manager the manager instance * @param string $fullname the preset fullname * @param string $current the current template name * @return string The HTML code for the action selector */ public function get_presets_preview_action_bar(manager $manager, string $fullname, string $current): string { global $PAGE; $renderer = $PAGE->get_renderer(manager::PLUGINNAME); $cm = $manager->get_coursemodule(); $menu = []; $selected = null; foreach (['listtemplate', 'singletemplate'] as $templatename) { $link = new moodle_url('/mod/data/preset.php', [ 'd' => $this->id, 'template' => $templatename, 'fullname' => $fullname, 'action' => 'preview', ]); $menu[$link->out(false)] = get_string($templatename, manager::PLUGINNAME); if (!$selected || $templatename == $current) { $selected = $link->out(false); } } $urlselect = new url_select($menu, $selected, null); $urlselect->set_label(get_string('templatesnavigation', manager::PLUGINNAME), ['class' => 'visually-hidden']); $data = [ 'title' => get_string('preview', manager::PLUGINNAME, preset::get_name_from_plugin($fullname)), 'hasback' => true, 'backtitle' => get_string('back'), 'backurl' => new moodle_url('/mod/data/preset.php', ['id' => $cm->id]), 'extraurlselect' => $urlselect->export_for_template($renderer), ]; return $renderer->render_from_template('mod_data/action_bar', $data); } /** * Helper method to get the selector for the presets action. * * @param bool $hasimport Whether the Import buttons must be included or not. * @return \action_menu|null The selector object used to display the presets actions. Null when the import button is not * displayed and the database hasn't any fields. */ protected function get_presets_actions_select(bool $hasimport = false): ?\action_menu { global $DB; $hasfields = $DB->record_exists('data_fields', ['dataid' => $this->id]); // Early return if the database has no fields and the import action won't be displayed. if (!$hasfields && !$hasimport) { return null; } $actionsselect = new \action_menu(); $actionsselect->set_menu_trigger(get_string('actions'), 'btn btn-secondary'); if ($hasimport) { // Import. $actionsselectparams = ['id' => $this->cmid]; $actionsselect->add(new \action_menu_link( new moodle_url('/mod/data/preset.php', $actionsselectparams), null, get_string('importpreset', 'mod_data'), false, ['data-action' => 'importpresets', 'data-dataid' => $this->cmid] )); } // If the database has no fields, export and save as preset options shouldn't be displayed. if ($hasfields) { // Export. $actionsselectparams = ['id' => $this->cmid, 'action' => 'export']; $actionsselect->add(new \action_menu_link( new moodle_url('/mod/data/preset.php', $actionsselectparams), null, get_string('exportpreset', 'mod_data'), false )); // Save as preset. $actionsselect->add(new \action_menu_link( new moodle_url('/mod/data/preset.php', $actionsselectparams), null, get_string('saveaspreset', 'mod_data'), false, ['data-action' => 'saveaspreset', 'data-dataid' => $this->id] )); } return $actionsselect; } } classes/output/templates_action_bar.php000064400000004762152265517740014460 0ustar00. namespace mod_data\output; use core\output\select_menu; use templatable; use renderable; /** * Renderable class for the action bar elements in the template pages in the database activity. * * @package mod_data * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class templates_action_bar implements templatable, renderable { /** @var int $id The database module id. */ private $id; /** @var select_menu $selectmenu The URL selector object. */ private $selectmenu; /** @var \action_menu $actionsselect The presets actions selector object. */ private $actionsselect; /** * The class constructor. * * @param int $id The database module id. * @param select_menu $selectmenu The URL selector object. * @param null $unused1 This parameter has been deprecated since 4.1 and should not be used anymore. * @param null $unused2 This parameter has been deprecated since 4.1 and should not be used anymore. * @param \action_menu $actionsselect The presets actions selector object. */ public function __construct(int $id, select_menu $selectmenu, $unused1, $unused2, \action_menu $actionsselect) { $this->id = $id; $this->selectmenu = $selectmenu; $this->actionsselect = $actionsselect; } /** * Export the data for the mustache template. * * @param \renderer_base $output renderer to be used to render the action bar elements. * @return array */ public function export_for_template(\renderer_base $output): array { return [ 'd' => $this->id, 'selectmenu' => $this->selectmenu->export_for_template($output), 'actionsselect' => $this->actionsselect->export_for_template($output), ]; } } classes/preset.php000064400000071056152265517740010243 0ustar00. namespace mod_data; use core_component; use invalid_parameter_exception; use data_field_base; use moodle_exception; use SimpleXMLElement; use stdClass; use stored_file; /** * Class preset for database activity. * * @package mod_data * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class preset { /** @var manager manager instance. */ private $manager; /** @var bool whether the preset is a plugin or has been saved by the user. */ public $isplugin; /** @var string The preset name. */ public $name; /** @var string The preset shortname. For datapreset plugins that is the folder; for saved presets, that's the preset name. */ public $shortname; /** @var string The preset description. */ public $description; /** @var stored_file For saved presets that's the file object for the root folder. It's null for plugins or for presets that * haven't been saved yet. */ public $storedfile; /** @var array|null the field sample instances. */ private $fields = null; /** @var stdClass|null the preset.xml parsed information. */ protected $xmlinfo = null; /** * Class constructor. * * @param manager|null $manager the current instance manager * @param bool $isplugin whether the preset is a plugin or has been saved by the user * @param string $name the preset name * @param string $shortname the preset shortname * @param string|null $description the preset description * @param stored_file|null $storedfile for saved presets, that's the file for the root folder * @throws invalid_parameter_exception */ protected function __construct( ?manager $manager, bool $isplugin, string $name, string $shortname, ?string $description = '', ?stored_file $storedfile = null ) { if (!$isplugin && is_null($manager)) { throw new invalid_parameter_exception('The $manager parameter can only be null for plugin presets.'); } $this->manager = $manager; $this->isplugin = $isplugin; $this->name = $name; $this->shortname = $shortname; $this->description = $description; $this->storedfile = $storedfile; } /** * Create a preset instance from a stored file. * * @param manager $manager the current instance manager * @param stored_file $file the preset root folder * @return preset|null If the given file doesn't belong to the expected component/filearea/context, null will be returned */ public static function create_from_storedfile(manager $manager, stored_file $file): ?self { if ($file->get_component() != DATA_PRESET_COMPONENT || $file->get_filearea() != DATA_PRESET_FILEAREA || $file->get_contextid() != DATA_PRESET_CONTEXT) { return null; } $isplugin = false; $name = trim($file->get_filepath(), '/'); $description = static::get_attribute_value($file->get_filepath(), 'description'); return new self($manager, $isplugin, $name, $name, $description, $file); } /** * Create a preset instance from a plugin. * * @param manager|null $manager the current instance manager * @param string $pluginname the datapreset plugin name * @return preset|null The plugin preset or null if there is no datapreset plugin with the given name. */ public static function create_from_plugin(?manager $manager, string $pluginname): ?self { $found = false; $plugins = array_keys(core_component::get_plugin_list('datapreset')); foreach ($plugins as $plugin) { if ($plugin == $pluginname) { $found = true; break; } } if (!$found) { // If there is no datapreset plugin with this name, return null. return null; } $name = static::get_name_from_plugin($pluginname); $description = static::get_description_from_plugin($pluginname); return new self($manager, true, $name, $pluginname, $description); } /** * Create a preset instance from a data_record entry, a preset name and a description. * * @param manager $manager the current instance manager * @param string $presetname the preset name * @param string|null $description the preset description * @return preset */ public static function create_from_instance(manager $manager, string $presetname, ?string $description = ''): self { $isplugin = false; $path = '/' . $presetname . '/'; $file = static::get_file($path, '.'); if (!is_null($file)) { // If the file is not empty, create the instance based on the storedfile. return self::create_from_storedfile($manager, $file); } return new self($manager, $isplugin, $presetname, $presetname, $description, $file); } /** * Create a preset instance from the preset fullname. * * The preset fullname is a concatenation of userid and pluginname|presetname used by most * preset pages. Plugins uses userid zero while preset instances has the owner as identifier. * * This method will throw an exception if the preset instance has a different userid thant the one * from the $fullname. However, it won't check the current user capabilities. * * @param manager $manager the current instance manager * @param string $fullname the preset full name * @return preset */ public static function create_from_fullname(manager $manager, string $fullname): self { $parts = explode('/', $fullname, 2); $userid = empty($parts[0]) ? 0 : (int)$parts[0]; $shortname = empty($parts[1]) ? '' : $parts[1]; // Shortnames with userid zero are plugins. if ($userid == 0) { return static::create_from_plugin($manager, $shortname); } $path = '/' . $shortname . '/'; $file = static::get_file($path, '.'); $result = static::create_from_storedfile($manager, $file); if ($result->get_userid() != $userid) { throw new moodle_exception('invalidpreset', manager::PLUGINNAME); } return $result; } /** * Save this preset. * * @return bool true if the preset has been saved; false otherwise. */ public function save(): bool { global $USER; if ($this->isplugin) { // Plugin presets can't be saved. return false; } if (!is_null($this->storedfile)) { // It's a pre-existing preset, so it needs to be updated. return $this->update_user_preset(); } // The preset hasn't been saved before. $fs = get_file_storage(); // Create and save the preset.xml file, with the description, settings, fields... $filerecord = static::get_filerecord('preset.xml', $this->get_path(), $USER->id); $fs->create_file_from_string($filerecord, $this->generate_preset_xml()); // Create and save the template files. $instance = $this->manager->get_instance(); foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { $filerecord->filename = $templatefile; $fs->create_file_from_string($filerecord, $instance->{$templatename}); } // Update the storedfile with the one we've just saved. $this->storedfile = static::get_file($this->get_path(), '.'); return true; } /** * Update the stored user preset. * This method is used internally by the save method. * * @return bool true if the preset has been saved; false otherwise. */ private function update_user_preset(): bool { global $USER; $result = false; $shouldbesaved = false; // Update description (if required). $oldpresetfile = static::get_file($this->storedfile->get_filepath(), 'preset.xml'); $presetxml = $oldpresetfile->get_content(); $parsedxml = simplexml_load_string($presetxml); if (property_exists($parsedxml, 'description')) { if ($parsedxml->description != $this->description) { $parsedxml->description = $this->description; $shouldbesaved = true; } } else { if (!is_null($this->description)) { $parsedxml->addChild('description', $this->description); $shouldbesaved = true; } } // Update name (if required). $oldname = trim($this->storedfile->get_filepath(), '/'); $newpath = '/' . $this->name . '/'; if ($oldname != $this->name) { // Preset name has changed, so files need to be updated too because the preset name is saved in the filepath. foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { $oldfile = static::get_file($this->storedfile->get_filepath(), $templatefile); $oldfile->rename($newpath, $templatefile); } // The root folder should also be renamed. $this->storedfile->rename($newpath, $this->storedfile->get_filename()); $shouldbesaved = true; } // Only save the new preset.xml if there are changes. if ($shouldbesaved) { // Before saving preset.xml, the old preset.xml file should be removed. $oldpresetfile->delete(); // Create the new file with the new content. $filerecord = static::get_filerecord('preset.xml', $newpath, $USER->id); $presetcontent = $parsedxml->asXML(); $fs = get_file_storage(); $fs->create_file_from_string($filerecord, $presetcontent); $result = true; } return $result; } /** * Export this preset. * * @return string the full path to the exported preset file. */ public function export(): string { if ($this->isplugin) { // For now, only saved presets can be exported. return ''; } $presetname = clean_filename($this->name) . '-preset-' . gmdate("Ymd_Hi"); $exportsubdir = "mod_data/presetexport/$presetname"; $exportdir = make_temp_directory($exportsubdir); // Generate and write the preset.xml file. $presetxmldata = static::generate_preset_xml(); $presetxmlfile = fopen($exportdir . '/preset.xml', 'w'); fwrite($presetxmlfile, $presetxmldata); fclose($presetxmlfile); // Write the template files. $instance = $this->manager->get_instance(); foreach (manager::TEMPLATES_LIST as $templatename => $templatefilename) { $templatefile = fopen("$exportdir/$templatefilename", 'w'); fwrite($templatefile, $instance->{$templatename} ?? ''); fclose($templatefile); } // Check if all files have been generated. if (! static::is_directory_a_preset($exportdir)) { throw new \moodle_exception('generateerror', 'data'); } $presetfilenames = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml']); $filelist = []; foreach ($presetfilenames as $filename) { $filelist[$filename] = $exportdir . '/' . $filename; } $exportfile = $exportdir.'.zip'; file_exists($exportfile) && unlink($exportfile); $fp = get_file_packer('application/zip'); $fp->archive_to_pathname($filelist, $exportfile); foreach ($filelist as $file) { unlink($file); } rmdir($exportdir); return $exportfile; } /** * Return the preset author. * * Preset plugins do not have any user id. * * @return int|null the userid or null if it is a plugin */ public function get_userid(): ?int { if (!empty($this->storedfile)) { return $this->storedfile->get_userid(); } return null; } /** * Return the preset fullname. * * Preset fullname is used mostly for urls. * * @return string the preset fullname */ public function get_fullname(): string { $userid = $this->get_userid() ?? '0'; return "{$userid}/{$this->shortname}"; } /** * Returns the preset path. * * @return string|null the preset path is null for plugins and /presetname/ for saved presets. */ public function get_path(): ?string { if ($this->isplugin) { return null; } if (!empty($this->storedfile)) { return $this->storedfile->get_filepath(); } return '/' . $this->name . '/'; } /** * Return the field instances of the preset. * * @param bool $forpreview if the fields are only for preview * @return data_field_base[] and array with field objects */ public function get_fields(bool $forpreview = false): array { if ($this->fields !== null) { return $this->fields; } // Parse the preset.xml file. $this->load_preset_xml(); if (empty($this->xmlinfo) || empty($this->xmlinfo->field)) { $this->fields = []; return $this->fields; } // Generate field instances. $result = []; foreach ($this->xmlinfo->field as $fieldinfo) { $result[(string) $fieldinfo->name] = $this->get_field_instance($fieldinfo, count($result), $forpreview); } $this->fields = $result; return $result; } /** * Convert a preset.xml field data into field instance. * * @param SimpleXMLElement $fieldinfo the field xml information * @param int $id the field id to use * @param bool $forpreview if the field should support preview * @return data_field_base the field instance */ private function get_field_instance( SimpleXMLElement $fieldinfo, int $id = 0, bool $forpreview = false ): data_field_base { global $CFG; // Some old field plugins require $CFG to be in the scope. $fieldrecord = $this->get_fake_field_record($fieldinfo, $id); $instance = $this->manager->get_instance(); $cm = $this->manager->get_coursemodule(); // Include the plugin. $filepath = "{$this->manager->path}/field/{$fieldrecord->type}/field.class.php"; if (file_exists($filepath)) { require_once($filepath); } $classname = "data_field_{$fieldrecord->type}"; $newfield = null; if (class_exists($classname)) { $newfield = new $classname($fieldrecord, $instance, $cm); if ($forpreview && !$newfield->supports_preview()) { $newfield = new data_field_base($fieldrecord, $instance, $cm); } } else { $newfield = new data_field_base($fieldrecord, $instance, $cm); } if ($forpreview) { $newfield->set_preview(true); } return $newfield; } /** * Generate a fake field record fomr the preset.xml field data. * * @param SimpleXMLElement $fieldinfo the field xml information * @param int $id the field id to use * @return stdClass the fake record */ private function get_fake_field_record(SimpleXMLElement $fieldinfo, int $id = 0): stdClass { $instance = $this->manager->get_instance(); // Generate stub record. $fieldrecord = (object)[ 'id' => $id, 'dataid' => $instance->id, 'type' => (string) $fieldinfo->type, 'name' => (string) $fieldinfo->name, 'description' => (string) $fieldinfo->description ?? '', 'required' => (int) $fieldinfo->required ?? 0, ]; for ($i = 1; $i < 11; $i++) { $name = "param{$i}"; $fieldrecord->{$name} = null; if (property_exists($fieldinfo, $name)) { $fieldrecord->{$name} = (string) $fieldinfo->{$name}; } } return $fieldrecord; } /** * Return sample entries to preview this preset. * * @param int $count the number of entries to generate. * @return array of sample entries */ public function get_sample_entries(int $count = 1): array { global $USER; $fields = $this->get_fields(); $instance = $this->manager->get_instance(); $entries = []; for ($current = 1; $current <= $count; $current++) { $entry = (object)[ 'id' => $current, 'userid' => $USER->id, 'groupid' => 0, 'dataid' => $instance->id, 'timecreated' => time(), 'timemodified' => time(), 'approved' => 1, ]; // Add all necessary user fields. $userfieldsapi = \core_user\fields::for_userpic()->excluding('id'); $fields = $userfieldsapi->get_required_fields(); foreach ($fields as $field) { $entry->{$field} = $USER->{$field}; } $entries[$current] = $entry; } return $entries; } /** * Load all the information from the preset.xml. */ protected function load_preset_xml() { if (!empty($this->xmlinfo)) { return; } // Load everything from the XML. $presetxml = null; if ($this->isplugin) { $path = $this->manager->path . '/preset/' . $this->shortname . '/preset.xml'; $presetxml = file_get_contents($path); } else { $presetxml = static::get_content_from_file($this->storedfile->get_filepath(), 'preset.xml'); } $this->xmlinfo = simplexml_load_string($presetxml); } /** * Return the template content from the preset. * * @param string $templatename the template name * @return string the template content */ public function get_template_content(string $templatename): string { $filename = "{$templatename}.html"; if ($templatename == 'csstemplate') { $filename = "{$templatename}.css"; } if ($templatename == 'jstemplate') { $filename = "{$templatename}.js"; } if ($this->isplugin) { $path = $this->manager->path . '/preset/' . $this->shortname . '/' . $filename; $result = file_get_contents($path); } else { $result = static::get_content_from_file($this->storedfile->get_filepath(), $filename); } if (empty($result)) { return ''; } return $result; } /** * Checks if a directory contains all the required files to define a preset. * * @param string $directory The patch to check if it contains the preset files or not. * @return bool True if the directory contains all the preset files; false otherwise. */ public static function is_directory_a_preset(string $directory): bool { $status = true; $directory = rtrim($directory, '/\\') . '/'; $presetfilenames = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml']); foreach ($presetfilenames as $filename) { $status &= file_exists($directory.$filename); } return $status; } /** * Returns the best name to show for a datapreset plugin. * * @param string $pluginname The datapreset plugin name. * @return string The plugin preset name to display. */ public static function get_name_from_plugin(string $pluginname): string { $pos = strpos($pluginname, '/'); if ($pos !== false) { $pluginname = substr($pluginname, $pos + 1); } if (!strpos(trim($pluginname), ' ') && get_string_manager()->string_exists('modulename', 'datapreset_'.$pluginname)) { return get_string('modulename', 'datapreset_'.$pluginname); } else { return $pluginname; } } /** * Returns the description to show for a datapreset plugin. * * @param string $pluginname The datapreset plugin name. * @return string The plugin preset description to display. */ public static function get_description_from_plugin(string $pluginname): string { if (get_string_manager()->string_exists('modulename_help', 'datapreset_'.$pluginname)) { return get_string('modulename_help', 'datapreset_'.$pluginname); } else { return ''; } } /** * Helper to get the value of one of the elements in the presets.xml file. * * @param string $filepath The preset filepath. * @param string $name Attribute name to return. * @return string|null The attribute value; null if the it doesn't exist or the file is not a valid XML. */ protected static function get_attribute_value(string $filepath, string $name): ?string { $value = null; $presetxml = static::get_content_from_file($filepath, 'preset.xml'); $parsedxml = simplexml_load_string($presetxml); if ($parsedxml) { switch ($name) { case 'description': if (property_exists($parsedxml, 'description')) { $value = $parsedxml->description; } break; } } return $value; } /** * Helper method to get a file record given a filename, a filepath and a userid, for any of the preset files. * * @param string $filename The filename for the filerecord that will be returned. * @param string $filepath The filepath for the filerecord that will be returned. * @param int $userid The userid for the filerecord that will be returned. * @return stdClass A filerecord object with the datapreset context, component and filearea and the given information. */ protected static function get_filerecord(string $filename, string $filepath, int $userid): stdClass { $filerecord = new stdClass; $filerecord->contextid = DATA_PRESET_CONTEXT; $filerecord->component = DATA_PRESET_COMPONENT; $filerecord->filearea = DATA_PRESET_FILEAREA; $filerecord->itemid = 0; $filerecord->filepath = $filepath; $filerecord->userid = $userid; $filerecord->filename = $filename; return $filerecord; } /** * Helper method to retrieve a file. * * @param string $filepath the directory to look in * @param string $filename the name of the file we want * @return stored_file|null the file or null if the file doesn't exist. */ public static function get_file(string $filepath, string $filename): ?stored_file { $file = null; $fs = get_file_storage(); $fileexists = $fs->file_exists( DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $filepath, $filename ); if ($fileexists) { $file = $fs->get_file( DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA, 0, $filepath, $filename ); } return $file; } /** * Helper method to retrieve the contents of a file. * * @param string $filepath the directory to look in * @param string $filename the name of the file we want * @return string|null the contents of the file or null if the file doesn't exist. */ protected static function get_content_from_file(string $filepath, string $filename): ?string { $templatefile = static::get_file($filepath, $filename); if ($templatefile) { return $templatefile->get_content(); } return null; } /** * Helper method to generate the XML for this preset. * * @return string The XML for the preset */ protected function generate_preset_xml(): string { global $DB; if ($this->isplugin) { // Only saved presets can generate the preset.xml file. return ''; } $presetxmldata = "\n\n"; // Add description. $presetxmldata .= '' . htmlspecialchars($this->description ?? '', ENT_COMPAT) . "\n\n"; // Add settings. // Raw settings are not preprocessed during saving of presets. $rawsettings = [ 'intro', 'comments', 'requiredentries', 'requiredentriestoview', 'maxentries', 'rssarticles', 'approval', 'manageapproved', 'defaultsortdir', ]; $presetxmldata .= "\n"; $instance = $this->manager->get_instance(); // First, settings that do not require any conversion. foreach ($rawsettings as $setting) { $presetxmldata .= "<$setting>" . htmlspecialchars($instance->$setting, ENT_COMPAT) . "\n"; } // Now specific settings. if ($instance->defaultsort > 0 && $sortfield = data_get_field_from_id($instance->defaultsort, $instance)) { $presetxmldata .= '' . htmlspecialchars($sortfield->field->name, ENT_COMPAT) . "\n"; } else { $presetxmldata .= "0\n"; } $presetxmldata .= "\n\n"; // Add fields. Grab all that are non-empty. $fields = $DB->get_records('data_fields', ['dataid' => $instance->id]); ksort($fields); if (!empty($fields)) { foreach ($fields as $field) { $presetxmldata .= "\n"; foreach ($field as $key => $value) { if ($value != '' && $key != 'id' && $key != 'dataid') { $presetxmldata .= "<$key>" . htmlspecialchars($value, ENT_COMPAT) . "\n"; } } $presetxmldata .= "\n\n"; } } $presetxmldata .= ''; // Check this content is a valid XML. $preset = new SimpleXMLElement($presetxmldata); return $preset->asXML(); } /** * Checks to see if the user has permission to manage the preset. * * @return bool Returns true if the user can manage this preset, false otherwise. */ public function can_manage(): bool { global $USER; if ($this->isplugin) { // Plugin presets can't be removed or edited. return false; } $context = $this->manager->get_context(); if (has_capability('mod/data:manageuserpresets', $context)) { return true; } else { if ($this->get_userid() == $USER->id) { return true; } } return false; } /** * Deletes all files related to a saved preset. * * @return bool True if the preset is a saved preset and the file exists in the file system; false otherwise. */ public function delete(): bool { if ($this->isplugin) { // Plugin presets can't be removed. return false; } $exists = false; $filepath = $this->get_path(); $dir = self::get_file($filepath, '.'); if (!empty($dir)) { $exists = true; $fs = get_file_storage(); $files = $fs->get_directory_files( $dir->get_contextid(), $dir->get_component(), $dir->get_filearea(), $dir->get_itemid(), $filepath ); if (!empty($files)) { foreach ($files as $file) { $file->delete(); } } $dir->delete(); // Reseting storedfile property because the file has been removed. $this->storedfile = null; } return $exists; } } classes/event/field_deleted.php000064400000006223152265517740012625 0ustar00. /** * The mod_data field deleted event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data field deleted event class. * * @property-read array $other { * Extra information about event. * * - string fieldname: the name of the field. * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class field_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data_fields'; $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventfielddeleted', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the field with id '$this->objectid' in the data activity " . "with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/field.php', array('d' => $this->other['dataid'])); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['fieldname'])) { throw new \coding_exception('The \'fieldname\' value must be set in other.'); } if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'data_fields', 'restore' => 'data_field'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/record_deleted.php000064400000005733152265517740013025 0ustar00. /** * The mod_data record deleted event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data record deleted event class. * * @property-read array $other { * Extra information about event. * * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class record_deleted extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data_records'; $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventrecorddeleted', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the data record with id '$this->objectid' in the data activity " . "with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/view.php', array('d' => $this->other['dataid'])); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'data_records', 'restore' => 'data_record'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/field_updated.php000064400000006223152265517740012645 0ustar00. /** * The mod_data field updated event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data field updated event class. * * @property-read array $other { * Extra information about event. * * - string fieldname: the name of the field. * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class field_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data_fields'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventfieldupdated', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the field with id '$this->objectid' in the data activity " . "with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/field.php', array('d' => $this->other['dataid'])); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['fieldname'])) { throw new \coding_exception('The \'fieldname\' value must be set in other.'); } if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'data_fields', 'restore' => 'data_field'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/field_created.php000064400000006256152265517740012634 0ustar00. /** * The mod_data field created event. * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data field created event class. * * @property-read array $other { * Extra information about event. * * - string fieldname: the name of the field. * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class field_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data_fields'; $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventfieldcreated', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' created the field with id '$this->objectid' for the data activity " . "with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/field.php', array('d' => $this->other['dataid'])); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['fieldname'])) { throw new \coding_exception('The \'fieldname\' value must be set in other.'); } if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'data_fields', 'restore' => 'data_field'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/course_module_viewed.php000064400000003163152265517740014264 0ustar00. /** * The mod_data course module viewed event. * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data course module viewed event class. * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } public static function get_objectid_mapping() { return array('db' => 'data', 'restore' => 'data'); } } classes/event/comment_deleted.php000064400000003406152265517740013204 0ustar00. /** * The mod_data comment deleted event. * * @package mod_data * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data comment deleted event class. * * @package mod_data * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_deleted extends \core\event\comment_deleted { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/view.php', array('id' => $this->contextinstanceid)); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' deleted the comment with id '$this->objectid' from the database activity with " . "course module id '$this->contextinstanceid'."; } } classes/event/record_updated.php000064400000005765152265517740013052 0ustar00. /** * The mod_data record updated event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data record updated event class. * * @property-read array $other { * Extra information about event. * * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class record_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data_records'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventrecordupdated', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the data record with id '$this->objectid' in the data activity " . "with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/view.php', array('d' => $this->other['dataid'], 'rid' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'data_records', 'restore' => 'data_record'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/comment_created.php000064400000003402152265517740013201 0ustar00. /** * The mod_data comment created event. * * @package mod_data * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data comment created event class. * * @package mod_data * @since Moodle 2.7 * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class comment_created extends \core\event\comment_created { /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/view.php', array('id' => $this->contextinstanceid)); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' added the comment with id '$this->objectid' to the database activity with " . "course module id '$this->contextinstanceid'."; } } classes/event/record_created.php000064400000005766152265517740013034 0ustar00. /** * The mod_data record created event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data record created event class. * * @property-read array $other { * Extra information about event. * * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class record_created extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['objecttable'] = 'data_records'; $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventrecordcreated', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' created the data record with id '$this->objectid' for the data activity " . "with course module id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/view.php', array('d' => $this->other['dataid'], 'rid' => $this->objectid)); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_objectid_mapping() { return array('db' => 'data_records', 'restore' => 'data_record'); } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/template_viewed.php000064400000005404152265517740013232 0ustar00. /** * The mod_data template viewed event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data template viewed event class. * * @property-read array $other { * Extra information about event. * * - int dataid the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventtemplateviewed', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the template for the data activity with course module " . "id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/templates.php', array('d' => $this->other['dataid'])); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/template_updated.php000064400000005413152265517740013375 0ustar00. /** * The mod_data template updated event. * * @package mod_data * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data template updated event class. * * @property-read array $other { * Extra information about event. * * - int dataid: the id of the data activity. * } * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template_updated extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventtemplateupdated', 'mod_data'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' updated the template for the data activity with course module " . "id '$this->contextinstanceid'."; } /** * Get URL related to the action. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/data/templates.php', array('d' => $this->other['dataid'])); } /** * Custom validation. * * @throws \coding_exception when validation does not pass. * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['dataid'])) { throw new \coding_exception('The \'dataid\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['dataid'] = array('db' => 'data', 'restore' => 'data'); return $othermapped; } } classes/event/course_module_instance_list_viewed.php000064400000002550152265517740017202 0ustar00. /** * The mod_data instance list viewed event. * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; defined('MOODLE_INTERNAL') || die(); /** * The mod_data instance list viewed event class. * * @package mod_data * @since Moodle 2.7 * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { // No code required here as the parent class handles it all. } classes/completion/custom_completion.php000064400000005433152265517740014651 0ustar00. declare(strict_types=1); namespace mod_data\completion; use core_completion\activity_custom_completion; /** * Activity custom completion subclass for the data activity. * * Class for defining mod_data's custom completion rules and fetching the completion statuses * of the custom completion rules for a given data instance and a user. * * @package mod_data * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom_completion extends activity_custom_completion { /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ public function get_state(string $rule): int { global $DB; $this->validate_rule($rule); $userentries = $DB->count_records('data_records', ['dataid' => $this->cm->instance, 'userid' => $this->userid]); $completionentries = $this->cm->customdata['customcompletionrules']['completionentries']; return ($completionentries <= $userentries) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; } /** * Fetch the list of custom completion rules that this module defines. * * @return array */ public static function get_defined_custom_rules(): array { return ['completionentries']; } /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ public function get_custom_rule_descriptions(): array { $entries = $this->cm->customdata['customcompletionrules']['completionentries'] ?? 0; return [ 'completionentries' => get_string('completiondetail:entries', 'data', $entries), ]; } /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ public function get_sort_order(): array { return [ 'completionview', 'completionentries', 'completionusegrade', 'completionpassgrade', ]; } } classes/analytics/indicator/cognitive_depth.php000064400000003310152265517740016043 0ustar00. /** * Cognitive depth indicator - data. * * @package mod_data * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Cognitive depth indicator - data. * * @package mod_data * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cognitive_depth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:cognitivedepth', 'mod_data'); } public function get_indicator_type() { return self::INDICATOR_COGNITIVE; } public function get_cognitive_depth_level(\cm_info $cm) { return self::COGNITIVE_LEVEL_2; } } classes/analytics/indicator/activity_base.php000064400000002677152265517740015535 0ustar00. /** * Activity base class. * * @package mod_data * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Activity base class. * * @package mod_data * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity { /** * Returns the name of the field that controls activity availability. * * @return null|string */ protected function get_timeclose_field() { return 'timeavailableto'; } } classes/analytics/indicator/social_breadth.php000064400000003275152265517740015645 0ustar00. /** * Social breadth indicator - data. * * @package mod_data * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\analytics\indicator; defined('MOODLE_INTERNAL') || die(); /** * Social breadth indicator - data. * * @package mod_data * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class social_breadth extends activity_base { /** * Returns the name. * * If there is a corresponding '_help' string this will be shown as well. * * @return \lang_string */ public static function get_name(): \lang_string { return new \lang_string('indicator:socialbreadth', 'mod_data'); } public function get_indicator_type() { return self::INDICATOR_SOCIAL; } public function get_social_breadth_level(\cm_info $cm) { return self::SOCIAL_LEVEL_1; } } classes/template.php000064400000105543152265517740010553 0ustar00. namespace mod_data; use action_menu; use action_menu_link_secondary; use core\output\checkbox_toggleall; use data_field_base; use html_writer; use mod_data\manager; use moodle_url; use pix_icon; use stdClass; use user_picture; use core_user; use portfolio_add_button; use data_portfolio_caller; use comment; use core_tag_tag; /** * Class template for database activity * * @package mod_data * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class template { /** @var manager the current instance manager. */ private $manager; /** @var stdClass the current instance record. */ private $instance; /** @var string the template. */ private $templatecontent; /** @var string the template name. */ private $templatename; /** @var moodle_url the base url. */ private $baseurl; /** @var string the current search if any. */ private $search; /** @var bool if ratings must be added. */ private $ratings; /** @var bool if comments must be added if not present in the template. */ private $forcecomments; /** @var bool if show more option must be added. */ private $showmore; /** @var bool if the current user can manage entries. */ private $canmanageentries = null; /** @var array if icons HTML. */ private $icons = []; /** @var array All template tags (calculated in load_template_tags). */ protected $tags = []; /** @var array The mod_data fields. */ protected $fields = []; /** @var array All fields that are not present in the template content. */ protected $otherfields = []; /** * Class contructor. * * See the add_options method for the available display options. * * @param manager $manager the current instance manager * @param string $templatecontent the template string to use * @param array $options an array of extra diplay options * @param array $fields alternative array of fields (for preview presets) */ public function __construct(manager $manager, string $templatecontent, array $options = [], ?array $fields = null) { $this->manager = $manager; $this->instance = $manager->get_instance(); $this->templatecontent = $templatecontent; $context = $manager->get_context(); $this->canmanageentries = has_capability('mod/data:manageentries', $context); $this->icons = $this->get_icons(); $this->fields = $fields ?? $manager->get_fields(); $this->add_options($options); $this->load_template_tags($templatecontent); } /** * Create a template class with the default template content. * * @param manager $manager the current instance manager. * @param string $templatename the template name. * @param bool $form whether the fields should be displayed as form instead of data. * @return self The template with the default content (to be displayed when no template is defined). */ public static function create_default_template( manager $manager, string $templatename, bool $form = false ): self { $renderer = $manager->get_renderer(); $content = ''; switch ($templatename) { case 'addtemplate': case 'asearchtemplate': case 'listtemplate': case 'rsstemplate': case 'singletemplate': $template = new \mod_data\output\defaulttemplate($manager->get_fields(), $templatename, $form); $content = $renderer->render_defaulttemplate($template); } // Some templates have extra options. $options = self::get_default_display_options($templatename); return new self($manager, $content, $options); } /** * Get default options for templates. * * For instance, the list template supports the show more button. * * @param string $templatename the template name. * @return array an array of extra diplay options. */ public static function get_default_display_options(string $templatename): array { $options = []; if ($templatename === 'singletemplate') { $options['comments'] = true; $options['ratings'] = true; } if ($templatename === 'listtemplate') { // The "Show more" button should be only displayed in the listtemplate. $options['showmore'] = true; } return $options; } /** * Return the raw template content. * * @return string the template content before parsing */ public function get_template_content(): string { return $this->templatecontent; } /** * Add extra display options. * * The extra options are: * - page: the current pagination page * - search: the current search text * - baseurl: an alternative entry url (moodle_url) * - comments: if comments must be added if not present * - ratings: if ratings must be added * * @param array $options the array of options. */ public function add_options(array $options = []) { $cm = $this->manager->get_coursemodule(); $baseurl = $options['baseurl'] ?? new moodle_url('/mod/data/view.php', ['id' => $cm->id]); if (isset($options['page'])) { $baseurl->params([ 'page' => $options['page'], ]); } $this->baseurl = $baseurl; // Save options. $this->search = $options['search'] ?? null; $this->ratings = $options['ratings'] ?? false; $this->forcecomments = $options['comments'] ?? false; $this->showmore = $options['showmore'] ?? false; $this->templatename = $options['templatename'] ?? 'singletemplate'; } /** * Scan the template tags. * * This method detects which tags are used in this template and store them * in the $this->tags attribute. This attribute will be used to determine * which replacements needs to be calculated. * * @param string $templatecontent the current template */ protected function load_template_tags(string $templatecontent) { // Detect action tags. $pattern = '/##(?P\w+?)##/'; $matches = []; preg_match_all($pattern, $templatecontent, $matches); if (!isset($matches['tags']) || empty($matches['tags'])) { return; } $this->tags = $matches['tags']; // Check if some tag require some extra template scan. foreach ($this->tags as $tagname) { $methodname = "preprocess_tag_{$tagname}"; if (method_exists($this, $methodname)) { $this->$methodname($templatecontent); } } } /** * Check if a tag is present in the template. * * @param bool $tagname the tag to check (without ##) * @return bool if the tag is present */ public function has_tag(string $tagname): bool { return in_array($tagname, $this->tags); } /** * Return the current template name. * * @return string the template name */ public function get_template_name(): string { return $this->templatename; } /** * Generate the list of action icons. * * @return pix_icon[] icon name => pix_icon */ protected function get_icons() { $attrs = ['class' => 'iconsmall dataicon']; return [ 'edit' => new pix_icon('t/editinline', get_string('edit'), '', $attrs), 'delete' => new pix_icon('t/delete', get_string('delete'), '', $attrs), 'more' => new pix_icon('t/preview', get_string('more', 'data'), '', $attrs), 'approve' => new pix_icon('t/approve', get_string('approve', 'data'), '', $attrs), 'disapprove' => new pix_icon('t/block', get_string('disapprove', 'data'), '', $attrs), ]; } /** * Return the parsed entry using a template. * * This method apply a template replacing all necessary tags. * * @param array $entries of entres to parse * @return string the entries outputs using the template */ public function parse_entries(array $entries): string { if (empty($entries)) { return ''; } $result = ''; foreach ($entries as $entry) { $result .= $this->parse_entry($entry); } return $result; } /** * Parse a single entry. * * @param stdClass $entry the entry to parse * @return string the parsed entry */ private function parse_entry(stdClass $entry): string { if (empty($this->templatecontent)) { return ''; } $context = $this->manager->get_context(); $canmanageentry = data_user_can_manage_entry($entry, $this->instance, $context); // Load all replacements for the entry. $fields = $this->get_fields_replacements($entry); $tags = $this->get_tags_replacements($entry, $canmanageentry); $replacements = array_merge($fields, $tags); $patterns = array_keys($replacements); $replacement = array_values($replacements); $result = str_ireplace($patterns, $replacement, $this->templatecontent); return $this->post_parse($result, $entry); } /** * Get all field replacements. * * @param stdClass $entry the entry object * @return array of pattern => replacement */ private function get_fields_replacements(stdClass $entry): array { $result = []; foreach ($this->fields as $field) { // Field value. $pattern = '[[' . $field->field->name . ']]'; $result[$pattern] = highlight( $this->search, $field->display_browse_field($entry->id, $this->templatename) ); // Other dynamic field information. $pattern = '[[' . $field->field->name . '#id]]'; $result[$pattern] = $field->field->id; $pattern = '[[' . $field->field->name . '#name]]'; $result[$pattern] = $field->field->name; $pattern = '[[' . $field->field->name . '#description]]'; $result[$pattern] = $field->field->description; } return $result; } /** * Get all standard tags replacements. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return array of pattern => replacement */ private function get_tags_replacements(stdClass $entry, bool $canmanageentry): array { $result = []; foreach ($this->tags as $tagname) { $methodname = "get_tag_{$tagname}_replacement"; if (method_exists($this, $methodname)) { $pattern = "##$tagname##"; $replacement = $this->$methodname($entry, $canmanageentry); $result[$pattern] = $replacement; } } return $result; } /** * Add any extra information to the parsed entry. * * @param string $result the parsed template with the entry data * @param stdClass $entry the entry object * @return string the final parsed template */ private function post_parse(string $result, stdClass $entry): string { if ($this->ratings) { $result .= data_print_ratings($this->instance, $entry, false); } if ($this->forcecomments && strpos($this->templatecontent, '##comments##') === false) { $result .= $this->get_tag_comments_replacement($entry, false); } return $result; } /** * Returns the ##edit## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_edit_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; if (!$canmanageentry) { return ''; } $backurl = new moodle_url($this->baseurl, [ 'rid' => $entry->id, 'mode' => 'single', ]); $url = new moodle_url('/mod/data/edit.php', $this->baseurl->params()); $url->params([ 'rid' => $entry->id, 'sesskey' => sesskey(), 'backto' => urlencode($backurl->out(false)) ]); return html_writer::tag( 'span', $OUTPUT->action_icon($url, $this->icons['edit']), ['class' => 'edit'] ); } /** * Returns the ##delete## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_delete_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; if (!$canmanageentry) { return ''; } $url = new moodle_url($this->baseurl, [ 'delete' => $entry->id, 'sesskey' => sesskey(), 'mode' => 'single', ]); return html_writer::tag( 'span', $OUTPUT->action_icon($url, $this->icons['delete']), ['class' => 'delete'] ); } /** * Returns the ##more## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_more_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; if (!$this->showmore) { return ''; } $url = new moodle_url($this->baseurl, [ 'rid' => $entry->id, 'filter' => 1, ]); return html_writer::tag( 'span', $OUTPUT->action_icon($url, $this->icons['more']), ['class' => 'more'] ); } /** * Returns the ##moreurl## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_moreurl_replacement(stdClass $entry, bool $canmanageentry): string { $url = new moodle_url($this->baseurl, [ 'rid' => $entry->id, 'filter' => 1, ]); return $url->out(false); } /** * Returns the ##delcheck## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_delcheck_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; if (!$this->canmanageentries) { return ''; } $checkbox = new checkbox_toggleall('listview-entries', false, [ 'id' => "entry_{$entry->id}", 'name' => 'delcheck[]', 'classes' => 'recordcheckbox', 'value' => $entry->id, 'label' => get_string('selectfordeletion', 'data'), 'labelclasses' => 'visually-hidden', ]); return $OUTPUT->render($checkbox); } /** * Returns the ##user## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_user_replacement(stdClass $entry, bool $canmanageentry): string { $cm = $this->manager->get_coursemodule(); $url = new moodle_url('/user/view.php', [ 'id' => $entry->userid, 'course' => $cm->course, ]); return html_writer::tag( 'a', fullname($entry), ['href' => $url->out(false)] ); } /** * Returns the ##userpicture## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_userpicture_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; $cm = $this->manager->get_coursemodule(); $user = user_picture::unalias($entry, null, 'userid'); // If the record didn't come with user data, retrieve the user from database. if (!isset($user->picture)) { $user = core_user::get_user($entry->userid); } return $OUTPUT->user_picture($user, ['courseid' => $cm->course, 'size' => 64]); } /** * Returns the ##export## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_export_replacement(stdClass $entry, bool $canmanageentry): string { global $CFG; if (empty($CFG->enableportfolios)) { return ''; } // Check the user can export the entry. $cm = $this->manager->get_coursemodule(); $context = $this->manager->get_context(); $canexportall = has_capability('mod/data:exportentry', $context); $canexportown = has_capability('mod/data:exportownentry', $context); if (!$canexportall && !(data_isowner($entry->id) && $canexportown)) { return ''; } // Add the portfolio export button. require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); $button->set_callback_options( 'data_portfolio_caller', ['id' => $cm->id, 'recordid' => $entry->id], 'mod_data' ); list($formats, $files) = data_portfolio_caller::formats($this->fields, $entry); $button->set_formats($formats); $result = $button->to_html(PORTFOLIO_ADD_ICON_LINK); if (is_null($result)) { $result = ''; } return $result; } /** * Returns the ##timeadded## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_timeadded_replacement(stdClass $entry, bool $canmanageentry): string { return html_writer::tag( 'span', userdate($entry->timecreated, get_string('strftimedatemonthabbr', 'langconfig')), ['title' => userdate($entry->timecreated)] ); } /** * Returns the ##timemodified## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_timemodified_replacement(stdClass $entry, bool $canmanageentry): string { return html_writer::tag( 'span', userdate($entry->timemodified, get_string('strftimedatemonthabbr', 'langconfig')), ['title' => userdate($entry->timemodified)] ); } /** * Returns the ##approve## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_approve_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; $context = $this->manager->get_context(); if (!has_capability('mod/data:approve', $context) || !$this->instance->approval || $entry->approved) { return ''; } $url = new moodle_url($this->baseurl, [ 'approve' => $entry->id, 'sesskey' => sesskey(), ]); return html_writer::tag( 'span', $OUTPUT->action_icon($url, $this->icons['approve']), ['class' => 'approve'] ); } /** * Returns the ##disapprove## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_disapprove_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; $context = $this->manager->get_context(); if (!has_capability('mod/data:approve', $context) || !$this->instance->approval || !$entry->approved) { return ''; } $url = new moodle_url($this->baseurl, [ 'disapprove' => $entry->id, 'sesskey' => sesskey(), ]); return html_writer::tag( 'span', $OUTPUT->action_icon($url, $this->icons['disapprove']), ['class' => 'disapprove'] ); } /** * Returns the ##approvalstatus## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_approvalstatus_replacement(stdClass $entry, bool $canmanageentry): string { if (!$this->instance->approval) { return ''; } return ($entry->approved) ? '' : html_writer::div(get_string('notapproved', 'data'), 'mod-data-approval-status-badge'); } /** * Returns the ##approvalstatusclass## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_approvalstatusclass_replacement(stdClass $entry, bool $canmanageentry): string { if (!$this->instance->approval) { return ''; } return ($entry->approved) ? 'approved' : 'notapproved'; } /** * Returns the ##comments## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_comments_replacement(stdClass $entry, bool $canmanageentry): string { global $CFG; if (empty($CFG->usecomments) || empty($this->instance->comments)) { return ''; } $context = $this->manager->get_context(); require_once($CFG->dirroot . '/comment/lib.php'); list($context, $course, $cm) = get_context_info_array($context->id); $cmdata = (object)[ 'context' => $context, 'course' => $course, 'cm' => $cm, 'area' => 'database_entry', 'itemid' => $entry->id, 'showcount' => true, 'component' => 'mod_data', ]; $comment = new comment($cmdata); return $comment->output(true); } /** * Returns the ##tags## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_tags_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; if (!core_tag_tag::is_enabled('mod_data', 'data_records')) { return ''; } return $OUTPUT->tag_list( core_tag_tag::get_item_tags('mod_data', 'data_records', $entry->id), '', 'data-tags' ); } /** * Returns the ##id## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_id_replacement(stdClass $entry, bool $canmanageentry): string { return (string) $entry->id; } /** * Prepare otherfield tag scanning the present template fields. * * @param string $templatecontent the template content */ protected function preprocess_tag_otherfields(string $templatecontent) { $otherfields = []; $fields = $this->manager->get_fields(); foreach ($fields as $field) { if (strpos($templatecontent, "[[" . $field->field->name . "]]") === false) { $otherfields[] = $field; } } $this->otherfields = $otherfields; } /** * Returns the ##otherfields## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_otherfields_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT; $fields = []; foreach ($this->otherfields as $field) { $fieldvalue = highlight( $this->search, $field->display_browse_field($entry->id, $this->templatename) ); $fieldinfo = [ 'fieldname' => $field->field->name, 'fieldcontent' => $fieldvalue, ]; $fields[] = $fieldinfo; } return $OUTPUT->render_from_template('mod_data/fields_otherfields', ['fields' => $fields]); } /** * Returns the ##actionsmenu## tag replacement for an entry. * * @param stdClass $entry the entry object * @param bool $canmanageentry if the current user can manage this entry * @return string the tag replacement */ protected function get_tag_actionsmenu_replacement(stdClass $entry, bool $canmanageentry): string { global $OUTPUT, $CFG; $actionmenu = new action_menu(); $actionmenu->set_kebab_trigger(); $actionmenu->set_action_label(get_string('actions')); $actionmenu->set_additional_classes('entry-actionsmenu'); // Show more. if ($this->showmore) { $showmoreurl = new moodle_url($this->baseurl, [ 'rid' => $entry->id, 'filter' => 1, ]); $actionmenu->add(new action_menu_link_secondary( $showmoreurl, null, get_string('showmore', 'mod_data') )); } if ($canmanageentry) { // Edit entry. $backurl = new moodle_url($this->baseurl, [ 'rid' => $entry->id, 'mode' => 'single', ]); $editurl = new moodle_url('/mod/data/edit.php', $this->baseurl->params()); $editurl->params([ 'rid' => $entry->id, 'backto' => urlencode($backurl->out(false)) ]); $actionmenu->add(new action_menu_link_secondary( $editurl, null, get_string('edit') )); // Delete entry. $deleteurl = new moodle_url($this->baseurl, [ 'delete' => $entry->id, 'mode' => 'single', ]); $actionmenu->add(new action_menu_link_secondary( $deleteurl, null, get_string('delete') )); } // Approve/disapprove entry. $context = $this->manager->get_context(); if (has_capability('mod/data:approve', $context) && $this->instance->approval) { if ($entry->approved) { $disapproveurl = new moodle_url($this->baseurl, [ 'disapprove' => $entry->id, 'sesskey' => sesskey(), ]); $actionmenu->add(new action_menu_link_secondary( $disapproveurl, null, get_string('disapprove', 'mod_data') )); } else { $approveurl = new moodle_url($this->baseurl, [ 'approve' => $entry->id, 'sesskey' => sesskey(), ]); $actionmenu->add(new action_menu_link_secondary( $approveurl, null, get_string('approve', 'mod_data') )); } } // Export entry to portfolio. if (!empty($CFG->enableportfolios)) { // Check the user can export the entry. $cm = $this->manager->get_coursemodule(); $canexportall = has_capability('mod/data:exportentry', $context); $canexportown = has_capability('mod/data:exportownentry', $context); if ($canexportall || (data_isowner($entry->id) && $canexportown)) { // Add the portfolio export button. require_once($CFG->libdir . '/portfoliolib.php'); $button = new portfolio_add_button(); $button->set_callback_options( 'data_portfolio_caller', ['id' => $cm->id, 'recordid' => $entry->id], 'mod_data' ); $fields = $this->manager->get_fields(); list($formats, $files) = data_portfolio_caller::formats($fields, $entry); $button->set_formats($formats); $exporturl = $button->to_html(PORTFOLIO_ADD_MOODLE_URL); if (!is_null($exporturl)) { $actionmenu->add(new action_menu_link_secondary( $exporturl, null, get_string('addtoportfolio', 'portfolio') )); } } } return $OUTPUT->render($actionmenu); } /** * Parse the template as if it was for add entry. * * This method is similar to the parse_entry but it uses the display_add_field method * instead of the display_browse_field. * * @param stdClass|null $processeddata the previous process data information. * @param int|null $entryid the possible entry id * @param stdClass|null $entrydata the entry data from a previous form or from a real entry * @return string the add entry HTML content */ public function parse_add_entry( ?stdClass $processeddata = null, ?int $entryid = null, ?stdClass $entrydata = null ): string { global $OUTPUT; $manager = $this->manager; $renderer = $manager->get_renderer(); $templatecontent = $this->templatecontent; if (!$processeddata) { $processeddata = (object)[ 'generalnotifications' => [], 'fieldnotifications' => [], ]; } $result = ''; foreach ($processeddata->generalnotifications as $notification) { $result .= $renderer->notification($notification); } $possiblefields = $manager->get_fields(); $patterns = []; $replacements = []; // Then we generate strings to replace. $otherfields = []; foreach ($possiblefields as $field) { $fieldinput = $this->get_field_input($processeddata, $field, $entryid, $entrydata); if (strpos($templatecontent, "[[" . $field->field->name . "]]") !== false) { // Replace the field tag. $patterns[] = "[[" . $field->field->name . "]]"; $replacements[] = $fieldinput; } else { // Is in another fields. $otherfields[] = [ 'fieldname' => $field->field->name, 'fieldcontent' => $fieldinput, ]; } // Replace the field id tag. $patterns[] = "[[" . $field->field->name . "#id]]"; $replacements[] = 'field_' . $field->field->id; $patterns[] = '[[' . $field->field->name . '#name]]'; $replacements[] = $field->field->name; $patterns[] = '[[' . $field->field->name . '#description]]'; $replacements[] = $field->field->description; } $patterns[] = "##otherfields##"; if (!empty($otherfields)) { $replacements[] = $OUTPUT->render_from_template( 'mod_data/fields_otherfields', ['fields' => $otherfields] ); } else { $replacements[] = ''; } if (core_tag_tag::is_enabled('mod_data', 'data_records')) { $patterns[] = "##tags##"; $replacements[] = data_generate_tag_form($entryid); } $result .= str_ireplace($patterns, $replacements, $templatecontent); return $result; } /** * Return the input form html from a specific field. * * @param stdClass $processeddata the previous process data information. * @param data_field_base $field the field object * @param int|null $entryid the possible entry id * @param stdClass|null $entrydata the entry data from a previous form or from a real entry * @return string the add entry HTML content */ private function get_field_input( stdClass $processeddata, data_field_base $field, ?int $entryid = null, ?stdClass $entrydata = null ): string { $renderer = $this->manager->get_renderer(); $errors = ''; $fieldnotifications = $processeddata->fieldnotifications[$field->field->name] ?? []; if (!empty($fieldnotifications)) { foreach ($fieldnotifications as $notification) { $errors .= $renderer->notification($notification); } } $fielddisplay = ''; if ($field->type === 'unknown') { if ($this->canmanageentries) { // Display notification for users that can manage entries. $errors .= $renderer->notification(get_string( 'missingfieldtype', 'data', (object)['name' => s($field->field->name)] )); } } else { $fielddisplay = $field->display_add_field($entryid, $entrydata); } return $errors . $fielddisplay; } } preset/imagegallery/lang/en/datapreset_imagegallery.php000064400000002301152265517740017434 0ustar00. /** * Strings for component 'datapreset_imagegallery', language 'en', branch 'MOODLE_20_STABLE' * * @package datapreset_imagegallery * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['modulename'] = 'Image gallery'; $string['modulename_help'] = 'Use this preset to collect images.'; $string['pluginname'] = 'Image gallery'; $string['privacy:metadata'] = 'The Image gallery plugin does not store any personal data.'; preset/imagegallery/rsstitletemplate.html000064400000000012152265517740015012 0ustar00[[title]] preset/imagegallery/version.php000064400000002250152265517740012723 0ustar00. /** * @package datapreset * @subpackage imagegallery * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datapreset_imagegallery'; // Full name of the plugin (used for diagnostics) preset/imagegallery/listtemplate.html000064400000001324152265517740014123 0ustar00
[[image]]
[[title]]
preset/imagegallery/classes/privacy/provider.php000064400000003024152265517740016202 0ustar00. /** * Privacy provider implementation for datapreset_imagegallery. * * @package datapreset_imagegallery * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datapreset_imagegallery\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy provider implementation for datapreset_imagegallery. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } preset/imagegallery/singletemplate.html000064400000002247152265517740014436 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##actionsmenu##
##approvalstatus##

[[title]]

[[description]]
[[image]]
##otherfields##
preset/imagegallery/csstemplate.css000064400000001011152265517740013555 0ustar00/****** List View CSS ******/ #imagegallery-list img.list_picture { width: 100%; height: 15vw; object-fit: cover; } #imagegallery-list .card { margin-top: 30px; margin-bottom: 15px; } .imagegallery-listentry .card-title { font-size: larger; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } /***** Single View CSS *****/ .imagegallery-single-body .col { padding: 10px 15px; } .imagegallery-single img.list_picture { max-width: 100%; height: auto; } preset/imagegallery/preset.xml000064400000001461152265517740012554 0ustar00 0 0 3 0 0 0 0 0 0 0 0 0 0 picture image 2097152 text title textarea description 0 60 10 1 0 preset/imagegallery/listtemplatefooter.html000064400000000007152265517740015337 0ustar00 preset/imagegallery/listtemplateheader.html000064400000000152152265517740015272 0ustar00
preset/imagegallery/jstemplate.js000064400000000000152265517740013222 0ustar00preset/imagegallery/tests/behat/imagegallery_preset.feature000064400000013774152265517740020370 0ustar00@mod @mod_data @datapreset @data_preset_imagegallery Feature: Users can use the Image gallery preset In order to create an Image gallery database As a user I need to apply and use the Image gallery preset Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Alice | Student | student1@example.com | | teacher1 | Pau | Teacher | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Mountain landscapes | Database intro | C1 | data1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" And the following "mod_data > entries" exist: | database | user | title | description | image | | data1 | student1 | First image | This is the description text for image 1 | first.png | | data1 | teacher1 | Second image | And this is the description text for image 2 | second.png | @javascript Scenario: Users view entries When I am on the "Mountain landscapes" "data activity" page logged in as student1 Then I should see "First image" And I should not see "This is the description text for image 1" And I should not see "first.png" And I should not see "Alice Student" in the "#imagegallery-list" "css_element" And "//a/child::img[contains(@src, 'first.png')]" "xpath_element" should exist And "Actions" "button" should exist in the "#imagegallery-list" "css_element" And I should see "Second image" And I should not see "And this is the description text for image 2" And I should not see "second.png" And I should not see "Pau Teacher" And "//a/child::img[contains(@src, 'second.png')]" "xpath_element" should exist # Single view. And I select "Single view" from the "jump" singleselect And I should see "First image" And I should see "Alice Student" in the ".imagegallery-single" "css_element" And I should see "This is the description text for image 1" And "//a/child::img[contains(@src, 'first.png')]" "xpath_element" should exist And "Actions" "button" should exist in the ".imagegallery-single" "css_element" And I should not see "Second image" And I should not see "And this is the description text for image 2" And I should not see "Pau Teacher" And "//a/child::img[contains(@src, 'second.png')]" "xpath_element" should not exist And I follow "Next" And I should see "Second image" And I should see "Pau Teacher" And I should see "And this is the description text for image 2" And "//a/child::img[contains(@src, 'second.png')]" "xpath_element" should exist # This student can't edit or delete this entry, so the Actions menu shouldn't be displayed. And "Actions" "button" should not exist in the ".imagegallery-single" "css_element" And I should not see "First image" And I should not see "Alice Student" in the ".imagegallery-single" "css_element" And I should not see "This is the description text for image 1" And "//a/child::img[contains(@src, 'first.png')]" "xpath_element" should not exist @javascript Scenario: Users can search entries Given I am on the "Mountain landscapes" "data activity" page logged in as student1 And "First image" "text" should appear before "Second image" "text" When I click on "Advanced search" "checkbox" And I should see "First name" And I should see "Last name" And I set the field "title" to "First image" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "First image" And I should not see "Second image" But I set the field "title" to "image" And I set the field "Order" to "Descending" And I click on "Save settings" "button" in the "data_adv_form" "region" And "Second image" "text" should appear before "First image" "text" @javascript Scenario: Users can add entries Given I am on the "Mountain landscapes" "data activity" page logged in as student1 When I press "Add entry" And I set the field "title" to "New image" And I set the field "description" to "This is the description for the new image." And I press "Save" Then I should see "New image" And I should see "This is the description for the new image." @javascript Scenario: Image gallery. Renaming a field should affect the template Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I navigate to "Fields" in current page administration And I open the action menu in "title" "table_row" And I choose "Edit" in the open action menu And I set the field "Field name" to "Edited field name" And I press "Save" And I should see "Field updated" When I navigate to "Database" in current page administration Then I click on "Advanced search" "checkbox" And I should see "Edited field name" And I click on "Add entry" "button" And I should see "Edited field name" @javascript Scenario: Image gallery. Has otherfields tag Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Extra field | Test field description | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I select "Single view" from the "jump" singleselect Then I should see "Extra field" And I click on "Add entry" "button" And I should see "Extra field" preset/imagegallery/addtemplate.html000064400000000575152265517740013707 0ustar00
[[title#name]]
[[title]]
[[image#name]]
[[image]]
[[description#name]]
[[description]]
##otherfields##
preset/imagegallery/asearchtemplate.html000064400000001203152265517740014552 0ustar00
First name
##firstname##
Last name
##lastname##
[[title#name]]
[[title]]
[[description#name]]
[[description]]
preset/imagegallery/rsstemplate.html000064400000002135152265517740013760 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##approvalstatus##

[[title]]

[[description]]
[[image]]
preset/journal/lang/en/datapreset_journal.php000064400000002231152265517740015456 0ustar00. /** * Strings for component 'datapreset_journal', language 'en'. * * @package datapreset_journal * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['modulename'] = 'Journal'; $string['modulename_help'] = 'Use this preset for a journal, diary, reflections tool or research log.'; $string['pluginname'] = 'Journal'; $string['privacy:metadata'] = 'The Journal plugin doesn\'t store any personal data.'; preset/journal/rsstitletemplate.html000064400000000012152265517740014022 0ustar00[[Title]] preset/journal/version.php000064400000002255152265517740011740 0ustar00. /** * Data Journal preset version information. * * @package datapreset_journal * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datapreset_journal'; // Full name of the plugin (used for diagnostics). preset/journal/listtemplate.html000064400000002205152265517740013132 0ustar00
##actionsmenu##
##approvalstatus##
[[Content]]
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
preset/journal/classes/privacy/provider.php000064400000002415152265517740015215 0ustar00. namespace datapreset_journal\privacy; /** * Privacy provider implementation for datapreset_journal. * * @package datapreset_journal * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } preset/journal/singletemplate.html000064400000002040152265517740013435 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##actionsmenu##
##approvalstatus##

[[title]]

[[Content]]
##otherfields##
preset/journal/csstemplate.css000064400000000244152265517740012574 0ustar00/****** List View CSS ******/ .truncate-overflow { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } preset/journal/preset.xml000064400000001113152265517740011556 0ustar00 0 0 0 0 0 0 1 0 0 text Title 0 textarea Content 0 60 10 1 0 preset/journal/listtemplatefooter.html000064400000000007152265517740014347 0ustar00
preset/journal/listtemplateheader.html000064400000000030152265517740014275 0ustar00
preset/journal/jstemplate.js000064400000000000152265517740012232 0ustar00preset/journal/tests/behat/journal_preset.feature000064400000012155152265517740016400 0ustar00@mod @mod_data @datapreset @datapreset_journal Feature: Users can use the Journal preset In order to create a Journal database As a user I need to apply and use the Journal preset Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Alice | Student | student1@example.com | | teacher1 | Pau | Teacher | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Student reflections | Database intro | C1 | data1 | And I am on the "Student reflections" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "fullname" "radio" in the "Journal" "table_row" And I click on "Use this preset" "button" And the following "mod_data > entries" exist: | database | user | Title | Content | | data1 | student1 | Reflection created by student | This is the content for the entry 1 | | data1 | teacher1 | Reflection created by teacher | And this is the content for the entry 2 | @javascript Scenario: Journal. Users view entries When I am on the "Student reflections" "data activity" page logged in as student1 Then I should see "Reflection created by student" And I should see "This is the content for the entry 1" And "Actions" "button" should exist in the "#journal-list" "css_element" And I should see "Reflection created by teacher" And I should see "And this is the content for the entry 2" # Single view. And I select "Single view" from the "jump" singleselect And I should see "Reflection created by student" And I should see "This is the content for the entry 1" And "Actions" "button" should exist in the ".journal-single" "css_element" And I should not see "Reflection created by teacher" And I should not see "And this is the content for the entry 2" And I follow "Next" And I should see "Reflection created by teacher" And I should see "And this is the content for the entry 2" # This student can't edit or delete this entry, so the Actions menu shouldn't be displayed. And "Actions" "button" should not exist in the ".journal-single" "css_element" And I should not see "Reflection created by student" And I should not see "This is the content for the entry 1" @javascript Scenario: Journal. Users can search entries Given I am on the "Student reflections" "data activity" page logged in as student1 And "Reflection created by student" "text" should appear before "Reflection created by teacher" "text" When I click on "Advanced search" "checkbox" And I should see "First name" And I should see "Last name" And I set the field "Title" to "student" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "Reflection created by student" And I should not see "Reflection created by teacher" But I set the field "Title" to "Reflection" And I set the field "Order" to "Descending" And I click on "Save settings" "button" in the "data_adv_form" "region" And "Reflection created by teacher" "text" should appear before "Reflection created by student" "text" @javascript Scenario: Journal. Users can add entries Given I am on the "Student reflections" "data activity" page logged in as student1 When I press "Add entry" And I set the field "Title" to "This is the title" And I set the field "Content" to "This is the content for the new entry." And I press "Save" Then I should see "This is the title" And I should see "This is the content for the new entry." @javascript Scenario: Journal. Renaming a field should affect the template Given I am on the "Student reflections" "data activity" page logged in as teacher1 And I navigate to "Fields" in current page administration And I open the action menu in "Content" "table_row" And I choose "Edit" in the open action menu And I set the field "Field name" to "Edited field name" And I press "Save" And I should see "Field updated" When I navigate to "Database" in current page administration Then I click on "Advanced search" "checkbox" And I should see "Edited field name" And I click on "Add entry" "button" And I should see "Edited field name" @javascript Scenario: Journal. Has otherfields tag Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Extra field | Test field description | And I am on the "Student reflections" "data activity" page logged in as teacher1 When I click on "Add entry" "button" Then I should see "Extra field" preset/journal/addtemplate.html000064400000000412152265517740012705 0ustar00
[[Title#name]]
[[Title]]
[[Content#name]]
[[Content]]
##otherfields##
preset/journal/asearchtemplate.html000064400000001132152265517740013563 0ustar00
First name
##firstname##
Last name
##lastname##
[[Title#name]]
[[Title]]
[[Content#name]]
[[Content]]
preset/journal/rsstemplate.html000064400000001726152265517740012775 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##approvalstatus##

[[title]]

[[Content]]
preset/resources/lang/en/datapreset_resources.php000064400000002274152265517740016365 0ustar00. /** * Strings for component 'datapreset_resources', language 'en'. * * @package datapreset_resources * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['modulename'] = 'Resources'; $string['modulename_help'] = 'Collect resources such as web links or book reviews in a table, with links to the detailed view.'; $string['pluginname'] = 'Resources'; $string['privacy:metadata'] = 'The Resources plugin does not store any personal data.'; preset/resources/rsstitletemplate.html000064400000000012152265517740014362 0ustar00[[Title]] preset/resources/version.php000064400000002263152265517740012277 0ustar00. /** * Data Resources preset version information. * * @package datapreset_resources * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datapreset_resources'; // Full name of the plugin (used for diagnostics). preset/resources/listtemplate.html000064400000000512152265517740013471 0ustar00 [[Title]] [[Type]] [[Author]] [[Web link]]
##approvalstatus##
##actionsmenu## preset/resources/classes/privacy/provider.php000064400000002423152265517740015554 0ustar00. namespace datapreset_resources\privacy; /** * Privacy provider implementation for datapreset_resources. * * @package datapreset_resources * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } preset/resources/singletemplate.html000064400000003715152265517740014007 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##actionsmenu##
##approvalstatus##

[[Title]]

Author of resource
[[Author]]
[[Type#name]]
[[Type]]
Link
[[Web link]]
[[Cover]]

Content

[[Description]]
##otherfields##
preset/resources/csstemplate.css000064400000000157152265517740013137 0ustar00/****** Single View CSS ******/ .resources-single .img-wrapper img { flex-shrink: 0; width: 200px; } preset/resources/preset.xml000064400000001750152265517740012125 0ustar00 0 0 0 0 0 0 1 0 0 text Title 0 text Author 0 picture Cover 0 0 textarea Description 0 60 10 1 0 url Web link 0 1 1 menu Type 0 Type1 Type2 Type3 preset/resources/listtemplatefooter.html000064400000000026152265517740014710 0ustar00 preset/resources/listtemplateheader.html000064400000000641152265517740014645 0ustar00 preset/resources/jstemplate.js000064400000000000152265517740012572 0ustar00preset/resources/tests/behat/resources_preset.feature000064400000013766152265517740017311 0ustar00@mod @mod_data @datapreset @datapreset_resources Feature: Users can use the Resources preset In order to create a Resources database As a user I need to apply and use the Resources preset Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Alice | Student | student1@example.com | | teacher1 | Pau | Teacher | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Student resources | Database intro | C1 | data1 | And I am on the "Student resources" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "fullname" "radio" in the "Resources" "table_row" And I click on "Use this preset" "button" And the following "mod_data > entries" exist: | database | user | Title | Description | Type | Author | Web link | Cover | | data1 | student1 | My favourite book | Book content | Type1 | The book author | http://myfavouritebook.cat | first.png | | data1 | teacher1 | My favourite podcast | Podcast content| Type2 | The podcast author | http://myfavouritepodcast.cat | second.png | @javascript Scenario: Resources. Users view entries When I am on the "Student resources" "data activity" page logged in as student1 Then I should see "My favourite book" And I should see "Type1" And I should see "The book author" And I should see "http://myfavouritebook.cat" And I should not see "Book content" And "Actions" "button" should exist in the "#resources-list" "css_element" And I should see "My favourite podcast" And I should see "Type2" And I should see "The podcast author" And I should see "http://myfavouritepodcast.cat" And I should not see "Podcast content" # Single view. And I select "Single view" from the "jump" singleselect And I should see "My favourite book" And I should see "Type1" And I should see "The book author" And I should see "http://myfavouritebook.cat" And I should see "Book content" And "Actions" "button" should exist in the ".resources-single" "css_element" And I should not see "My favourite podcast" And I should not see "Type2" And I should not see "The podcast author" And I should not see "http://myfavouritepodcast.cat" And I should not see "Podcast content" And I follow "Next" And I should see "My favourite podcast" And I should see "Type2" And I should see "The podcast author" And I should see "http://myfavouritepodcast.cat" And I should see "Podcast content" # This student can't edit or delete this entry, so the Actions menu shouldn't be displayed. And "Actions" "button" should not exist in the ".resources-single" "css_element" And I should not see "My favourite book" And I should not see "Type1" And I should not see "The book author" And I should not see "http://myfavouritebook.cat" And I should not see "Book content" @javascript Scenario: Resources. Users can search entries Given I am on the "Student resources" "data activity" page logged in as student1 And "My favourite book" "text" should appear before "My favourite podcast" "text" When I click on "Advanced search" "checkbox" And I should see "First name" And I should see "Last name" And I set the field "Title" to "book" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "My favourite book" And I should not see "My favourite podcast" But I set the field "Title" to "favourite" And I set the field "Order" to "Descending" And I click on "Save settings" "button" in the "data_adv_form" "region" And "My favourite podcast" "text" should appear before "My favourite book" "text" @javascript Scenario: Resources. Users can add entries Given I am on the "Student resources" "data activity" page logged in as student1 When I press "Add entry" And I set the field "Title" to "This is the title" And I set the field "Author" to "This is the author" And I set the field "Description" to "This is description." And I set the field "Web link" to "https://thisisthelink.cat" And I set the field "Type" to "Type2" And I press "Save" Then I should see "This is the title" And I should see "This is the author" And I should see "https://thisisthelink.cat" And I should see "Type2" @javascript Scenario: Resources. Renaming a field should affect the template Given I am on the "Student resources" "data activity" page logged in as teacher1 And I navigate to "Fields" in current page administration And I open the action menu in "Type" "table_row" And I choose "Edit" in the open action menu And I set the field "Field name" to "Edited field name" And I press "Save" And I should see "Field updated" When I navigate to "Database" in current page administration Then I click on "Advanced search" "checkbox" And I should see "Edited field name" And I click on "Add entry" "button" And I should see "Edited field name" @javascript Scenario: Resources. Has otherfields tag Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Extra field | Test field description | And I am on the "Student resources" "data activity" page logged in as teacher1 When I select "Single view" from the "jump" singleselect Then I should see "Extra field" And I click on "Add entry" "button" And I should see "Extra field" preset/resources/addtemplate.html000064400000001250152265517740013246 0ustar00
[[Title#name]]
[[Title]]
Author of resource
[[Author]]
[[Cover#name]]
[[Cover]]
Content
[[Description]]
[[Web link#name]]
[[Web link]]
[[Type#name]]
[[Type]]
##otherfields##
preset/resources/asearchtemplate.html000064400000001771152265517740014134 0ustar00
First name
##firstname##
Last name
##lastname##
[[Title#name]]
[[Title]]
Author of resource
[[Author]]
Content
[[Description]]
[[Web link#name]]
[[Web link]]
[[Type#name]]
[[Type]]
preset/resources/rsstemplate.html000064400000003630152265517740013331 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##approvalstatus##

[[Title]]

Author of resource
[[Author]]
[[Type#name]]
[[Type]]
Link
[[Web link]]
[[Cover]]

Content

[[Description]]
preset/proposals/lang/en/datapreset_proposals.php000064400000002352152265517740016402 0ustar00. /** * Strings for component 'datapreset_proposals', language 'en'. * * @package datapreset_proposals * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['modulename'] = 'Proposals'; $string['modulename_help'] = 'Use this preset to collect proposals for topics, research, projects, etc. Fields include status such as approved, declined, revise or pending.'; $string['pluginname'] = 'Proposals'; $string['privacy:metadata'] = 'The Proposals plugin does not store any personal data.'; preset/proposals/rsstitletemplate.html000064400000000012152265517740014372 0ustar00[[Title]] preset/proposals/version.php000064400000002263152265517740012307 0ustar00. /** * Data Proposals preset version information. * * @package datapreset_proposals * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datapreset_proposals'; // Full name of the plugin (used for diagnostics). preset/proposals/listtemplate.html000064400000002233152265517740013503 0ustar00
preset/proposals/classes/privacy/provider.php000064400000002423152265517740015564 0ustar00. namespace datapreset_proposals\privacy; /** * Privacy provider implementation for datapreset_proposals. * * @package datapreset_proposals * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } preset/proposals/singletemplate.html000064400000003016152265517740014011 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##actionsmenu##
##approvalstatus##

[[Title]]

[[Status#name]]

[[Status]]

[[Summary#name]]

[[Summary]]

[[Content#name]]

[[Content]]
##otherfields##
preset/proposals/csstemplate.css000064400000000246152265517740013146 0ustar00/****** List View CSS ******/ #proposals-list .card-header a.d-block { color: black; font-weight: bold; } #proposals-list .card { overflow: visible; } preset/proposals/preset.xml000064400000001437152265517740012137 0ustar00 0 0 0 0 0 0 1 0 0 text Title 0 menu Status 0 Approved In review Rejected Pending text Summary 0 textarea Content 0 60 10 1 0 preset/proposals/listtemplatefooter.html000064400000000007152265517740014717 0ustar00 preset/proposals/listtemplateheader.html000064400000000134152265517740014652 0ustar00
preset/proposals/jstemplate.js000064400000000000152265517740012602 0ustar00preset/proposals/tests/behat/proposals_preset.feature000064400000013674152265517740017327 0ustar00@mod @mod_data @datapreset @datapreset_proposals Feature: Users can use the Proposals preset In order to create a Proposals database As a user I need to apply and use the Proposals preset Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Alice | Student | student1@example.com | | teacher1 | Pau | Teacher | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Student projects | Database intro | C1 | data1 | And I am on the "Student projects" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "fullname" "radio" in the "Proposals" "table_row" And I click on "Use this preset" "button" And the following "mod_data > entries" exist: | database | user | Title | Summary | Content | Status | | data1 | student1 | Project created by student | Summary 1 | Content for entry 1 | Pending | | data1 | teacher1 | Project created by teacher | Summary 2 | And content for entry 2 | Rejected | @javascript Scenario: Proposals. Users view entries When I am on the "Student projects" "data activity" page logged in as student1 Then I should see "Project created by student" And "Summary 1" "text" should exist And "Actions" "button" should exist in the "#proposals-list" "css_element" And I should see "Project created by teacher" And "Summary 2" "text" should exist And I click on "Project created by student" "link" And I click on "Project created by teacher" "link" And I should see "Summary 1" And I should not see "Content for entry 1" And I should see "Summary 2" And I should not see "And content for entry 2" # Single view. And I select "Single view" from the "jump" singleselect And I should see "Project created by student" And I should see "Summary 1" And I should see "Content for entry 1" And I should see "Pending" And "Actions" "button" should exist in the ".proposals-single" "css_element" And I should not see "Project created by teacher" And I should not see "Summary 2" And I should not see "And content for entry 2" And I should not see "Rejected" And I follow "Next" And I should see "Project created by teacher" And I should see "Summary 2" And I should see "And content for entry 2" And I should see "Rejected" # This student can't edit or delete this entry, so the Actions menu shouldn't be displayed. And "Actions" "button" should not exist in the ".proposals-single" "css_element" And I should not see "Project created by student" And I should not see "Summary 1" And I should not see "Content for entry 1" And I should not see "Pending" @javascript Scenario: Proposals. Users can search entries Given I am on the "Student projects" "data activity" page logged in as student1 And "Project created by student" "text" should appear before "Project created by teacher" "text" When I click on "Advanced search" "checkbox" And I should see "First name" And I should see "Last name" And I set the field "Title" to "student" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "Project created by student" And I should not see "Project created by teacher" But I set the field "Title" to "Project" And I set the field "Order" to "Descending" And I click on "Save settings" "button" in the "data_adv_form" "region" And "Project created by teacher" "text" should appear before "Project created by student" "text" @javascript Scenario: Proposals. Users can add entries Given I am on the "Student projects" "data activity" page logged in as student1 When I press "Add entry" And I set the field "Title" to "This is the title" And I set the field "Summary" to "This is the summary for the new entry." And I set the field "Content" to "This is the content for the new entry." And I set the field "Status" to "Approved" And I press "Save" Then I should see "This is the title" And I should see "Approved" And I should see "This is the summary for the new entry." And I should see "This is the content for the new entry." @javascript Scenario: Proposals. Renaming a field should affect the template Given I am on the "Student projects" "data activity" page logged in as teacher1 And I navigate to "Fields" in current page administration And I open the action menu in "Summary" "table_row" And I choose "Edit" in the open action menu And I set the field "Field name" to "Edited field name" And I press "Save" And I should see "Field updated" When I navigate to "Database" in current page administration Then I click on "Advanced search" "checkbox" And I should see "Edited field name" And I select "Single view" from the "jump" singleselect And I should see "Edited field name" And I click on "Add entry" "button" And I should see "Edited field name" @javascript Scenario: Proposals. Has otherfields tag Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Extra field | Test field description | And I am on the "Student projects" "data activity" page logged in as teacher1 When I select "Single view" from the "jump" singleselect Then I should see "Extra field" And I click on "Add entry" "button" And I should see "Extra field" preset/proposals/addtemplate.html000064400000000736152265517740013266 0ustar00
[[Title#name]]
[[Title]]
[[Status#name]]
[[Status]]
[[Summary#name]]
[[Summary]]
[[Content#name]]
[[Content]]
##otherfields##
preset/proposals/asearchtemplate.html000064400000001574152265517740014145 0ustar00
First name
##firstname##
Last name
##lastname##
[[Title#name]]
[[Title]]
[[Status#name]]
[[Status]]
[[Summary#name]]
[[Summary]]
[[Content#name]]
[[Content]]
preset/proposals/rsstemplate.html000064400000002661152265517740013344 0ustar00
##userpicture##
##user##
##timeadded##
Last edited: ##timemodified##
##approvalstatus##

[[Title]]

[[Status#name]]
[[Status]]

[[Summary#name]]

[[Summary]]

[[Content#name]]

[[Content]]
field/picture/lang/en/datafield_picture.php000064400000003045152265517740015026 0ustar00. /** * Strings for component 'datafield_picture', language 'en', branch 'master' * * @package datafield * @subpackage picture * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Image'; $string['fieldtypelabel'] = 'Image'; $string['error_invalidparam1'] = 'Width in single view needs to be a numeric value.'; $string['error_invalidparam2'] = 'Height in single view needs to be a numeric value.'; $string['error_invalidparam4'] = 'Width in list view needs to be a numeric value.'; $string['error_invalidparam5'] = 'Height in list view needs to be a numeric value.'; $string['privacy:metadata'] = 'The Image field component doesn\'t store any personal data; it uses tables defined in mod_data.'; $string['sample'] = 'Image description placeholder'; field/picture/pix/sample.png000064400000006160152265517740012112 0ustar00PNG  IHDRAsBIT|d pHYs+tEXtSoftwarewww.inkscape.org< IDATxkS[%$nl1$1N˴3 ,a0Ht^z5F?vZ{n' E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR:u2F=%h/ժ{ nZqp08&ӘNuE ZVt:vceXZ\{,?kG&Iى~/sՊX}7bGHp_GxYVVq?}6^Q dMܺ>d΍8L>Çٳ &.n7</b0\dq,uMGYšs3mW_< M׳3~wdMf8_̇nwyfpw 3=?" )@FB*790NFd2vz= - Tf8n|pt;G]xQ\V M$ ÇzfFzocss3VVWKpR Ͽ}xx</^}%|G ۷v1/ݍ8~p44#J1 矊}s`8 B@(h4W^䴳6x8P p?_}{{;%~>0P>p0,u8& ^W 3N`XDžIFJ90ǕAe>& f<WQ+towv FB szZ}$?''1ouS$# svVgggsqDD֛# өX~;8k Xq@_^y#"NBarVWm% f\ٛ[Vz+eX6 R~h7t k#pzV_q#pZZ\RqUts:a>+'ֹ-}WTg/,b7|.[]rPNE.iܫjOeX΢RtPWՊ|yE)2G"4P?{9kaa!}ɊQ֑eDh*T_~:kuu5]<<.s$B5BzN~?8ePbVzQ_*t6ţ#_]D|*7q2$Ƨ1LnGgq1q޽>漮x\D@r=(U~өP;@`E$X7дx\& pよP'+4=D "B.m "TM@wnk<.UniɿU?,{BU[ao/~x7x@9Z #"P6EEDƣx@:u|7ꦎ_g`yR,r *k PE@x# Ny766R7^|guCxYzGaZ;C-#gIDh9jbmm-~ut:ncµƧq00NNF1 jbӉrVVns% tqv~Vvqi":vc *,R E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@HR E@H٤2 hIENDB`field/picture/pix/icon.svg000064400000001153152265517740011571 0ustar00 field/picture/pix/sample.svg000064400000006376152265517740012136 0ustar00 image/svg+xml field/picture/version.php000064400000002234152265517740011517 0ustar00. /** * @package datafield * @subpackage picture * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_picture'; // Full name of the plugin (used for diagnostics) field/picture/classes/privacy/provider.php000064400000006710152265517740015001 0ustar00. /** * Privacy Subsystem implementation for datafield_picture. * * @package datafield_picture * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_picture\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_picture implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { if ($fieldobj->param1) { $defaultvalue->field['width'] = $fieldobj->param1; } if ($fieldobj->param2) { $defaultvalue->field['height'] = $fieldobj->param2; } if ($fieldobj->param3) { $defaultvalue->field['maxbytes'] = $fieldobj->param3; } // Change file name to file path. $defaultvalue->file = writer::with_context($context) ->rewrite_pluginfile_urls([$recordobj->id, $contentobj->id], 'mod_data', 'content', $contentobj->id, '@@PLUGINFILE@@/' . $defaultvalue->content); if (isset($defaultvalue->content1)) { $defaultvalue->alttext = $defaultvalue->content1; } unset($defaultvalue->content); unset($defaultvalue->content1); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/picture/templates/picture.mustache000064400000007151152265517740014530 0ustar00{{! 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 . }} {{! @template datafield_picture/picture The purpose of this template is to enable the user to define a picture field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 int The fields width in single view value. * param2 int The fields height in single view value. * param3 int The file's maximum size. * param4 int The fields width in list view value. * param5 int The fields height in list view value. * filesizes array The field's file sizes. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": 100, "param2": 200, "param3": 67108864, "param4": 300, "param5": 400, "options": [ { "name": "Site upload limit (64MB)", "value": "0", "selected": true }, { "name": "64MB", "value": "67108864", "selected": false } ] } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/picture/field.class.php000064400000045204152265517740012225 0ustar00. /** * Class picture field for database activity * * @package datafield_picture * @copyright 2005 Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_field_picture extends data_field_base { var $type = 'picture'; var $previewwidth = 50; var $previewheight = 50; public function supports_preview(): bool { return true; } public function get_data_content_preview(int $recordid): stdClass { return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => 'datafield_picture/preview', 'content1' => get_string('sample', 'datafield_picture'), 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $CFG, $DB, $OUTPUT, $USER, $PAGE; // Necessary for the constants used in args. require_once($CFG->dirroot . '/repository/lib.php'); $file = false; $content = false; $alttext = ''; $itemid = null; $fs = get_file_storage(); if ($formdata) { $fieldname = 'field_' . $this->field->id . '_file'; $itemid = clean_param($formdata->$fieldname, PARAM_INT); $fieldname = 'field_' . $this->field->id . '_alttext'; if (isset($formdata->$fieldname)) { $alttext = $formdata->$fieldname; } } else if ($recordid) { if (!$content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid))) { // Quickly make one now! $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $id = $DB->insert_record('data_content', $content); $content = $DB->get_record('data_content', array('id' => $id)); } file_prepare_draft_area($itemid, $this->context->id, 'mod_data', 'content', $content->id); if (!empty($content->content)) { if ($file = $fs->get_file($this->context->id, 'mod_data', 'content', $content->id, '/', $content->content)) { $usercontext = context_user::instance($USER->id); if ($thumbfile = $fs->get_file($usercontext->id, 'user', 'draft', $itemid, '/', 'thumb_'.$content->content)) { $thumbfile->delete(); } } } $alttext = $content->content1; } else { $itemid = file_get_unused_draft_itemid(); } $str = '
'; $str .= '
'.s($this->field->name); if ($this->field->required) { $str .= ' ' . get_string('requiredelement', 'form') . ''; $image = $OUTPUT->pix_icon('req', get_string('requiredelement', 'form')); $str .= html_writer::div($image, 'inline-req'); } else { $str .= ''; } $str .= ''; $options = new stdClass(); $options->maxbytes = $this->field->param3; $options->maxfiles = 1; // Only one picture permitted. $options->itemid = $itemid; $options->accepted_types = array('web_image'); $options->return_types = FILE_INTERNAL; $options->context = $PAGE->context; if (!empty($file)) { $options->filename = $file->get_filename(); $options->filepath = '/'; } $fm = new form_filemanager($options); // Print out file manager. $output = $PAGE->get_renderer('core', 'files'); $str .= '
'; $str .= $output->render($fm); $str .= '
'; $str .= ''; $str .= ' '; $str .= '
'; $str .= '
'; $str .= '
'; $str .= '
'; return $str; } /** * Validate the image field type parameters. * * This will check for valid numeric values in the width and height fields. * * @param stdClass $fieldinput the field input data * @return array array of error messages if width or height parameters are not numeric * @throws coding_exception */ public function validate(stdClass $fieldinput): array { $errors = []; // These are the params we have to check if they are numeric, because they represent width and height of the image // in single and list view. $widthandheightparams = ['param1', 'param2', 'param4', 'param5']; foreach ($widthandheightparams as $param) { if (!empty($fieldinput->$param) && !is_numeric($fieldinput->$param)) { $errors[$param] = get_string('error_invalid' . $param, 'datafield_picture'); } } return $errors; } // TODO delete this function and instead subclass data_field_file - see MDL-16493 function get_file($recordid, $content=null) { global $DB; if (empty($content)) { if (!$content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { return null; } } $fs = get_file_storage(); if (!$file = $fs->get_file($this->context->id, 'mod_data', 'content', $content->id, '/', $content->content)) { return null; } return $file; } function display_search_field($value = '') { return '' . ''; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_picture_$i"; return array(" ({$tablealias}.fieldid = {$this->field->id} AND ".$DB->sql_like("{$tablealias}.content", ":$name", false).") ", array($name=>"%$value%")); } function display_browse_field($recordid, $template) { global $OUTPUT; $content = $this->get_data_content($recordid); if (!$content || empty($content->content)) { return ''; } $alt = $content->content1; $title = $alt; $width = $this->field->param1 ? ' width="' . s($this->field->param1) . '" ' : ' '; $height = $this->field->param2 ? ' height="' . s($this->field->param2) . '" ' : ' '; if ($this->preview) { $imgurl = $OUTPUT->image_url('sample', 'datafield_picture'); return '' . s($alt) . ''; } if ($template == 'listtemplate') { $filename = 'thumb_' . $content->content; // Thumbnails are already converted to the correct width and height. $width = ''; $height = ''; $url = new moodle_url('/mod/data/view.php', ['d' => $this->field->dataid, 'rid' => $recordid]); } else { $filename = $content->content; $url = null; } $imgurl = moodle_url::make_pluginfile_url($this->context->id, 'mod_data', 'content', $content->id, '/', $filename); if (!$url) { $url = $imgurl; } $img = '' . s($alt) .
            ''; return '' . $img . ''; } function update_field() { global $DB, $OUTPUT; // Get the old field data so that we can check whether the thumbnail dimensions have changed $oldfield = $DB->get_record('data_fields', array('id'=>$this->field->id)); $DB->update_record('data_fields', $this->field); // Have the thumbnail dimensions changed? if ($oldfield && ($oldfield->param4 != $this->field->param4 || $oldfield->param5 != $this->field->param5)) { // Check through all existing records and update the thumbnail if ($contents = $DB->get_records('data_content', array('fieldid'=>$this->field->id))) { $fs = get_file_storage(); if (count($contents) > 20) { echo $OUTPUT->notification(get_string('resizingimages', 'data'), 'notifysuccess'); echo "\n\n"; // To make sure that ob_flush() has the desired effect ob_flush(); } foreach ($contents as $content) { if (!$file = $fs->get_file($this->context->id, 'mod_data', 'content', $content->id, '/', $content->content)) { continue; } if ($thumbfile = $fs->get_file($this->context->id, 'mod_data', 'content', $content->id, '/', 'thumb_'.$content->content)) { $thumbfile->delete(); } core_php_time_limit::raise(300); // Might be slow! $this->update_thumbnail($content, $file); } } } return true; } function update_content($recordid, $value, $name='') { global $CFG, $DB, $USER; // Should always be available since it is set by display_add_field before initializing the draft area. $content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid)); if (!$content) { $content = (object)array('fieldid' => $this->field->id, 'recordid' => $recordid); $content->id = $DB->insert_record('data_content', $content); } $names = explode('_', $name); switch ($names[2]) { case 'file': $fs = get_file_storage(); file_save_draft_area_files($value, $this->context->id, 'mod_data', 'content', $content->id); $usercontext = context_user::instance($USER->id); $files = $fs->get_area_files( $this->context->id, 'mod_data', 'content', $content->id, 'itemid, filepath, filename', false); // We expect no or just one file (maxfiles = 1 option is set for the form_filemanager). if (count($files) == 0) { $content->content = null; } else { $file = array_values($files)[0]; if (count($files) > 1) { // This should not happen with a consistent database. Inform admins/developers about the inconsistency. debugging('more then one file found in mod_data instance {$this->data->id} picture field (field id: {$this->field->id}) area during update data record {$recordid} (content id: {$content->id})', DEBUG_NORMAL); } if ($file->get_imageinfo() === false) { $url = new moodle_url('/mod/data/edit.php', array('d' => $this->field->dataid)); redirect($url, get_string('invalidfiletype', 'error', $file->get_filename())); } $content->content = $file->get_filename(); $this->update_thumbnail($content, $file); } $DB->update_record('data_content', $content); break; case 'alttext': // only changing alt tag $content->content1 = clean_param($value, PARAM_NOTAGS); $DB->update_record('data_content', $content); break; default: break; } } function update_thumbnail($content, $file) { // (Re)generate thumbnail image according to the dimensions specified in the field settings. // If thumbnail width and height are BOTH not specified then no thumbnail is generated, and // additionally an attempted delete of the existing thumbnail takes place. $fs = get_file_storage(); $filerecord = [ 'contextid' => $file->get_contextid(), 'component' => $file->get_component(), 'filearea' => $file->get_filearea(), 'itemid' => $file->get_itemid(), 'filepath' => $file->get_filepath(), 'filename' => 'thumb_' . $file->get_filename(), 'userid' => $file->get_userid() ]; try { // This may fail for various reasons. $newwidth = isset($this->field->param4) ? (int) $this->field->param4 : null; $newheight = isset($this->field->param5) ? (int) $this->field->param5 : null; $fs->convert_image($filerecord, $file, $newwidth, $newheight, true); return true; } catch (Exception $e) { debugging($e->getMessage()); return false; } } /** * Here we export the text value of a picture field which is the filename of the exported picture. * * @param stdClass $record the record which is being exported * @return string the value which will be stored in the exported file for this field */ public function export_text_value(stdClass $record): string { return !empty($record->content) ? $record->content : ''; } /** * Specifies that this field type supports the export of files. * * @return bool true which means that file export is being supported by this field type */ public function file_export_supported(): bool { return true; } /** * Exports the file content for file export. * * @param stdClass $record the data content record the file belongs to * @return null|string The file content of the stored file or null if no file should be exported for this record */ public function export_file_value(stdClass $record): null|string { $file = $this->get_file($record->id); return $file ? $file->get_content() : null; } /** * Specifies that this field type supports the import of files. * * @return bool true which means that file import is being supported by this field type */ public function file_import_supported(): bool { return true; } /** * Provides the necessary code for importing a file when importing the content of a mod_data instance. * * @param int $contentid the id of the mod_data content record * @param string $filecontent the content of the file to import as string * @param string $filename the filename the imported file should get * @return void */ public function import_file_value(int $contentid, string $filecontent, string $filename): void { $filerecord = [ 'contextid' => $this->context->id, 'component' => 'mod_data', 'filearea' => 'content', 'itemid' => $contentid, 'filepath' => '/', 'filename' => $filename, ]; $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, $filecontent); $this->update_thumbnail(null, $file); } function file_ok($path) { return true; } /** * Custom notempty function * * @param string $value * @param string $name * @return bool */ function notemptyfield($value, $name) { global $USER; $names = explode('_', $name); if ($names[2] == 'file') { $usercontext = context_user::instance($USER->id); $fs = get_file_storage(); $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $value); return count($files) >= 2; } return false; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } public function get_field_params(): array { global $DB, $CFG; $data = parent::get_field_params(); $course = $DB->get_record('course', ['id' => $this->data->course]); $filesizes = get_max_upload_sizes($CFG->maxbytes, $course->maxbytes, 0, $this->field->param3); foreach ($filesizes as $value => $name) { if (!((isset($this->field->param3) && $value == $this->field->param3))) { $data['options'][] = ['name' => $name, 'value' => $value, 'selected' => 0]; } else { $data['options'][] = ['name' => $name, 'value' => $value, 'selected' => 1]; } } return $data; } } field/number/lang/en/datafield_number.php000064400000002233152265517740014456 0ustar00. /** * Strings for component 'datafield_number', language 'en', branch 'master' * * @package datafield * @subpackage number * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Number'; $string['fieldtypelabel'] = 'Number'; $string['privacy:metadata'] = 'The Number field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/number/pix/icon.svg000064400000001464152265517740011413 0ustar00 field/number/version.php000064400000002232152265517740011332 0ustar00. /** * @package datafield * @subpackage number * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_number'; // Full name of the plugin (used for diagnostics) field/number/classes/privacy/provider.php000064400000005467152265517740014626 0ustar00. /** * Privacy Subsystem implementation for datafield_number. * * @package datafield_number * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_number\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_number implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { if (preg_match("/^\d+$/", trim($fieldobj->param1))) { $defaultvalue->field['decimals'] = trim($fieldobj->param1); } writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/number/templates/number.mustache000064400000002463152265517740014163 0ustar00{{! 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 . }} {{! @template datafield_number/number The purpose of this template is to enable the user to define a number field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. Example context (json): { "name": "A name", "description": "A description", "required": true } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/number/field.class.php000064400000012370152265517740012040 0ustar00 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => 1233 + $recordid, 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function update_content($recordid, $value, $name='') { global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $value = trim($value); if (strlen($value) > 0) { $content->content = floatval($value); } else { $content->content = null; } if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content || $content->content === '') { return ''; } $number = $content->content; $decimals = trim($this->field->param1 ?? ''); // Only apply number formatting if param1 contains an integer number >= 0. if (preg_match("/^\d+$/", $decimals)) { $decimals = $decimals * 1; // Removes leading zeros (eg. '007' -> '7'; '00' -> '0'). $str = format_float($number, $decimals, true); } else { $str = $number; } return $str; } function display_search_field($value = '') { return '' . ''; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } // need to cast? function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_number_$i"; $varcharcontent = $DB->sql_compare_text("{$tablealias}.content"); return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name=>$value)); } function get_sort_sql($fieldname) { global $DB; return $DB->sql_cast_char2real($fieldname, true); } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { return strval($value) !== ''; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/menu/lang/en/datafield_menu.php000064400000002254152265517740013611 0ustar00. /** * Strings for component 'datafield_menu', language 'en', branch 'master' * * @package datafield * @subpackage menu * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Dropdown list'; $string['fieldtypelabel'] = 'Dropdown list'; $string['privacy:metadata'] = 'The Dropdown list field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/menu/pix/icon.svg000064400000001043152265517740011060 0ustar00 field/menu/version.php000064400000002227152265517740011012 0ustar00. /** * @package datafield * @subpackage menu * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_menu'; // Full name of the plugin (used for diagnostics) field/menu/classes/privacy/provider.php000064400000005425152265517740014274 0ustar00. /** * Privacy Subsystem implementation for datafield_menu. * * @package datafield_menu * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_menu\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_menu implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/menu/templates/menu.mustache000064400000003304152265517740013306 0ustar00{{! 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 . }} {{! @template datafield_menu/menu The purpose of this template is to enable the user to define a menu field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 string The field's options. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": "One\nTwo\nThree" } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/menu/field.class.php000064400000014772152265517740011524 0ustar00field->param1); $options = array_map('trim', $options); $selected = $options[$recordid % count($options)]; return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => $selected, 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $DB, $OUTPUT; if ($formdata) { $fieldname = 'field_' . $this->field->id; $content = $formdata->$fieldname; } else if ($recordid) { $content = $DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid)); $content = trim($content); } else { $content = ''; } $str = '
'; $options = array(); $rawoptions = explode("\n",$this->field->param1); foreach ($rawoptions as $option) { $option = trim($option); if (strlen($option) > 0) { $options[$option] = $option; } } $str .= ''; $str .= html_writer::select($options, 'field_'.$this->field->id, $content, array('' => get_string('menuchoose', 'data')), ['id' => 'field_'.$this->field->id, 'class' => 'mod-data-input form-select']); $str .= '
'; return $str; } function display_search_field($content = '') { global $CFG, $DB; $varcharcontent = $DB->sql_compare_text('content', 255); $sql = "SELECT DISTINCT $varcharcontent AS content FROM {data_content} WHERE fieldid=? AND content IS NOT NULL"; $usedoptions = array(); if ($used = $DB->get_records_sql($sql, array($this->field->id))) { foreach ($used as $data) { $value = $data->content; if ($value === '') { continue; } $usedoptions[$value] = $value; } } $options = array(); foreach (explode("\n",$this->field->param1) as $option) { $option = trim($option); if (!isset($usedoptions[$option])) { continue; } $options[$option] = $option; } if (!$options) { // oh, nothing to search for return ''; } $return = html_writer::label(get_string('fieldtypelabel', "datafield_" . $this->type), 'menuf_' . $this->field->id, false, array('class' => 'accesshide')); $return .= html_writer::select($options, 'f_'.$this->field->id, $content, array('' => get_string('menuchoose', 'data')), ['class' => 'form-select']); return $return; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_menu_$i"; $varcharcontent = $DB->sql_compare_text("{$tablealias}.content", 255); return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name=>$value)); } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { return strval($value) !== ''; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/textarea/lang/en/datafield_textarea.php000064400000003163152265517740015333 0ustar00. /** * Strings for component 'datafield_textarea', language 'en', branch 'master' * * @package datafield * @subpackage textarea * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['maxbytes'] = 'Maximum embedded file size (bytes)'; $string['maxbytes_desc'] = 'If set to zero will be unlimited by default'; $string['pluginname'] = 'Text area'; $string['fieldtypelabel'] = 'Text area'; $string['privacy:metadata'] = 'The Text area field component doesn\'t store any personal data; it uses tables defined in mod_data.'; $string['sample'] = '

This is a text area. It lets users enter long form text which spans over multiple lines.

You can choose the size of your text areas in rows and columns.

The text in a text area can be formatted, for example in bold, and also include links or images.

'; field/textarea/pix/icon.svg000064400000001065152265517740011735 0ustar00 field/textarea/version.php000064400000002236152265517740011663 0ustar00. /** * @package datafield * @subpackage textarea * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_textarea'; // Full name of the plugin (used for diagnostics) field/textarea/classes/privacy/provider.php000064400000006527152265517740015151 0ustar00. /** * Privacy Subsystem implementation for datafield_textarea. * * @package datafield_textarea * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_textarea\privacy; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_textarea implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $subcontext = [$recordobj->id, $contentobj->id]; $defaultvalue->content = writer::with_context($context) ->rewrite_pluginfile_urls($subcontext, 'mod_data', 'content', $contentobj->id, $defaultvalue->content); $defaultvalue->contentformat = $defaultvalue->content1; unset($defaultvalue->content1); $defaultvalue->field['autolink'] = transform::yesno($fieldobj->param1); $defaultvalue->field['rows'] = $fieldobj->param3; $defaultvalue->field['cols'] = $fieldobj->param2; if ($fieldobj->param5) { $defaultvalue->field['maxbytes'] = $fieldobj->param5; } writer::with_context($context)->export_data($subcontext, $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/textarea/templates/textarea.mustache000064400000005527152265517740015041 0ustar00{{! 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 . }} {{! @template datafield_textarea/textarea The purpose of this template is to enable the user to define a textarea field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param2 int The field's width in columns. * param3 int The field's height in rows. * param5 int The field's attached files maxsize. Example context (json): { "name": "A name", "description": "A description", "required": true, "param2": 60, "param3": 35, "param5": 4096 } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
{{#str}}columns, data{{/str}}
{{#str}}rows, data{{/str}}
{{#str}}maxbytes_desc, datafield_textarea{{/str}}
field/textarea/field.class.php000064400000036043152265517740012370 0ustar00dirroot.'/lib/filelib.php'); require_once($CFG->dirroot.'/repository/lib.php'); class data_field_textarea extends data_field_base { var $type = 'textarea'; /** * Prefix for the field name to split field id and the random part. * @var string */ protected const RND_PREFIX = 'xZx'; /** * priority for globalsearch indexing * * @var int */ protected static $priority = self::LOW_PRIORITY; /** * The field name for the content field. * * @var string */ protected $fieldname = null; /** * Returns a random field name for the content field. * * @return string */ protected function get_content_field_name() { if ($this->fieldname === null) { $this->fieldname = sprintf( 'field_%s_%s%s', $this->field->id, self::RND_PREFIX, bin2hex(random_bytes(5)) ); } return $this->fieldname; } /** * Check if the given field name is the content field that contains the actual data. * * @param string $name * @return bool */ protected function is_content_field(string $name): bool { return (strlen($name) === 13 && substr($name, 0, 3) === self::RND_PREFIX); } public function supports_preview(): bool { return true; } public function get_data_content_preview(int $recordid): stdClass { return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => get_string('sample', 'datafield_textarea'), 'content1' => 1, 'content2' => null, 'content3' => null, 'content4' => null, ]; } /** * Returns options for embedded files * * @return array */ private function get_options() { if (!isset($this->field->param5)) { $this->field->param5 = 0; } $options = array(); $options['trusttext'] = false; $options['forcehttps'] = false; $options['subdirs'] = false; $options['maxfiles'] = -1; $options['context'] = $this->context; $options['maxbytes'] = $this->field->param5; $options['changeformat'] = 0; $options['noclean'] = false; return $options; } function display_add_field($recordid = 0, $formdata = null) { global $CFG, $DB, $OUTPUT, $PAGE; $text = ''; $format = 0; $str = '
'; $str .= ''; editors_head_setup(); $options = $this->get_options(); $itemid = $this->field->id; $field = 'field_'.$itemid; if ($formdata) { $fieldname = 'field_' . $this->field->id . '_content1'; if (isset($formdata->$fieldname)) { $format = $formdata->$fieldname; } else { $format = file_get_unused_draft_itemid(); } $fieldname = 'field_' . $this->field->id . '_itemid'; if (isset($formdata->$fieldname)) { $draftitemid = clean_param($formdata->$fieldname, PARAM_INT); } else { $draftitemid = file_get_unused_draft_itemid(); } $fieldname = 'field_' . $this->field->id . '_' . self::RND_PREFIX; foreach (array_keys(get_object_vars($formdata)) as $prop) { if (strpos($prop, $fieldname) === 0) { $text = $formdata->$prop; break; } } } else if ($recordid && $content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid))) { $format = $content->content1; $text = clean_text($content->content, $format); $text = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_data', 'content', $content->id, $options, $text); } else { $draftitemid = file_get_unused_draft_itemid(); $format = editors_get_preferred_format(); } // get filepicker info // $fpoptions = array(); if ($options['maxfiles'] != 0 ) { $args = new stdClass(); // need these three to filter repositories list $args->accepted_types = array('web_image'); $args->return_types = (FILE_INTERNAL | FILE_EXTERNAL); $args->context = $this->context; $args->env = 'filepicker'; // advimage plugin $image_options = initialise_filepicker($args); $image_options->context = $this->context; $image_options->client_id = uniqid(); $image_options->maxbytes = $options['maxbytes']; $image_options->env = 'editor'; $image_options->itemid = $draftitemid; // moodlemedia plugin $args->accepted_types = array('video', 'audio'); $media_options = initialise_filepicker($args); $media_options->context = $this->context; $media_options->client_id = uniqid(); $media_options->maxbytes = $options['maxbytes']; $media_options->env = 'editor'; $media_options->itemid = $draftitemid; // advlink plugin $args->accepted_types = '*'; $link_options = initialise_filepicker($args); $link_options->context = $this->context; $link_options->client_id = uniqid(); $link_options->maxbytes = $options['maxbytes']; $link_options->env = 'editor'; $link_options->itemid = $draftitemid; // Subtitles, for multimedia. $args->accepted_types = ['.vtt']; $subtitleoptions = initialise_filepicker($args); $subtitleoptions->context = $this->context; $subtitleoptions->client_id = uniqid(); $subtitleoptions->maxbytes = $options['maxbytes']; $subtitleoptions->env = 'editor'; $subtitleoptions->itemid = $draftitemid; // H5P plugin. $args->accepted_types = ['h5p']; $h5poptions = initialise_filepicker($args); $h5poptions->context = $this->context; $h5poptions->client_id = uniqid(); $h5poptions->maxbytes = $options['maxbytes']; $h5poptions->env = 'editor'; $h5poptions->itemid = $draftitemid; $fpoptions['image'] = $image_options; $fpoptions['media'] = $media_options; $fpoptions['link'] = $link_options; $fpoptions['subtitle'] = $subtitleoptions; $fpoptions['h5p'] = $h5poptions; } $editor = editors_get_preferred_editor($format); $strformats = format_text_menu(); $formats = $editor->get_supported_formats(); foreach ($formats as $fid) { $formats[$fid] = $strformats[$fid]; } $editor->set_text($text); $editor->use_editor($this->get_content_field_name(), $options, $fpoptions); $str .= ''; $str .= '
'; $str .= '
'; $str .= '
'; $str .= ''; $str .= '
'; $str .= '
'; $str .= '
'; return $str; } function display_search_field($value = '') { return '' . ''; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_textarea_$i"; return array(" ({$tablealias}.fieldid = {$this->field->id} AND ".$DB->sql_like("{$tablealias}.content", ":$name", false).") ", array($name=>"%$value%")); } function print_after_form() { } function update_content($recordid, $value, $name='') { global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $names = explode('_', $name); if (!empty($names[2])) { if ($names[2] == 'itemid') { // the value will be retrieved by file_get_submitted_draft_itemid, do not need to save in DB return true; } else if ($this->is_content_field($names[2])) { $content->content = clean_param($value, PARAM_RAW_TRIMMED); } else { $content->{$names[2]} = clean_param($value, PARAM_NOTAGS); // content[1-4] } } else { $content->content = clean_param($value, PARAM_RAW_TRIMMED); } if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; } else { $content->id = $DB->insert_record('data_content', $content); if (!$content->id) { return false; } } if (!empty($content->content)) { $draftitemid = file_get_submitted_draft_itemid('field_'. $this->field->id. '_itemid'); $options = $this->get_options(); $content->content = file_save_draft_area_files($draftitemid, $this->context->id, 'mod_data', 'content', $content->id, $options, $content->content); } $rv = $DB->update_record('data_content', $content); return $rv; } /** * Display the content of the field in browse mode * * @param int $recordid * @param object $template * @return bool|string */ function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content || !isset($content->content)) { return ''; } $options = new stdClass(); if ($this->field->param1 == '1') { // We are autolinking this field, so disable linking within us. $options->filter = false; } $options->para = false; $str = file_rewrite_pluginfile_urls( $content->content, 'pluginfile.php', $this->context->id, 'mod_data', 'content', $content->id, $this->get_options() ); $str = format_text($str, $content->content1, $options); return '
' . $str . '
'; } /** * Whether this module support files * * @param string $relativepath * @return bool */ function file_ok($relativepath) { return true; } /** * Only look at the first item (second is format) * * @param string $value * @param string $name * @return bool */ function notemptyfield($value, $name) { $names = explode('_', $name); // Clean first. if (count($names) == 2) { // Don't assume that this is coming from a text editor with tags. return strval($value) !== ''; } if ($this->is_content_field($names[2])) { return strval($value) !== ''; } return false; } /** * Returns the presentable string value for a field content. * * The returned string should be plain text. * * @param stdClass $content * @return string */ public static function get_content_value($content) { return content_to_text($content->content, $content->content1); } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/date/lang/en/datafield_date.php000064400000002221152265517740013525 0ustar00. /** * Strings for component 'datafield_date', language 'en', branch 'master' * * @package datafield * @subpackage date * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Date'; $string['fieldtypelabel'] = 'Date'; $string['privacy:metadata'] = 'The Date field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/date/pix/icon.svg000064400000001037152265517740011034 0ustar00 field/date/version.php000064400000002227152265517740010763 0ustar00. /** * @package datafield * @subpackage date * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_date'; // Full name of the plugin (used for diagnostics) field/date/classes/privacy/provider.php000064400000005423152265517740014243 0ustar00. /** * Privacy Subsystem implementation for datafield_date. * * @package datafield_date * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_date\privacy; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_date implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->content = transform::date($defaultvalue->content); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/date/templates/date.mustache000064400000002274152265517740013235 0ustar00{{! 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 . }} {{! @template datafield_date/date The purpose of this template is to enable the user to define a date field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. Example context (json): { "name": "A name", "description": "A description" } }}
{{> mod_data/fields/basicfields }}
field/date/field.class.php000064400000022346152265517740011471 0ustar00 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => (string) time(), 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $DB, $OUTPUT; if ($formdata) { $fieldname = 'field_' . $this->field->id . '_day'; $day = $formdata->$fieldname; $fieldname = 'field_' . $this->field->id . '_month'; $month = $formdata->$fieldname; $fieldname = 'field_' . $this->field->id . '_year'; $year = $formdata->$fieldname; $calendartype = \core_calendar\type_factory::get_calendar_instance(); $gregoriandate = $calendartype->convert_to_gregorian($year, $month, $day); $content = make_timestamp( $gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute'], 0, 0, false); } else if ($recordid) { $content = (int)$DB->get_field('data_content', 'content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid)); } else { $content = time(); } $str = '
'; $dayselector = html_writer::select_time( type: 'days', name: "field_{$this->field->id}_day", currenttime: $content, timezone: 0, ); $monthselector = html_writer::select_time( type: 'months', name: "field_{$this->field->id}_month", currenttime: $content, timezone: 0, ); $yearselector = html_writer::select_time( type: 'years', name: "field_{$this->field->id}_year", currenttime: $content, timezone: 0, ); $str .= $dayselector . $monthselector . $yearselector; $str .= '
'; return $str; } // Enable the following three functions once core API issues have been addressed. /** * Display the search field in advanced search page * @param mixed $value * @return string * @throws coding_exception */ public function display_search_field($value = null) { $currenttime = time(); $selectors = html_writer::select_time('days', 'f_' . $this->field->id . '_d', $value['timestamp'] ?? $currenttime) . html_writer::select_time('months', 'f_' . $this->field->id . '_m', $value['timestamp'] ?? $currenttime) . html_writer::select_time('years', 'f_' . $this->field->id . '_y', $value['timestamp'] ?? $currenttime); $datecheck = html_writer::checkbox('f_' . $this->field->id . '_z', 1, $value['usedate'] ?? 0); $str = '
' . $selectors . ' ' . $datecheck . ' ' . get_string('usedate', 'data') . '
'; return $str; } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_date_$i"; $varcharcontent = $DB->sql_compare_text("{$tablealias}.content"); return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name => $value['timestamp'])); } public function parse_search_field($defaults = null) { $paramday = 'f_'.$this->field->id.'_d'; $parammonth = 'f_'.$this->field->id.'_m'; $paramyear = 'f_'.$this->field->id.'_y'; $paramusedate = 'f_'.$this->field->id.'_z'; if (empty($defaults[$paramday])) { // One empty means the other ones are empty too. $defaults = array($paramday => 0, $parammonth => 0, $paramyear => 0, $paramusedate => 0); } $day = optional_param($paramday, $defaults[$paramday], PARAM_INT); $month = optional_param($parammonth, $defaults[$parammonth], PARAM_INT); $year = optional_param($paramyear, $defaults[$paramyear], PARAM_INT); $usedate = optional_param($paramusedate, $defaults[$paramusedate], PARAM_INT); $data = array(); if (!empty($day) && !empty($month) && !empty($year) && $usedate == 1) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $gregoriandate = $calendartype->convert_to_gregorian($year, $month, $day); $data['timestamp'] = make_timestamp( $gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute'], 0, 0, false); $data['usedate'] = 1; return $data; } else { return 0; } } function update_content($recordid, $value, $name='') { global $DB; $names = explode('_',$name); $name = $names[2]; // day month or year $this->$name = $value; if ($this->day and $this->month and $this->year) { // All of them have been collected now $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $calendartype = \core_calendar\type_factory::get_calendar_instance(); $gregoriandate = $calendartype->convert_to_gregorian($this->year, $this->month, $this->day); $content->content = make_timestamp( $gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute'], 0, 0, false); if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } } function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content || empty($content->content)) { return ''; } return userdate($content->content, get_string('strftimedate'), 0); } function get_sort_sql($fieldname) { global $DB; return $DB->sql_cast_char2real($fieldname, true); } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/checkbox/lang/en/datafield_checkbox.php000064400000002253152265517740015254 0ustar00. /** * Strings for component 'datafield_checkbox', language 'en', branch 'master' * * @package datafield * @subpackage checkbox * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Checkboxes'; $string['fieldtypelabel'] = 'Checkboxes'; $string['privacy:metadata'] = 'The Checkboxes field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/checkbox/pix/icon.svg000064400000001147152265517740011707 0ustar00 field/checkbox/version.php000064400000002236152265517740011634 0ustar00. /** * @package datafield * @subpackage checkbox * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_checkbox'; // Full name of the plugin (used for diagnostics) field/checkbox/classes/privacy/provider.php000064400000005555152265517740015122 0ustar00. /** * Privacy Subsystem implementation for datafield_checkbox. * * @package datafield_checkbox * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_checkbox\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_checkbox implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY); $defaultvalue->content = explode('##', $defaultvalue->content); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/checkbox/templates/checkbox.mustache000064400000003450152265517740014754 0ustar00{{! 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 . }} {{! @template datafield_checkbox/checkbox The purpose of this template is to enable the user to define a checkbox field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 string The field's options. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": "One\nTwo\nThree" } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/checkbox/field.class.php000064400000025646152265517740012350 0ustar00field->param1); $options = array_map('trim', $options); $selected = $options[$recordid % count($options)]; return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => $selected, 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $DB, $OUTPUT; if ($formdata) { $fieldname = 'field_' . $this->field->id; $content = $formdata->$fieldname ?? []; } else if ($recordid) { $content = $DB->get_field('data_content', 'content', ['fieldid' => $this->field->id, 'recordid' => $recordid]); $content = explode('##', $content ?? ''); } else { $content = []; } $str = '
'; $str .= '
'.s($this->field->name); if ($this->field->required) { $str .= '$nbsp;' . get_string('requiredelement', 'form'); $str .= ''; $image = $OUTPUT->pix_icon('req', get_string('requiredelement', 'form')); $str .= html_writer::div($image, 'inline-req'); } else { $str .= ''; } $i = 0; foreach (explode("\n", $this->field->param1) as $checkbox) { $checkbox = trim($checkbox); if ($checkbox === '') { continue; // skip empty lines } $str .= ''; $str .= 'field->id.'_'.$i.'">'.$checkbox.'
'; $i++; } $str .= '
'; $str .= '
'; return $str; } function display_search_field($value='') { global $CFG, $DB; if (is_array($value)) { $content = $value['checked']; $allrequired = $value['allrequired'] ? true : false; } else { $content = array(); $allrequired = false; } $str = ''; $found = false; $marginclass = ['class' => 'me-1']; foreach (explode("\n",$this->field->param1) as $checkbox) { $checkbox = trim($checkbox); if (in_array($checkbox, $content)) { $str .= html_writer::checkbox('f_'.$this->field->id.'[]', s($checkbox), true, $checkbox, $marginclass); } else { $str .= html_writer::checkbox('f_'.$this->field->id.'[]', s($checkbox), false, $checkbox, $marginclass); } $str .= html_writer::empty_tag('br'); $found = true; } if (!$found) { return ''; } $requiredstr = get_string('selectedrequired', 'data'); $str .= html_writer::checkbox('f_'.$this->field->id.'_allreq', null, $allrequired, $requiredstr, $marginclass); return $str; } public function parse_search_field($defaults = null) { $paramselected = 'f_'.$this->field->id; $paramallrequired = 'f_'.$this->field->id.'_allreq'; if (empty($defaults[$paramselected])) { // One empty means the other ones are empty too. $defaults = array($paramselected => array(), $paramallrequired => 0); } $selected = optional_param_array($paramselected, $defaults[$paramselected], PARAM_NOTAGS); $allrequired = optional_param($paramallrequired, $defaults[$paramallrequired], PARAM_BOOL); if (empty($selected)) { // no searching return ''; } return array('checked'=>$selected, 'allrequired'=>$allrequired); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_checkbox_{$i}_"; $params = array(); $varcharcontent = $DB->sql_compare_text("{$tablealias}.content", 255); $allrequired = $value['allrequired']; $selected = $value['checked']; if ($selected) { $conditions = array(); $j=0; foreach ($selected as $sel) { $j++; $xname = $name.$j; $likesel = str_replace('%', '\%', $sel); $likeselsel = str_replace('_', '\_', $likesel); $conditions[] = "({$tablealias}.fieldid = {$this->field->id} AND ({$varcharcontent} = :{$xname}a OR {$tablealias}.content LIKE :{$xname}b OR {$tablealias}.content LIKE :{$xname}c OR {$tablealias}.content LIKE :{$xname}d))"; $params[$xname.'a'] = $sel; $params[$xname.'b'] = "$likesel##%"; $params[$xname.'c'] = "%##$likesel"; $params[$xname.'d'] = "%##$likesel##%"; } if ($allrequired) { return array(" (".implode(" AND ", $conditions).") ", $params); } else { return array(" (".implode(" OR ", $conditions).") ", $params); } } else { return array(" ", array()); } } function update_content($recordid, $value, $name='') { global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $content->content = $this->format_data_field_checkbox_content($value); if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content || empty($content->content)) { return ''; } $options = explode("\n", $this->field->param1); $options = array_map('trim', $options); $contentarray = explode('##', $content->content); $str = ''; foreach ($contentarray as $line) { if (!in_array($line, $options)) { // Hmm, looks like somebody edited the field definition. continue; } $str .= $line . "
\n"; } return $str; } function format_data_field_checkbox_content($content) { if (!is_array($content)) { return NULL; } $options = explode("\n", $this->field->param1); $options = array_map('trim', $options); $vals = array(); foreach ($content as $key=>$val) { if ($key === 'xxx') { continue; } if (!in_array($val, $options)) { continue; } $vals[] = $val; } if (empty($vals)) { return NULL; } return implode('##', $vals); } /** * Check whether any boxes in the checkbox where checked. * * @param mixed $value The submitted values * @param mixed $name * @return bool */ function notemptyfield($value, $name) { $found = false; foreach ($value as $checkboxitem) { if (strval($checkboxitem) !== '') { $found = true; break; } } return $found; } /** * Returns the presentable string value for a field content. * * The returned string should be plain text. * * @param stdClass $content * @return string */ public static function get_content_value($content) { $arr = explode('##', $content->content); $strvalue = ''; foreach ($arr as $a) { $strvalue .= $a . ' '; } return trim($strvalue, "\r\n "); } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/latlong/lang/en/datafield_latlong.php000064400000002254152265517740015001 0ustar00. /** * Strings for component 'datafield_latlong', language 'en', branch 'master' * * @package datafield * @subpackage latlong * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Coordinates'; $string['fieldtypelabel'] = 'Coordinates'; $string['privacy:metadata'] = 'The Coordinates field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/latlong/pix/icon.svg000064400000002105152265517740011554 0ustar00 field/latlong/version.php000064400000002170152265517740011503 0ustar00. /** * @package datafield * @subpackage latlong * @copyright 2006 Dan Stowell * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_latlong'; // Full name of the plugin (used for diagnostics) field/latlong/classes/privacy/provider.php000064400000006363152265517740014772 0ustar00. /** * Privacy Subsystem implementation for datafield_latlong. * * @package datafield_latlong * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_latlong\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_latlong implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->field['services'] = explode(',', $fieldobj->param1); if ($fieldobj->param2 > 0) { $defaultvalue->field['label'] = 'Content of field '.$fieldobj->param2; } else if ($fieldobj->param2 == -2) { $defaultvalue->field['label'] = 'lattitude/longitude'; } else { $defaultvalue->field['label'] = 'item #'; } $defaultvalue->lattitude = $contentobj->content; $defaultvalue->longitude = $contentobj->content1; unset($defaultvalue->content); unset($defaultvalue->content1); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/latlong/templates/latlong.mustache000064400000007572152265517740014511 0ustar00{{! 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 . }} {{! @template datafield_latlong/latlong The purpose of this template is to enable the user to define a latlong field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * latlonglinkservices array The field's Link out services. * otherfields array The field's Other fields. * otherfieldsoptgroups array The field's Other fields optgroups. * dataid int The field's dataid. * fieldid int The field's fieldid. Example context (json): { "name": "A name", "description": "A description", "required": true, "latlonglinkservices": [ { "name": "Google Maps", "selected": true }, { "name": "Google Earth", "selected": false } ], "latlonglinkservicessize": 2, "otherfields": [ { "name": "Entry #", "value": "-1", "selected": false }, { "name": "Latitude/Longitude", "value": "-2", "selected": false } ], "otherfieldsoptgroups": [ { "label": "Other fields:", "options": [ { "name": "Field A", "value": "6", "selected": false }, { "name": "Field B", "value": "7", "selected": true } ] } ], "dataid": 6, "fieldid": 0 } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
{{#fieldid}} {{/fieldid}}
field/latlong/field.class.php000064400000040746152265517740012220 0ustar00 "http://maps.google.com/maps?q=@lat@,+@long@&iwloc=A&hl=en", "Google Earth" => "@wwwroot@/mod/data/field/latlong/kml.php?d=@dataid@&fieldid=@fieldid@&rid=@recordid@", "Geabios" => "http://www.geabios.com/html/services/maps/PublicMap.htm?lat=@lat@&lon=@long@&fov=0.3&title=Moodle%20data%20item", "OpenStreetMap" => "http://www.openstreetmap.org/index.html?lat=@lat@&lon=@long@&zoom=11", "Multimap" => "http://www.multimap.com/map/browse.cgi?scale=200000&lon=@long@&lat=@lat@&icon=x" ); // Other map sources listed at http://kvaleberg.com/extensions/mapsources/index.php?params=51_30.4167_N_0_7.65_W_region:earth public function supports_preview(): bool { return true; } public function get_data_content_preview(int $recordid): stdClass { return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => 41.391205, 'content1' => 2.163873, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $CFG, $DB, $OUTPUT; $lat = ''; $long = ''; if ($formdata) { $fieldname = 'field_' . $this->field->id . '_0'; $lat = $formdata->$fieldname; $fieldname = 'field_' . $this->field->id . '_1'; $long = $formdata->$fieldname; } else if ($recordid) { if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $lat = $content->content; $long = $content->content1; } } $str = '
'; $str .= '
'.s($this->field->name).''; $str .= '
Title Type Author of resource Link
'; $classes = 'mod-data-input form-control-static'; $str .= ''; $str .= ''; $str .= '
'; $classes = 'mod-data-input form-control-static'; $str .= ''; $str .= 'field->id.'_0" value="'; $str .= s($lat).'" size="10" />°N
field->id.'_1" id="field_'.$this->field->id.'_1" value="'; $str .= s($long).'" size="10" />°E
'; $str .= ''; $str .= '
'; return $str; } function display_search_field($value = '') { global $CFG, $DB; $varcharlat = $DB->sql_compare_text('content'); $varcharlong= $DB->sql_compare_text('content1'); $latlongsrs = $DB->get_recordset_sql( "SELECT DISTINCT $varcharlat AS la, $varcharlong AS lo FROM {data_content} WHERE fieldid = ? ORDER BY $varcharlat, $varcharlong", array($this->field->id)); $options = array(); foreach ($latlongsrs as $latlong) { $latitude = format_float($latlong->la, 4); $longitude = format_float($latlong->lo, 4); if ($latitude && $longitude) { $options[$latlong->la . ',' . $latlong->lo] = $latitude . ' ' . $longitude; } } $latlongsrs->close(); $classes = array('class' => 'accesshide'); $return = html_writer::label(get_string('latlong', 'data'), 'menuf_'.$this->field->id, false, $classes); $classes = ['class' => 'form-select']; $return .= html_writer::select($options, 'f_'.$this->field->id, $value, array('' => get_string('menuchoose', 'data')), $classes); return $return; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name1 = "df_latlong1_$i"; $name2 = "df_latlong2_$i"; $varcharlat = $DB->sql_compare_text("{$tablealias}.content"); $varcharlong= $DB->sql_compare_text("{$tablealias}.content1"); $latlong[0] = ''; $latlong[1] = ''; $latlong = explode (',', $value, 2); return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharlat = :$name1 AND $varcharlong = :$name2) ", array($name1=>$latlong[0], $name2=>$latlong[1])); } function display_browse_field($recordid, $template) { global $CFG; $content = $this->get_data_content($recordid); if (!$content) { return ''; } $lat = $content->content; if (strlen($lat ?? '') < 1) { return ''; } $long = $content->content1; if (strlen($long ?? '') < 1) { return ''; } // We use format_float to display in the regional format. if ($lat < 0) { $compasslat = format_float(-$lat, 4) . '°S'; } else { $compasslat = format_float($lat, 4) . '°N'; } if ($long < 0) { $compasslong = format_float(-$long, 4) . '°W'; } else { $compasslong = format_float($long, 4) . '°E'; } // Now let's create the jump-to-services link. $servicesshown = explode(',', $this->field->param1); // These are the different things that can be magically inserted into URL schemes. $urlreplacements = array( '@lat@' => $lat, '@long@' => $long, '@wwwroot@' => $CFG->wwwroot, '@contentid@' => $content->id, '@dataid@' => $this->data->id, '@courseid@' => $this->data->course, '@fieldid@' => $content->fieldid, '@recordid@' => $content->recordid, ); if (count($servicesshown) == 1 && $servicesshown[0]) { $str = " $compasslat $compasslong"; } else if (count($servicesshown) > 1) { $str = '
'; $str .= "$compasslat, $compasslong\n"; $str .= ""; $str .= ''; $str .= '
'; } else { $str = "$compasslat, $compasslong"; } return $str; } function update_content_import($recordid, $value, $name='') { $values = explode(" ", $value, 2); foreach ($values as $index => $value) { $this->update_content($recordid, $value, $name . '_' . $index); } } function update_content($recordid, $value, $name='') { global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; // When updating these values (which might be region formatted) we should format // the float to allow for a consistent float format in the database. $value = unformat_float($value); $value = trim($value ?? ''); if (strlen($value) > 0) { $value = floatval($value); } else { $value = null; } $names = explode('_', $name); switch ($names[2]) { case 0: // update lat $content->content = $value; break; case 1: // update long $content->content1 = $value; break; default: break; } if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } function get_sort_sql($fieldname) { global $DB; return $DB->sql_cast_char2real($fieldname, true); } function export_text_value($record) { // The content here is from the database and does not require location formating. return sprintf('%01.4f', $record->content) . ' ' . sprintf('%01.4f', $record->content1); } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { return isset($value) && !($value == ''); } /** * Validate values for this field. * Both the Latitude and the Longitude fields need to be filled in. * * @param array $values The entered values for the lat. and long. * @return string|bool Error message or false. */ public function field_validation($values) { $valuecount = 0; // The lat long class has two values that need to be checked. foreach ($values as $value) { if (isset($value->value) && !($value->value == '')) { $valuecount++; } } // If we have nothing filled in or both filled in then everything is okay. if ($valuecount == 0 || $valuecount == 2) { return false; } // If we get here then only one field has been filled in. return get_string('latlongboth', 'data'); } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } public function get_field_params(): array { global $DB; $data = parent::get_field_params(); $data['dataid'] = $this->data->id; if (isset($this->field->param1)) { $data["param1"] = $this->field->param1; if (isset($this->linkoutservices)) { $serviceschosen = explode(',', htmlspecialchars($this->field->param1)); foreach ($this->linkoutservices as $servicename => $serviceurl) { $servicename = htmlspecialchars($servicename); $data['latlonglinkservices'][] = [ 'name' => $servicename, 'selected' => in_array($servicename, $serviceschosen), ]; unset($serviceschosen[$servicename]); } $data['latlonglinkservicessize'] = count($this->linkoutservices); } } $data["otherfields"][] = [ 'value' => -1, 'name' => get_string('entry', 'data') . " #", 'selected' => $this->field->param2 == -1, ]; $data['otherfields'][] = [ 'value' => -2, 'name' => get_string('latitude', 'data') . "/" . get_string('longitude', 'data'), 'selected' => $this->field->param2 == -2, ]; // Fetch all "suitable" other fields that exist for this database. $textfields = $DB->get_records('data_fields', ['dataid' => $this->data->id, 'type' => 'text']); if (count($textfields) > 0) { $data['otherfieldsoptgroups']['label'] = get_string('latlongotherfields', 'data') . ":"; foreach ($textfields as $textfield) { $data['otherfieldsoptgroups']['options'][] = [ 'value' => $textfield->id, 'name' => $textfield->name, 'selected' => $this->field->param2 == $textfield->id, ]; } } if (isset($data['otherfieldsoptgroups'])) { $data['otherfields'][] = $data['otherfieldsoptgroups']; } if (isset($this->field->id)) { $data['fieldid'] = $this->field->id; } return $data; } } field/latlong/kml.php000064400000014131152265517740010601 0ustar00. // A lot of this initial stuff is copied from mod/data/view.php require_once('../../../../config.php'); require_once('../../lib.php'); // Optional params: row id "rid" - if set then export just one, otherwise export all $d = required_param('d', PARAM_INT); // database id $fieldid = required_param('fieldid', PARAM_INT); // field id $rid = optional_param('rid', 0, PARAM_INT); //record id $url = new moodle_url('/mod/data/field/latlong/kml.php', array('d'=>$d, 'fieldid'=>$fieldid)); if ($rid !== 0) { $url->param('rid', $rid); } $PAGE->set_url($url); if ($rid) { if (! $record = $DB->get_record('data_records', array('id'=>$rid))) { throw new \moodle_exception('invalidrecord', 'data'); } if (! $data = $DB->get_record('data', array('id'=>$record->dataid))) { throw new \moodle_exception('invalidid', 'data'); } if (! $course = $DB->get_record('course', array('id'=>$data->course))) { throw new \moodle_exception('coursemisconf'); } if (! $cm = get_coursemodule_from_instance('data', $data->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $field = $DB->get_record('data_fields', array('id'=>$fieldid))) { throw new \moodle_exception('invalidfieldid', 'data'); } if (! $field->type == 'latlong') { // Make sure we're looking at a latlong data type! throw new \moodle_exception('invalidfieldtype', 'data'); } if (! $content = $DB->get_record('data_content', array('fieldid'=>$fieldid, 'recordid'=>$rid))) { throw new \moodle_exception('nofieldcontent', 'data'); } } else { // We must have $d if (! $data = $DB->get_record('data', array('id'=>$d))) { throw new \moodle_exception('invalidid', 'data'); } if (! $course = $DB->get_record('course', array('id'=>$data->course))) { throw new \moodle_exception('coursemisconf'); } if (! $cm = get_coursemodule_from_instance('data', $data->id, $course->id)) { throw new \moodle_exception('invalidcoursemodule'); } if (! $field = $DB->get_record('data_fields', array('id'=>$fieldid))) { throw new \moodle_exception('invalidfieldid', 'data'); } if (! $field->type == 'latlong') { // Make sure we're looking at a latlong data type! throw new \moodle_exception('invalidfieldtype', 'data'); } $record = NULL; } require_course_login($course, true, $cm); $context = context_module::instance($cm->id); // If we have an empty Database then redirect because this page is useless without data. if (has_capability('mod/data:managetemplates', $context)) { if (!$DB->record_exists('data_fields', array('dataid'=>$data->id))) { // Brand new database! redirect($CFG->wwwroot.'/mod/data/field.php?d='.$data->id); // Redirect to field entry } } //header('Content-type: text/plain'); // This is handy for debug purposes to look at the KML in the browser header('Content-type: application/vnd.google-earth.kml+xml kml'); header('Content-Disposition: attachment; filename="moodleearth-'.$d.'-'.$rid.'-'.$fieldid.'.kml"'); echo data_latlong_kml_top(); if($rid) { // List one single item $pm = new stdClass(); $pm->name = data_latlong_kml_get_item_name($content, $field); $pm->description = "<a href='$CFG->wwwroot/mod/data/view.php?d=$d&rid=$rid'>Item #$rid</a> in Moodle data activity"; $pm->long = $content->content1; $pm->lat = $content->content; echo data_latlong_kml_placemark($pm); } else { // List all items in turn $contents = $DB->get_records('data_content', array('fieldid'=>$fieldid)); echo ''; foreach($contents as $content) { $pm->name = data_latlong_kml_get_item_name($content, $field); $pm->description = "<a href='$CFG->wwwroot/mod/data/view.php?d=$d&rid=$content->recordid'>Item #$content->recordid</a> in Moodle data activity"; $pm->long = $content->content1; $pm->lat = $content->content; echo data_latlong_kml_placemark($pm); } echo ''; } echo data_latlong_kml_bottom(); function data_latlong_kml_top() { return ' '; } function data_latlong_kml_placemark($pm) { return ' '.$pm->description.' '.$pm->name.' '.$pm->long.' '.$pm->lat.' 30500.8880792294568 46.72425699662645 0.0 0 1 relativeToGround '.$pm->long.','.$pm->lat.',50 '; } function data_latlong_kml_bottom() { return ''; } function data_latlong_kml_get_item_name($content, $field) { global $DB; // $field->param2 contains the user-specified labelling method $name = ''; if($field->param2 > 0) { $name = htmlspecialchars($DB->get_field('data_content', 'content', array('fieldid'=>$field->param2, 'recordid'=>$content->recordid)), ENT_COMPAT); }elseif($field->param2 == -2) { $name = $content->content . ', ' . $content->content1; } if($name=='') { // Done this way so that "item #" is the default that catches any problems $name = get_string('entry', 'data') . " #$content->recordid"; } return $name; } field/multimenu/lang/en/datafield_multimenu.php000064400000002301152265517740015730 0ustar00. /** * Strings for component 'datafield_multimenu', language 'en', branch 'master' * * @package datafield * @subpackage multimenu * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Multi-select list'; $string['fieldtypelabel'] = 'Multi-select list'; $string['privacy:metadata'] = 'The Multi-select list field component doesn\'t store any personal data, it uses tables defined in mod_data'; field/multimenu/pix/icon.svg000064400000001043152265517740012133 0ustar00 field/multimenu/version.php000064400000002240152265517740012060 0ustar00. /** * @package datafield * @subpackage multimenu * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_multimenu'; // Full name of the plugin (used for diagnostics) field/multimenu/classes/privacy/provider.php000064400000005731152265517740015347 0ustar00. /** * Privacy Subsystem implementation for datafield_multimenu. * * @package datafield_multimenu * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_multimenu\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_multimenu implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY); for ($i = 1; $i <= 10; $i++) { unset($defaultvalue->field['param' . $i]); } $defaultvalue->content = explode('##', $defaultvalue->content); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/multimenu/templates/multimenu.mustache000064400000003323152265517740015435 0ustar00{{! 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 . }} {{! @template datafield_multimenu/multimenu The purpose of this template is to enable the user to define a multimenu field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 string The field's options. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": "One\nTwo\nThree" } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/multimenu/field.class.php000064400000027557152265517740012604 0ustar00field->param1); $options = array_map('trim', $options); $selected = $options[$recordid % count($options)]; $selected .= '##' . $options[($recordid + 1) % count($options)]; return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => $selected, 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $DB, $OUTPUT; if ($formdata) { $fieldname = 'field_' . $this->field->id; if (isset($formdata->$fieldname)) { $content = $formdata->$fieldname; } else { $content = array(); } } else if ($recordid) { $contentfield = $DB->get_field('data_content', 'content', ['fieldid' => $this->field->id, 'recordid' => $recordid]); $content = explode('##', $contentfield ?? ''); } else { $content = array(); } $str = '
'; $str .= ''; // hidden field - needed for empty selection $str .= ''; $str .= ''; $str .= '
'; return $str; } function display_search_field($value = '') { global $CFG, $DB; if (is_array($value)){ $content = $value['selected']; $allrequired = $value['allrequired'] ? true : false; } else { $content = array(); $allrequired = false; } static $c = 0; $str = ''; $str .= ''; $str .= html_writer::checkbox('f_'.$this->field->id.'_allreq', null, $allrequired, get_string('selectedrequired', 'data'), array('class' => 'me-1')); return $str; } public function parse_search_field($defaults = null) { $paramselected = 'f_'.$this->field->id; $paramallrequired = 'f_'.$this->field->id.'_allreq'; if (empty($defaults[$paramselected])) { // One empty means the other ones are empty too. $defaults = array($paramselected => array(), $paramallrequired => 0); } $selected = optional_param_array($paramselected, $defaults[$paramselected], PARAM_NOTAGS); $allrequired = optional_param($paramallrequired, $defaults[$paramallrequired], PARAM_BOOL); if (empty($selected)) { // no searching return ''; } return array('selected'=>$selected, 'allrequired'=>$allrequired); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_multimenu_{$i}_"; $params = array(); $varcharcontent = $DB->sql_compare_text("{$tablealias}.content", 255); $allrequired = $value['allrequired']; $selected = $value['selected']; if ($selected) { $conditions = array(); $j=0; foreach ($selected as $sel) { $j++; $xname = $name.$j; $likesel = str_replace('%', '\%', $sel); $likeselsel = str_replace('_', '\_', $likesel); $conditions[] = "({$tablealias}.fieldid = {$this->field->id} AND ({$varcharcontent} = :{$xname}a OR {$tablealias}.content LIKE :{$xname}b OR {$tablealias}.content LIKE :{$xname}c OR {$tablealias}.content LIKE :{$xname}d))"; $params[$xname.'a'] = $sel; $params[$xname.'b'] = "$likesel##%"; $params[$xname.'c'] = "%##$likesel"; $params[$xname.'d'] = "%##$likesel##%"; } if ($allrequired) { return array(" (".implode(" AND ", $conditions).") ", $params); } else { return array(" (".implode(" OR ", $conditions).") ", $params); } } else { return array(" ", array()); } } function update_content($recordid, $value, $name='') { global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $content->content = $this->format_data_field_multimenu_content($value); if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } function format_data_field_multimenu_content($content) { if (!is_array($content)) { return NULL; } $options = explode("\n", $this->field->param1); $options = array_map('trim', $options); $vals = array(); foreach ($content as $key=>$val) { if ($key === 'xxx') { continue; } if (!in_array($val, $options)) { continue; } $vals[] = $val; } if (empty($vals)) { return NULL; } return implode('##', $vals); } function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content || empty($content->content)) { return ''; } $options = explode("\n", $this->field->param1); $options = array_map('trim', $options); $contentarray = explode('##', $content->content); $str = ''; foreach ($contentarray as $line) { if (!in_array($line, $options)) { // Hmm, looks like somebody edited the field definition. continue; } $str .= $line . "
\n"; } return $str; } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { unset($value['xxx']); return !empty($value); } /** * Returns the presentable string value for a field content. * * The returned string should be plain text. * * @param stdClass $content * @return string */ public static function get_content_value($content) { $arr = explode('##', $content->content); $strvalue = ''; foreach ($arr as $a) { $strvalue .= $a . ' '; } return trim($strvalue, "\r\n "); } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/radiobutton/lang/en/datafield_radiobutton.php000064400000002273152265517740016566 0ustar00. /** * Strings for component 'datafield_radiobutton', language 'en', branch 'master' * * @package datafield * @subpackage radiobutton * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Radio buttons'; $string['fieldtypelabel'] = 'Radio buttons'; $string['privacy:metadata'] = 'The Radio buttons field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/radiobutton/pix/icon.svg000064400000000631152265517740012450 0ustar00 field/radiobutton/version.php000064400000002244152265517740012377 0ustar00. /** * @package datafield * @subpackage radiobutton * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_radiobutton'; // Full name of the plugin (used for diagnostics) field/radiobutton/classes/privacy/provider.php000064400000005461152265517740015662 0ustar00. /** * Privacy Subsystem implementation for datafield_radiobutton. * * @package datafield_radiobutton * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_radiobutton\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_radiobutton implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->field['options'] = preg_split('/\s*\n\s*/', trim($fieldobj->param1), -1, PREG_SPLIT_NO_EMPTY); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/radiobutton/templates/radiobutton.mustache000064400000003331152265517740016262 0ustar00{{! 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 . }} {{! @template datafield_radiobutton/radiobutton The purpose of this template is to enable the user to define a radiobutton field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 string The field's options. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": "One\nTwo\nThree" } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/radiobutton/field.class.php000064400000015047152265517740013106 0ustar00field->param1); $options = array_map('trim', $options); $selected = $options[$recordid % count($options)]; return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => $selected, 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $CFG, $DB, $OUTPUT; if ($formdata) { $fieldname = 'field_' . $this->field->id; if (isset($formdata->$fieldname)) { $content = $formdata->$fieldname; } else { $content = ''; } } else if ($recordid) { $contentfield = $DB->get_field('data_content', 'content', ['fieldid' => $this->field->id, 'recordid' => $recordid]); $content = trim($contentfield ?? ''); } else { $content = ''; } $str = '
'; $str .= '
' . s($this->field->name); if ($this->field->required) { $str .= ' ' . get_string('requiredelement', 'form') . ''; $image = $OUTPUT->pix_icon('req', get_string('requiredelement', 'form')); $str .= html_writer::div($image, 'inline-req'); } else { $str .= ''; } $i = 0; $requiredstr = ''; $options = explode("\n", $this->field->param1); foreach ($options as $radio) { $radio = trim($radio); if ($radio === '') { continue; // skip empty lines } $str .= 'field->id.'_'.$i.'">'.$radio.'
'; $i++; } $str .= '
'; $str .= '
'; return $str; } function display_search_field($value = '') { global $CFG, $DB; $varcharcontent = $DB->sql_compare_text('content', 255); $used = $DB->get_records_sql( "SELECT DISTINCT $varcharcontent AS content FROM {data_content} WHERE fieldid=? ORDER BY $varcharcontent", array($this->field->id)); $options = array(); if(!empty($used)) { foreach ($used as $rec) { $options[$rec->content] = $rec->content; //Build following indicies from the sql. } } $return = html_writer::label(get_string('fieldtypelabel', "datafield_" . $this->type), 'menuf_' . $this->field->id, false, array('class' => 'accesshide')); $return .= html_writer::select($options, 'f_'.$this->field->id, $value, ['' => 'choosedots'], ['class' => 'form-select']); return $return; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_radiobutton_$i"; $varcharcontent = $DB->sql_compare_text("{$tablealias}.content", 255); return array(" ({$tablealias}.fieldid = {$this->field->id} AND $varcharcontent = :$name) ", array($name=>$value)); } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { return strval($value) !== ''; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/text/lang/en/datafield_text.php000064400000002317152265517740013651 0ustar00. /** * Strings for component 'datafield_text', language 'en', branch 'master' * * @package datafield * @subpackage text * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Short text'; $string['fieldtypelabel'] = 'Short text'; $string['privacy:metadata'] = 'The Short text field component doesn\'t store any personal data; it uses tables defined in mod_data.'; $string['sample'] = 'This is a short text'; field/text/pix/icon.svg000064400000001510152265517740011077 0ustar00 field/text/version.php000064400000002227152265517740011032 0ustar00. /** * @package datafield * @subpackage text * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_text'; // Full name of the plugin (used for diagnostics) field/text/classes/privacy/provider.php000064400000005237152265517740014315 0ustar00. /** * Privacy Subsystem implementation for datafield_text. * * @package datafield_text * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_text\privacy; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_text implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/text/templates/text.mustache000064400000002626152265517740013354 0ustar00{{! 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 . }} {{! @template datafield_text/text The purpose of this template is to enable the user to define a text field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 string The field's auto link value. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": 1 } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }} {{> datafield_text/autolink }}
field/text/templates/autolink.mustache000064400000002673152265517740014220 0ustar00{{! 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 . }} {{! @template datafield_text/autolink The purpose of this template is to enable the user to define the 'auto link' field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. Example context (json): { "param1": false } }}
field/text/field.class.php000064400000007563152265517740011544 0ustar00 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => get_string('sample', 'datafield_text'), 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_search_field($value = '') { return '' . ''; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_text_$i"; return array(" ({$tablealias}.fieldid = {$this->field->id} AND ".$DB->sql_like("{$tablealias}.content", ":$name", false).") ", array($name=>"%$value%")); } /** * Check if a field from an add form is empty * * @param mixed $value * @param mixed $name * @return bool */ function notemptyfield($value, $name) { return strval($value) !== ''; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/url/lang/en/datafield_url.php000064400000002310152265517740013276 0ustar00. /** * Strings for component 'datafield_url', language 'en', branch 'master' * * @package datafield * @subpackage url * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'URL'; $string['openlinkinnewwindow'] = 'Open link in new window'; $string['fieldtypelabel'] = 'URL'; $string['privacy:metadata'] = 'The URL field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/url/pix/icon.svg000064400000002026152265517740010720 0ustar00 field/url/version.php000064400000002226152265517740010647 0ustar00. /** * @package datafield * @subpackage url * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_url'; // Full name of the plugin (used for diagnostics) field/url/classes/privacy/provider.php000064400000006225152265517740014131 0ustar00. /** * Privacy Subsystem implementation for datafield_url. * * @package datafield_url * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_url\privacy; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use mod_data\privacy\datafield_provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_url implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { $defaultvalue->field['autolink'] = transform::yesno($fieldobj->param1); if (!empty($fieldobj->param2)) { $defaultvalue->field['forcetext'] = $fieldobj->param2; } $defaultvalue->field['blanktarget'] = transform::yesno($fieldobj->param3); $defaultvalue->url = $contentobj->content; $defaultvalue->text = $contentobj->content1; unset($defaultvalue->content); unset($defaultvalue->content1); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/url/templates/url.mustache000064400000005112152265517740013001 0ustar00{{! 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 . }} {{! @template datafield_url/url The purpose of this template is to enable the user to define a URL field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * param1 string The field's auto link value. * param3 string The field's open link in new window value. * param2 string The field's forced link name. Example context (json): { "name": "A name", "description": "A description", "required": true, "param1": 1, "param3": 1, "param2": "Link" } }}
{{> mod_data/fields/basicfields }}
{{> mod_data/fields/requiredfield }}
field/url/field.class.php000064400000024477152265517740011365 0ustar00 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => 'https://example.com', 'content1' => null, 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $CFG, $DB, $OUTPUT, $PAGE; require_once($CFG->dirroot. '/repository/lib.php'); // necessary for the constants used in args $args = new stdClass(); $args->accepted_types = '*'; $args->return_types = FILE_EXTERNAL; $args->context = $this->context; $args->env = 'url'; $fp = new file_picker($args); $options = $fp->options; $fieldid = 'field_url_'.$options->client_id; $straddlink = get_string('choosealink', 'repository'); $url = ''; $text = ''; if ($formdata) { $fieldname = 'field_' . $this->field->id . '_0'; $url = $formdata->$fieldname; $fieldname = 'field_' . $this->field->id . '_1'; if (isset($formdata->$fieldname)) { $text = $formdata->$fieldname; } } else if ($recordid) { if ($content = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $url = $content->content; $text = $content->content1; } } $autolinkable = !empty($this->field->param1) && empty($this->field->param2); $str = '
'; $label = ''; if ($autolinkable) { $str .= ''; $str .= ''; $str .= '
'; $str .= '' . get_string('url', 'data') . ':'; if (!empty($image)) { $str .= $image; } $str .= ''; $str .= $label; $str .= ''; $str .= '
' . get_string('text', 'data') . ':'; $str .= '
'; } else { // Just the URL field $str .= $label; $str .= 'repositories) > 0) { $str .= ''; } } // print out file picker //$str .= $OUTPUT->render($fp); $module = array('name'=>'data_urlpicker', 'fullpath'=>'/mod/data/data.js', 'requires'=>array('core_filepicker')); $PAGE->requires->js_init_call('M.data_urlpicker.init', array($options), true, $module); $str .= '
'; return $str; } function display_search_field($value = '') { return '' . ''; } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_url_$i"; return array(" ({$tablealias}.fieldid = {$this->field->id} AND ".$DB->sql_like("{$tablealias}.content", ":$name", false).") ", array($name=>"%$value%")); } function display_browse_field($recordid, $template) { $content = $this->get_data_content($recordid); if (!$content) { return ''; } $url = empty($content->content) ? '' : $content->content; $text = empty($content->content1) ? '' : $content->content1; if (empty($url) || ($url == 'http://')) { return ''; } if (!empty($this->field->param2)) { // Param2 forces the text to something. $text = $this->field->param2; } if ($this->field->param1) { // Param1 defines whether we want to autolink the url. $attributes = ['class' => 'data-field-link']; if ($this->field->param3) { // Param3 defines whether this URL should open in a new window. $attributes['target'] = '_blank'; $attributes['rel'] = 'noreferrer'; } if (empty($text)) { $text = $url; } $str = html_writer::link($url, $text, $attributes); } else { $str = $url; } return $str; } function update_content_import($recordid, $value, $name='') { $values = explode(" ", $value, 2); foreach ($values as $index => $value) { $this->update_content($recordid, $value, $name . '_' . $index); } } function update_content($recordid, $value, $name='') { global $DB; $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $names = explode('_', $name); switch ($names[2]) { case 0: // update link $content->content = clean_param($value, PARAM_URL); break; case 1: // add text $content->content1 = clean_param($value, PARAM_NOTAGS); break; default: break; } if (!empty($content->content) && (strpos($content->content, '://') === false) && (strpos($content->content, '/') !== 0)) { $content->content = 'http://' . $content->content; } if ($oldcontent = $DB->get_record('data_content', array('fieldid'=>$this->field->id, 'recordid'=>$recordid))) { $content->id = $oldcontent->id; return $DB->update_record('data_content', $content); } else { return $DB->insert_record('data_content', $content); } } function notemptyfield($value, $name) { $names = explode('_',$name); $value = clean_param($value, PARAM_URL); //clean first if ($names[2] == '0') { return ($value!='http://' && !empty($value)); } return false; } function export_text_value($record) { return $record->content . " " . $record->content1; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } } field/file/lang/en/datafield_file.php000064400000002221152265517740013531 0ustar00. /** * Strings for component 'datafield_file', language 'en', branch 'master' * * @package datafield * @subpackage file * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'File'; $string['fieldtypelabel'] = 'File'; $string['privacy:metadata'] = 'The File field component doesn\'t store any personal data; it uses tables defined in mod_data.'; field/file/pix/icon.svg000064400000001013152265517740011030 0ustar00 field/file/version.php000064400000002227152265517740010765 0ustar00. /** * @package datafield * @subpackage file * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'datafield_file'; // Full name of the plugin (used for diagnostics) field/file/classes/privacy/provider.php000064400000006110152265517740014237 0ustar00. /** * Privacy Subsystem implementation for datafield_file. * * @package datafield_file * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace datafield_file\privacy; use mod_data\privacy\datafield_provider; use core_privacy\local\request\writer; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for datafield_file implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider, datafield_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } /** * Exports data about one record in {data_content} table. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} * @param \stdClass $defaultvalue pre-populated default value that most of plugins will use */ public static function export_data_content($context, $recordobj, $fieldobj, $contentobj, $defaultvalue) { if ($fieldobj->param3) { $defaultvalue->field['maxbytes'] = $fieldobj->param3; } // Change file name to file path. $defaultvalue->file = writer::with_context($context) ->rewrite_pluginfile_urls([$recordobj->id, $contentobj->id], 'mod_data', 'content', $contentobj->id, '@@PLUGINFILE@@/' . $defaultvalue->content); unset($defaultvalue->content); writer::with_context($context)->export_data([$recordobj->id, $contentobj->id], $defaultvalue); } /** * Allows plugins to delete locally stored data. * * @param \context_module $context * @param \stdClass $recordobj record from DB table {data_records} * @param \stdClass $fieldobj record from DB table {data_fields} * @param \stdClass $contentobj record from DB table {data_content} */ public static function delete_data_content($context, $recordobj, $fieldobj, $contentobj) { } }field/file/templates/file.mustache000064400000003723152265517740013241 0ustar00{{! 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 . }} {{! @template datafield_file/file The purpose of this template is to enable the user to define a file field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. * required bool The field's requiredness. * filesizes array The field's file sizes. Example context (json): { "name": "A name", "description": "A description", "required": true, "filesizes": [ { "name": "Site upload limit (64MB)", "value": "0", "selected": true }, { "name": "64MB", "value": "67108864", "selected": false } ] } }}
{{> mod_data/fields/basicfields }} {{> mod_data/fields/requiredfield }}
field/file/field.class.php000064400000027761152265517740011501 0ustar00. /** * Class file field for database activity * * @package datafield_file * @copyright 2005 Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_field_file extends data_field_base { var $type = 'file'; public function supports_preview(): bool { return true; } public function get_data_content_preview(int $recordid): stdClass { return (object)[ 'id' => 0, 'fieldid' => $this->field->id, 'recordid' => $recordid, 'content' => 'samplefile.csv', 'content1' => 'samplefile.csv', 'content2' => null, 'content3' => null, 'content4' => null, ]; } function display_add_field($recordid = 0, $formdata = null) { global $CFG, $DB, $OUTPUT, $PAGE; // Necessary for the constants used in args. require_once($CFG->dirroot . '/repository/lib.php'); $itemid = null; // editing an existing database entry if ($formdata) { $fieldname = 'field_' . $this->field->id . '_file'; $itemid = clean_param($formdata->$fieldname, PARAM_INT); } else if ($recordid) { if (!$content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid))) { // Quickly make one now! $content = new stdClass(); $content->fieldid = $this->field->id; $content->recordid = $recordid; $id = $DB->insert_record('data_content', $content); $content = $DB->get_record('data_content', array('id' => $id)); } file_prepare_draft_area($itemid, $this->context->id, 'mod_data', 'content', $content->id); } else { $itemid = file_get_unused_draft_itemid(); } // database entry label $html = '
'; $html .= '
'.s($this->field->name); if ($this->field->required) { $html .= ' ' . get_string('requiredelement', 'form') . ''; $image = $OUTPUT->pix_icon('req', get_string('requiredelement', 'form')); $html .= html_writer::div($image, 'inline-req'); } else { $html .= ''; } // itemid element $html .= ''; $options = new stdClass(); $options->maxbytes = $this->field->param3; $options->maxfiles = 1; // Limit to one file for the moment, this may be changed if requested as a feature in the future. $options->itemid = $itemid; $options->accepted_types = '*'; $options->return_types = FILE_INTERNAL | FILE_CONTROLLED_LINK; $options->context = $PAGE->context; $fm = new form_filemanager($options); // Print out file manager. $output = $PAGE->get_renderer('core', 'files'); $html .= '
'; $html .= $output->render($fm); $html .= '
'; $html .= '
'; $html .= '
'; return $html; } function display_search_field($value = '') { return '' . ''; } function generate_sql($tablealias, $value) { global $DB; static $i=0; $i++; $name = "df_file_$i"; return array(" ({$tablealias}.fieldid = {$this->field->id} AND ".$DB->sql_like("{$tablealias}.content", ":$name", false).") ", array($name=>"%$value%")); } public function parse_search_field($defaults = null) { $param = 'f_'.$this->field->id; if (empty($defaults[$param])) { $defaults = array($param => ''); } return optional_param($param, $defaults[$param], PARAM_NOTAGS); } function get_file($recordid, $content=null) { global $DB; if (empty($content)) { if (!$content = $this->get_data_content($recordid)) { return null; } } $fs = get_file_storage(); if (!$file = $fs->get_file($this->context->id, 'mod_data', 'content', $content->id, '/', $content->content)) { return null; } return $file; } function display_browse_field($recordid, $template) { global $OUTPUT; $content = $this->get_data_content($recordid); if (!$content || empty($content->content)) { return ''; } $file = null; $url = ''; $name = !empty($content->content1) ? $content->content1 : $content->content; if ($this->preview) { $file = (object)[ 'filename' => $content->content, 'mimetype' => 'text/csv', ]; $name = $content->content; } else { $file = $this->get_file($recordid, $content); if (!$file) { return ''; } $fileurl = moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename() ); $url = $fileurl->out(); } $icon = $OUTPUT->pix_icon( file_file_icon($file), get_mimetype_description($file), 'moodle', ['width' => 16, 'height' => 16] ); return $icon . ' ' . s($name) . ''; } // content: "a##b" where a is the file name, b is the display name function update_content($recordid, $value, $name='') { global $CFG, $DB, $USER; $fs = get_file_storage(); // Should always be available since it is set by display_add_field before initializing the draft area. $content = $DB->get_record('data_content', array('fieldid' => $this->field->id, 'recordid' => $recordid)); if (!$content) { $content = (object)array('fieldid' => $this->field->id, 'recordid' => $recordid); $content->id = $DB->insert_record('data_content', $content); } file_save_draft_area_files($value, $this->context->id, 'mod_data', 'content', $content->id); $usercontext = context_user::instance($USER->id); $files = $fs->get_area_files($this->context->id, 'mod_data', 'content', $content->id, 'itemid, filepath, filename', false); // We expect no or just one file (maxfiles = 1 option is set for the form_filemanager). if (count($files) == 0) { $content->content = null; } else { $content->content = array_values($files)[0]->get_filename(); if (count($files) > 1) { // This should not happen with a consistent database. Inform admins/developers about the inconsistency. debugging('more then one file found in mod_data instance {$this->data->id} file field (field id: {$this->field->id}) area during update data record {$recordid} (content id: {$content->id})', DEBUG_NORMAL); } } $DB->update_record('data_content', $content); } /** * Here we export the text value of a file field which is the filename of the exported file. * * @param stdClass $record the record which is being exported * @return string the value which will be stored in the exported file for this field */ public function export_text_value(stdClass $record): string { return !empty($record->content) ? $record->content : ''; } /** * Specifies that this field type supports the export of files. * * @return bool true which means that file export is being supported by this field type */ public function file_export_supported(): bool { return true; } /** * Specifies that this field type supports the import of files. * * @return bool true which means that file import is being supported by this field type */ public function file_import_supported(): bool { return true; } /** * Provides the necessary code for importing a file when importing the content of a mod_data instance. * * @param int $contentid the id of the mod_data content record * @param string $filecontent the content of the file to import as string * @param string $filename the filename the imported file should get * @return void */ public function import_file_value(int $contentid, string $filecontent, string $filename): void { $filerecord = [ 'contextid' => $this->context->id, 'component' => 'mod_data', 'filearea' => 'content', 'itemid' => $contentid, 'filepath' => '/', 'filename' => $filename, ]; $fs = get_file_storage(); $fs->create_file_from_string($filerecord, $filecontent); } /** * Exports the file content for file export. * * @param stdClass $record the data content record the file belongs to * @return null|string The file content of the stored file or null if no file should be exported for this record */ public function export_file_value(stdClass $record): null|string { $file = $this->get_file($record->id); return $file ? $file->get_content() : null; } function file_ok($path) { return true; } /** * Custom notempty function * * @param string $value * @param string $name * @return bool */ function notemptyfield($value, $name) { global $USER; $names = explode('_', $name); if ($names[2] == 'file') { $usercontext = context_user::instance($USER->id); $fs = get_file_storage(); $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $value); return count($files) >= 2; } return false; } /** * Return the plugin configs for external functions. * * @return array the list of config parameters * @since Moodle 3.3 */ public function get_config_for_external() { // Return all the config parameters. $configs = []; for ($i = 1; $i <= 10; $i++) { $configs["param$i"] = $this->field->{"param$i"}; } return $configs; } public function get_field_params(): array { global $DB, $CFG; $data = parent::get_field_params(); $course = $DB->get_record('course', ['id' => $this->data->course]); $filesizes = get_max_upload_sizes($CFG->maxbytes, $course->maxbytes, 0, $this->field->param3); foreach ($filesizes as $value => $name) { if (!((isset($this->field->param3) && $value == $this->field->param3))) { $data['filesizes'][] = ['name' => $name, 'value' => $value, 'selected' => 0]; } else { $data['filesizes'][] = ['name' => $name, 'value' => $value, 'selected' => 1]; } } return $data; } } templates/defaulttemplate_asearchtemplate.mustache000064400000003767152265517740016732 0ustar00{{! 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 . }} {{! @template mod_data/defaulttemplate_asearchtemplate Default template for the advanced search view in the database activity. Context variables required for this template: * fields - The database fields to display Example context (json): { "fields": [ { "fieldname": "Field1", "fieldcontent": "[[Field1]]" }, { "fieldname": "Field2", "fieldcontent": "[[Field2]]" } ] } }}
{{#str}}authorfirstname, mod_data{{/str}}
##firstname##
{{#str}}authorlastname, mod_data{{/str}}
##lastname##
{{#fields}}
{{fieldname}}
{{{fieldcontent}}}
{{/fields}} {{#tags}}
{{#str}}tags{{/str}}
{{.}}
{{/tags}}
templates/template_editor_tools.mustache000064400000011212152265517740014711 0ustar00{{! 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 . }} {{! @template mod_data/template_editor_tools Tag tool box for the template editor in the mod_data. Example context (json): { "toolshelp": "Available tags", "hastools": true, "tools": [ { "name": "Fields", "tags": [ { "tag": "[[Checkme]]", "tagname": "Checkme - [[Checkme]]" }, { "tag": "[[Description]]", "tagname": "Description - [[Description]]" }, { "tag": "[[Name]]", "tagname": "Name - [[Name]]" } ] }, { "name": "Actions", "tags": [ { "tag": "##actionsmenu##", "tagname": "Actions menu - ##actionsmenu##" }, { "tag": "##edit##", "tagname": "Edit - ##edit##" }, { "tag": "##delete##", "tagname": "Delete - ##delete##" }, { "tag": "##approve##", "tagname": "Approve - ##approve##" }, { "tag": "##disapprove##", "tagname": "Undo approval - ##disapprove##" }, { "tag": "##export##", "tagname": "Export - ##export##" }, { "tag": "##more##", "tagname": "More - ##more##" }, { "tag": "##moreurl##", "tagname": "More URL - ##moreurl##" }, { "tag": "##delcheck##", "tagname": "Bulk delete checkbox - ##delcheck##" } ] }, { "name": "Other", "tags": [ { "tag": "##tags##", "tagname": "Tags - ##tags##" }, { "tag": "##timeadded##", "tagname": "Time added - ##timeadded##" }, { "tag": "##timemodified##", "tagname": "Time modified - ##timemodified##" }, { "tag": "##user##", "tagname": "User - ##user##" }, { "tag": "##userpicture##", "tagname": "User picture - ##userpicture##" }, { "tag": "##approvalstatus##", "tagname": "Approval status - ##approvalstatus##" }, { "tag": "##id##", "tagname": "Entry ID - ##id##" }, { "tag": "##comments##", "tagname": "Comments - ##comments##" } ] } ] } }} {{#hastools}}
{{{toolshelp}}}
{{/hastools}} templates/template_editor.mustache000064400000007777152265517740013516 0ustar00{{! 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 . }} {{! @template mod_data/template_editor Template editor in the database activity. Example context (json): { "title": "Defines browsing interface for multiple entries", "sesskey": "XXXXX", "disableeditor": true, "url": {}, "usehtmleditor": true, "toolbar": { "toolshelp": "Available tags help", "hastools": true, "tools": [ { "name": "Fields", "tags": [ { "tag": "[[Checkme]]", "tagname": "Checkme - [[Checkme]]" }, { "tag": "[[Description]]", "tagname": "Description - [[Description]]" }, { "tag": "[[Name]]", "tagname": "Name - [[Name]]" } ] } ] }, "editors": [ { "name": "Header", "fieldname": "listtemplateheader", "value": "" }, { "name": "Repeated entry", "fieldname": "listtemplate", "value": "Template content" }, { "name": "Footer", "fieldname": "listtemplatefooter", "value": "" } ] } }}
{{title}}
{{#toolbar}} {{> mod_data/template_editor_tools }} {{/toolbar}}
{{#editors}}
{{/editors}}
{{#disableeditor}}
{{/disableeditor}} {{< core/sticky_footer }} {{$ stickycontent }}
{{/ stickycontent }} {{/ core/sticky_footer }}
templates/defaulttemplate_singletemplate.mustache000064400000005226152265517740016575 0ustar00{{! 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 . }} {{! @template mod_data/defaulttemplate_singletemplate Default template for the single view page in the database activity. Context variables required for this template: * fields - The database fields to display Example context (json): { "fields": [ { "fieldname": "Field1", "fieldcontent": "[[Field1]]" }, { "fieldname": "Field2", "fieldcontent": "[[Field2]]" } ] } }}
##userpicture##
##user##
##timeadded##
{{#str}}datemodified, mod_data{{/str}} ##timemodified##
##actionsmenu##
##approvalstatus##

{{#fields}}
{{fieldname}}

{{{fieldcontent}}}

{{/fields}} ##otherfields## {{#tags}}
{{#str}}tags{{/str}}

{{.}}

{{/tags}}
templates/template_editor_resetmodal.mustache000064400000002231152265517740015711 0ustar00{{! 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 . }} {{! @template mod_data/template_editor_resetmodal Reset template modal content. Example context (json): { "resetallname": "resetallcheck" } }}

{{#str}} resettemplateconfirm, mod_data, {{templatename}} {{/str}}

templates/defaulttemplate_rsstemplate.mustache000064400000002750152265517740016122 0ustar00{{! 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 . }} {{! @template mod_data/defaulttemplate_rsstemplate Default template for the RSS page in the database activity. Context variables required for this template: * fields - The database fields to display Example context (json): { "fields": [ { "fieldname": "Field1", "fieldcontent": "[[Field1]]" }, { "fieldname": "Field2", "fieldcontent": "[[Field2]]" } ] } }}
{{#fields}}
{{fieldname}}

{{{fieldcontent}}}

{{/fields}}
templates/save_as_preset.mustache000064400000002433152265517740013320 0ustar00{{! 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 . }} {{! @template mod_data/save_as_preset Trigger button for the save as preset modal form. Classes required for JS: * none Data attributes required for JS: * All data attributes are required Context variables required for this template: * d - The database id. Example context (json): { "d": 1 } }} {{#js}} require(['mod_data/saveaspreset'], function(saveAsPreset) { saveAsPreset.init(); }); {{/js}} templates/fields_footer.mustache000064400000002210152265517740013132 0ustar00{{! 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 . }} {{! @template mod_data/fields_footer The mod_data fields sticky footer content. Example context (json): { "pageurl": "http://yoursite/mod/data/field.php" } }} {{< core/sticky_footer }} {{$ stickyclasses }} justify-content-start{{/ stickyclasses }} {{$ stickycontent }} {{/ stickycontent }} {{/ core/sticky_footer }} templates/fields_otherfields.mustache000064400000002271152265517740014153 0ustar00{{! 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 . }} {{! @template mod_data/fields_otherfields The ##otherfields## template. Example context (json): { "fieldname": "This is the field name", "fieldcontent": "This is the field name", "classes": "added_fields" } }}
{{#fields}}
{{fieldname}}

{{{fieldcontent}}}

{{/fields}}
templates/view_footer.mustache000064400000003050152265517740012641 0ustar00{{! 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 . }} {{! @template mod_data/view_footer The mod_data sticky footer content. Example context (json): { "selectall": "", "deleteselected": "", "pagination": "1, 2, 3...", "addentrybutton": { "disabled": false, "url": "#", "id": "test-id", "classes": "btn btn-link", "attributes": [ { "name": "title", "value": "Add entry" } ], "text": "Add" } } }} {{#selectall}} {{/selectall}} {{#addentrybutton}} {{/addentrybutton}} templates/zero_state.mustache000064400000006221152265517740012473 0ustar00{{! 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 . }} {{! @template mod_data/zero_state Form containing all database presets displayed within a table. Context variables required for this template: * noitemsimgurl - The image url. * importpresetbutton stdClass - Import preset single button to be rendered * createfieldbutton stdClass - Create a field action menu to be rendered * usepresetbutton stdClass - Use a preset single button to be rendered Example context (json): { "noitemsimgurl": "https://moodlesite/theme/image.php/boost/mod_data/1535727318/nofields", "title": "Title", "importpresetbutton": { "id" : "id1", "method" : "get", "url" : "#", "primary" : false, "tooltip" : "This is a tooltip", "label" : "Button1", "attributes": [ { "name": "data-attribute", "value": "no" } ] }, "createfieldbutton": { "id" : "id2", "method" : "get", "url" : "#", "primary" : true, "tooltip" : "This is a tooltip", "label" : "Button2", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] }, "usepresetbutton": { "id" : "id3", "method" : "get", "url" : "#", "primary" : false, "tooltip" : "This is a tooltip", "label" : "Button3", "attributes": [ { "name": "data-attribute", "value": "perhaps" } ] } } }}
{{{ title }}}
{{#intro}}

{{{ intro }}}

{{/intro}}
{{#createfieldbutton}} {{>core/action_menu}} {{/createfieldbutton}} {{#importpresetbutton}} {{>core/single_button}} {{/importpresetbutton}} {{#usepresetbutton}} {{>core/single_button}} {{/usepresetbutton}}
{{#js}} require(['mod_data/importpresets'], function(importPresetsModal) { importPresetsModal.init(); }); {{/js}}
templates/action_bar.mustache000064400000010137152265517740012416 0ustar00{{! 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 . }} {{! @template mod_data/action_bar General actions bar at the top of the pages in the database activity. Context variables required for this template: * d - The database instance id. * urlselect - The data object containing the required properties to render core/url_select. * fieldselect - The data object containing the required properties to render core/single_select. * saveaspreset - Whether to display the save as preset button (mod_data/save_as_preset). * exportpreset - The data object containing the required properties to render core/single_button. Example context (json): { "d": 1, "hasback": true, "backurl": "https://example.com", "backtitle": "Back", "title": "Some title", "urlselect": { "id": "url_select_test", "action": "https://example.com/post", "formid": "url_select_form", "sesskey": "sesskey", "classes": "urlselect", "label": "", "helpicon": false, "showbutton": null, "options": [ { "name": "Some name", "value": "/mod/data/someurl.php", "selected": false } ], "disabled": false, "title": null }, "extraurlselect": { "id": "extra_url_select_test", "action": "https://example.com/post", "formid": "extra_url_select_form", "sesskey": "sesskey", "classes": "urlselect", "label": "", "helpicon": false, "showbutton": null, "options": [ { "name": "Some name", "value": "/mod/data/someurl.php", "selected": false } ], "disabled": false, "title": null }, "buttons": { "id": "single_button_test", "method" : "post", "formid": "single_button_form", "url" : "https://example.com/post", "primary" : false, "tooltip" : null, "label" : "Label", "attributes": [] } } }}
{{#hasback}} {{/hasback}} {{#urlselect}} {{/urlselect}} {{#title}} {{/title}}
{{#extraurlselect}} {{/extraurlselect}} {{#buttons}} {{/buttons}}

templates/templates_action_bar.mustache000064400000006724152265517740014503 0ustar00{{! 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 . }} {{! @template mod_data/templates_action_bar Actions bar at the top of the template pages in the database activity. Context variables required for this template: * d - The database id. * selectmenu - The data object containing the required properties to render core/selectmenu. * actionsselect - The presets actions selector. Example context (json): { "d": 1, "selectmenu": { "name": "Templates tertiary navigation selector", "value": "opt", "baseid": "select-menu1", "selectedoption": "Single view template", "options": [ { "selected": false, "name": "Add entry template", "id": "select-menu-group1" }, { "selected": true, "name": "Single view template", "value": "opt2", "id": "select-menu-option2" } ] }, "actionsselect": { "classes": "moodle-actionmenu", "instance": "1", "triggerrole": "button", "primary": { "attributes": [ {"name": "id", "value": "action-menu-1"}, {"name": "role", "value": "menu"} ], "items": [ { "attributes": [ {"name": "role", "value": "menuitem"} ], "rawhtml": "Actions" } ] }, "secondary": { "attributes": [ {"name": "id", "value": "action-menu-1-menu"}, {"name": "role", "value": "menuitem"} ], "items": [ { "rawhtml": "Import preset" }, { "rawhtml": "Export preset" } ] } } } }}
{{#actionsselect}} {{/actionsselect}}

{{#js}} require(['mod_data/saveaspreset', 'mod_data/resetalltemplates'], function(saveAsPreset, resetAll) { saveAsPreset.init(); resetAll.init(); }); {{/js}} templates/view_noentries.mustache000064400000004605152265517740013360 0ustar00{{! 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 . }} {{! @template mod_data/view_noentries Form containing all database presets displayed within a table. Context variables required for this template: * noitemsimgurl - The image url. * importentriesbutton stdClass - Import entries single button to be rendered * addentrybutton stdClass - Add entry single button to be rendered Example context (json): { "noitemsimgurl": "https://moodlesite/theme/image.php/boost/mod_data/1535727318/nofields", "importentriesbutton": { "id" : "id1", "method" : "get", "url" : "#", "primary" : false, "tooltip" : "This is a tooltip", "label" : "Button1", "attributes": [ { "name": "data-attribute", "value": "no" } ] }, "addentrybutton": { "id" : "id2", "method" : "get", "url" : "#", "primary" : true, "tooltip" : "This is a tooltip", "label" : "Button2", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] } } }}
{{#str}} norecords, mod_data {{/str}}
{{#importentriesbutton}} {{> core/single_button }} {{/importentriesbutton}} {{#addentrybutton}} {{> core/single_button }} {{/addentrybutton}}
templates/presets.mustache000064400000011073152265517740012002 0ustar00{{! 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 . }} {{! @template mod_data/presets Form containing all database presets displayed within a table. Context variables required for this template: * formactionurl - The form action url. * id - The database course module id (cmid). * presets - List of presets containing id, name, fullname, shortname and actions. Example context (json): { "formactionurl": "http://www.example.com", "id": 1, "presets": [ { "id": 1, "cmid": 1, "name": "Image gallery", "shortname": "imagegallery", "fullname": "Image gallery", "description": "Use this preset to collect images", "userid": 0, "url": "http://www.example.com", "actions": [], "presetindex": 0 }, { "id": 2, "cmid": 2, "name": "Preset saved manually", "shortname": "Preset saved manually", "fullname": "Preset saved manually (admin)", "userid": 2, "url": "http://www.example.com", "actions": [], "presetindex": 1 } ] } }} {{#str}}presetshelp, mod_data{{/str}}
{{#str}}presets, mod_data{{/str}} {{#presets}} {{/presets}}
{{#str}}select{{/str}} {{#str}} name {{/str}} {{#str}} description {{/str}} {{#str}}actions{{/str}}
{{fullname}} {{description}} {{#actions}}
{{>core/action_menu}}
{{/actions}}
{{< core/sticky_footer }} {{$ stickycontent }} {{/ stickycontent }} {{/ core/sticky_footer }}
{{#js}} require(['mod_data/selectpreset'], function(selectPreset) { selectPreset.init(); }); require(['mod_data/editpreset'], function(editPreset) { editPreset.init(); }); require(['mod_data/importmappingdialogue'], function(importPreset) { importPreset.init(); }); {{/js}} templates/fields_mapping_body.mustache000064400000003311152265517740014307 0ustar00{{! 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 . }} {{! @template mod_data/fields_mapping_body Display a modal to decide on field mapping Example context (json): { "presetname": "Image gallery", "fieldstocreate": "A, B, C", "fieldstodelete": "A, B, C", "cancelbutton": "", "mapfieldsbutton": "", "applybutton": "" } }}
{{#fieldstocreate}}
{{#str}}mapping:fieldstocreate, mod_data, {{.}}{{/str}}
{{/fieldstocreate}} {{#fieldstoremove}}
{{#str}}mapping:fieldstodelete, mod_data, {{.}}{{/str}}
{{#fieldstocreate}}
{{#str}}mapping:warningmessagedeleteandcreate, mod_data{{/str}}
{{/fieldstocreate}} {{^fieldstocreate}}
{{#str}}mapping:warningmessagedelete, mod_data{{/str}}
{{/fieldstocreate}} {{/fieldstoremove}}
templates/view_action_bar.mustache000064400000007065152265517740013456 0ustar00{{! 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 . }} {{! @template mod_data/view_action_bar Actions bar at the top of the view pages in the database activity. Context variables required for this template: * urlselect - The data object containing the required properties to render core/url_select. * actionsselect - The presets actions selector. Example context (json): { "urlselect": { "id": "url_select_test", "action": "https://example.com/post", "formid": "url_select_form", "sesskey": "sesskey", "classes": "urlselect", "label": "", "helpicon": false, "showbutton": null, "options": [ { "name": "List view", "value": "/mod/data/someurl.php", "selected": false }, { "name": "Single view", "value": "/mod/data/someurl.php", "selected": true } ], "disabled": false, "title": null }, "actionsselect": { "classes": "moodle-actionmenu", "instance": "1", "triggerrole": "button", "primary": { "attributes": [ {"name": "id", "value": "action-menu-1"}, {"name": "role", "value": "menu"} ], "items": [ { "attributes": [ {"name": "role", "value": "menuitem"} ], "rawhtml": "Actions" } ] }, "secondary": { "attributes": [ {"name": "id", "value": "action-menu-1-menu"}, {"name": "role", "value": "menuitem"} ], "items": [ { "rawhtml": "Import entries" }, { "rawhtml": "Export entries" } ] } } } }}
{{#addentrybutton}} {{/addentrybutton}}
{{#urlselect}} {{/urlselect}}
{{#actionsselect}} {{/actionsselect}}

templates/fields/requiredfield.mustache000064400000002617152265517740014413 0ustar00{{! 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 . }} {{! @template mod_data/fields/requiredfield The purpose of this template is to enable the user to define the 'required' field. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "required": false } }}
templates/fields/basicfields.mustache000064400000003356152265517740014040 0ustar00{{! 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 . }} {{! @template mod_data/fields/basicfields The purpose of this template is to enable the user to define the two values which every field has. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * name string The field's name. * description string The field's description. Example context (json): { "name": "A name", "description": "A description" } }}
templates/preset_preview.mustache000064400000003562152265517740013364 0ustar00{{! 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 . }} {{! @template mod_data/preset_preview The preset preview. Example context (json): { "formactionurl": "#", "cmid": "42", "description": "Preset description", "preview": "

The preset preview HTML

", "userid": "0", "shortname": "imagegallery" } }}
{{{preview}}}
{{< core/sticky_footer }} {{$ stickycontent }}
{{/ stickycontent }} {{/ core/sticky_footer }} {{#js}} require(['mod_data/importmappingdialogue'], function(importPreset) { importPreset.init(); }); {{/js}} templates/presets_action_bar.mustache000064400000005222152265517740014162 0ustar00{{! 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 . }} {{! @template mod_data/presets_action_bar Actions bar at the top of the presets page in the database activity. Context variables required for this template: * actionsselect - The presets actions selector. * id - The database course module id (cmid). Example context (json): { "actionsselect": { "classes": "moodle-actionmenu", "instance": "1", "triggerrole": "button", "primary": { "attributes": [ {"name": "id", "value": "action-menu-1"}, {"name": "role", "value": "menu"} ], "items": [ { "attributes": [ {"name": "role", "value": "menuitem"} ], "rawhtml": "Actions" } ] }, "secondary": { "attributes": [ {"name": "id", "value": "action-menu-1-menu"}, {"name": "role", "value": "menuitem"} ], "items": [ { "rawhtml": "Import preset" }, { "rawhtml": "Export preset" } ] } }, "id" : 1 } }}

{{#str}}presets, mod_data{{/str}}

{{#actionsselect}} {{/actionsselect}}

{{#js}} require(['mod_data/saveaspreset', 'mod_data/importpresets'], (saveAsPreset, importPresets) => { saveAsPreset.init(); importPresets.init(); }); {{/js}} templates/fields_action_bar.mustache000064400000005570152265517740013751 0ustar00{{! 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 . }} {{! @template mod_data/fields_action_bar Actions bar at the top of the field pages in the database activity. Context variables required for this template: * d - The database id. * fieldselect - The data object containing the required properties to render core/single_select. Example context (json): { "tertiarytitle": "Manage fields", "fieldselect": { "id": "single_select_test", "name": "fieldselector", "method": "get", "action": "https://example.com/post", "formid": "single_select_form", "sesskey": "sesskey", "classes": "singleselect", "label": "", "helpicon": false, "options": [ { "name": "Some name", "value": "somevalue", "selected": false, "optgroup": false } ], "disabled": false, "title": null }, "hasback" : true, "backurl": "https://moodlesite.com/mod/data/preset.php", "backtitle": "Back" } }}
{{#hasback}} {{/hasback}} {{#urlselect}} {{/urlselect}} {{#tertiarytitle}} {{/tertiarytitle}}
{{#fieldselect}} {{/fieldselect}}

templates/fields_mapping_footer.mustache000064400000002366152265517740014661 0ustar00{{! 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 . }} {{! @template mod_data/fields_mapping_footer Display a modal to decide on field mapping Example context (json): { "mapfieldsbutton": "", "applybutton": "" } }} {{#str}} mapping:mapfields, mod_data {{/str}} {{#str}} mapping:applypresets, mod_data {{/str}} templates/defaulttemplate_listtemplate.mustache000064400000005125152265517740016265 0ustar00{{! 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 . }} {{! @template mod_data/defaulttemplate_listtemplate Default template for the list view page in the database activity. Context variables required for this template: * fields - The database fields to display Example context (json): { "fields": [ { "fieldname": "Field1", "fieldcontent": "[[Field1]]" }, { "fieldname": "Field2", "fieldcontent": "[[Field2]]" } ] } }}
##userpicture##
##user##
##timeadded##
{{#str}}datemodified, mod_data{{/str}} ##timemodified##
##actionsmenu##
##approvalstatus##

{{#fields}}
{{fieldname}}
{{{fieldcontent}}}
{{/fields}} {{#tags}}
{{#str}}tags{{/str}}

{{.}}

{{/tags}}
templates/defaulttemplate_addtemplate.mustache000064400000003065152265517740016043 0ustar00{{! 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 . }} {{! @template mod_data/defaulttemplate_addtemplate Default template for the add page in the database activity. Context variables required for this template: * fields - The database fields to display Example context (json): { "fields": [ { "fieldname": "Field1", "fieldcontent": "[[Field1]]" }, { "fieldname": "Field2", "fieldcontent": "[[Field2]]" } ] } }}
{{#fields}}
{{fieldname}}
{{{fieldcontent}}}
{{/fields}} ##otherfields## {{#tags}}
{{#str}}tags{{/str}}
{{{.}}}
{{/tags}}
upgrade.txt000064400000016422152265517740006757 0ustar00=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /mod/data - plugins, information provided here is intended especially for developers. === 4.4 === * The following behat steps are now deprecated. Use data generators instead: - I add a ":fieldtype" field to ":activityname" database and I fill the form with: - I add an entry to ":activityname" database with: * Use of mustache templates for rendering add/modify fields forms. Elimination of all mod/data/field/*/mod.html files. * A function get_field_params() in data_field_base can be extended by datafield plugins to get their parameters. === 4.3 === * Function data_export_xls() has been deprecated and moved to deprecatedlib, because xls support has already been dropped. * Functions data_export_csv(), data_export_ods() and data_get_exportdata() have been deprecated due to a bigger refactoring of the way data is being exported. This is now being done by new exporter classes \mod_data\local\exporter\csv_entries_exporter and \mod_data\local\exporter\ods_entries_exporter (inheriting from exporter base class \mod_data\local\exporter\entries_exporter) as well as \mod_data\local\exporter\utils::data_exportdata(). * Function data_import_csv() has been deprecated and moved to deprecatedlib due to a bigger rework of the way data is being imported. This is now being done by new importer class \mod_data\local\importer\csv_importer inheriting from new class \mod_data\local\importer. * Field base class now has two new methods file_export_supported() and export_file_value(). The method file_export_supported() can be overwritten to declare that a field type can/wants to export a file. In this case this field type will have to implement the method export_file_value() returning this file for exporting. Also: This field type will have to export the name of the file by overwriting text_export_supported() to return true and make the method export_text_value() return the name of the file. * The field types file and picture now are able to export the file/picture. * Field base class now has two new methods file_import_supported() and import_file_value(). The method file_import_supported() can be overwritten to declare that a field type is able to import a file. In this case this field type will have to implement the method import_file_value() doing the actual import of the file being passed. * The field types file and picture now are able to import the file/picture. === 4.2 === * The field base class now has a method validate(). Overwrite it in the field type to provide validation of field type's parameters in the field add/modify form. * New tags are added to the current mod_data\templates class: ##otherfields##, [[FIELD#name]], and [[FIELD#description]]. * The mod_data\template class can provide preprocessor methods to optimize some tags. From now on, when load_template_tags detects a tag, it will call a "preprocess_tag_TAGNAME" method if it exists. This preprocessing can be used, for example, to precalculate some content. Currently, this preprocessor is used to detect which fields needs to be renderer when a ##otherfields## is used. === 4.1 === * The method data_view is now deprecated. Use $maganer->set_module_viewed instead. * The data_print_template function is now deprecated and replaced by mod_data\template class. * The data_print_ratings function now has an extra $print to get the ratings output instead of printing it directly. * The following functions have been deprecated because they have been moved to the manager class: - data_get_available_presets - data_get_available_site_presets - data_preset_name - data_presets_export - data_presets_generate_xml - data_presets_save - is_directory_a_preset * mod_data_external::add_entry() function throws an error when trying to add an entry to a database with no field created. * data_user_can_add_entry() function returns false for any user if there is no field created on the database. * From now on, the data_generate_default_template method will always return a string with the template content or an empty string when there is no content available. * The following classes have been deprecated from lib.php because they have been moved to use manager class: - data_preset_importer - data_preset_existing_importer - data_preset_upload_importer * import_setting_mappings() function has been deprecated. Use importing_preset() instead. * $fieldselect single_select type parameter has been deprecated for fields_action_bar class constructor, and a new action_menu type parameter has been added. * The $urlselect, $saveaspresetbutton and $exportpresetbutton parameters in the constructor of the fields_action_bar class have been deprecated too and are not used anymore. * The $saveaspresetbutton and $exportpresetbutton parameters in the constructor of the templates_action_bar class have been deprecated and are not used anymore. Besides, a new parameter, $actionsselect, has been added to the constructor of this class. * A new parameter, $actionsselect, has been added to the presets_action_bar constructor. * A new parameter, $mode, has been added to the view_action_bar constructor. * The $hassaveaspreset and $haveexportpreset parameters in the get_fields_action_bar() action_bar class have been deprecated and are not used anymore. === 3.7 === * External functions get_entries, get_entry and search_entries now return an additional field "tags" containing the entry tags. === 3.4 === * External function mod_data_external::search_entries() now returns the maxcount field: Total count of records that the user could see in the database (if all the search criterias were removed). * External function get_entry now returns an additional field "ratinginfo" containing the entry rating information. === 3.3.2 === * data_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function now optionally takes the module instance object or ID, and the course module object or ID. Please try to send the full objects instead of the ids to save DB calls. === 3.3 === * External function get_databases_by_courses now return more fields for users with mod/data:viewentry capability enabled: maxentries, rssarticles, singletemplate, listtemplate, listtemplateheader, listtemplatefooter, addtemplate, rsstemplate, rsstitletemplate, csstemplate, jstemplate, asearchtemplate, approval, defaultsort, defaultsortdir, manageapproved. * Data field classes extending data_field_base should implement the get_config_for_external method. This method is used for returning the field settings for external functions. You should check the user capabilities of the current user before returning any field setting value. This is intended to protect field settings like private keys for external systems. * Required entries has been added to the Activity completion setting of mod_form. Entries required for completion (in the entries section) is no longer displayed for new instances and will be deprecated in a future release in favour of the new completion setting. === 3.2 === * New hook - update_content_import - Can be implemented by field subplugins data_field_* class This can be used to pre-process data from a csv file before it is inserted into the database. locallib.php000064400000161242152265517740007062 0ustar00. /** * @package mod_data * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->libdir . '/portfolio/caller.php'); require_once($CFG->libdir . '/filelib.php'); /** * The class to handle entry exports of a database module */ class data_portfolio_caller extends portfolio_module_caller_base { /** @var int the single record to export */ protected $recordid; /** @var object the record from the data table */ private $data; /**#@+ @var array the fields used and their fieldtypes */ private $fields; private $fieldtypes; /** @var object the records to export */ private $records; /** @var int how many records are 'mine' */ private $minecount; /** * the required callback arguments for a single-record export * * @return array */ public static function expected_callbackargs() { return array( 'id' => true, 'recordid' => false, ); } /** * @param array $callbackargs the arguments passed through */ public function __construct($callbackargs) { parent::__construct($callbackargs); } /** * load up the data needed for the export * * @global object $DB */ public function load_data() { global $DB, $USER; if (!$this->cm = get_coursemodule_from_id('data', $this->id)) { throw new portfolio_caller_exception('invalidid', 'data'); } if (!$this->data = $DB->get_record('data', array('id' => $this->cm->instance))) { throw new portfolio_caller_exception('invalidid', 'data'); } $fieldrecords = $DB->get_records('data_fields', array('dataid' => $this->cm->instance), 'id'); // populate objets for this databases fields $this->fields = array(); foreach ($fieldrecords as $fieldrecord) { $tmp = data_get_field($fieldrecord, $this->data); $this->fields[] = $tmp; $this->fieldtypes[] = $tmp->type; } $this->records = array(); if ($this->recordid) { $tmp = $DB->get_record('data_records', array('id' => $this->recordid)); $tmp->content = $DB->get_records('data_content', array('recordid' => $this->recordid)); $this->records[] = $tmp; } else { $where = array('dataid' => $this->data->id); if (!has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))) { $where['userid'] = $USER->id; // get them all in case, we'll unset ones that aren't ours later if necessary } $tmp = $DB->get_records('data_records', $where); foreach ($tmp as $t) { $t->content = $DB->get_records('data_content', array('recordid' => $t->id)); $this->records[] = $t; } $this->minecount = $DB->count_records('data_records', array('dataid' => $this->data->id, 'userid' => $USER->id)); } if ($this->recordid) { list($formats, $files) = self::formats($this->fields, $this->records[0]); $this->set_file_and_format_data($files); } } /** * How long we think the export will take * Single entry is probably not too long. * But we check for filesizes * Else base it on the number of records * * @return one of PORTFOLIO_TIME_XX constants */ public function expected_time() { if ($this->recordid) { return $this->expected_time_file(); } else { return portfolio_expected_time_db(count($this->records)); } } /** * Calculate the shal1 of this export * Dependent on the export format. * @return string */ public function get_sha1() { // in the case that we're exporting a subclass of 'file' and we have a singlefile, // then we're not exporting any metadata, just the file by itself by mimetype. if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) { return $this->get_sha1_file(); } // otherwise we're exporting some sort of multipart content so use the data $str = ''; foreach ($this->records as $record) { foreach ($record as $data) { if (is_array($data) || is_object($data)) { $keys = array_keys($data); $testkey = array_pop($keys); if (is_array($data[$testkey]) || is_object($data[$testkey])) { foreach ($data as $d) { $str .= implode(',', (array)$d); } } else { $str .= implode(',', (array)$data); } } else { $str .= $data; } } } return sha1($str . ',' . $this->exporter->get('formatclass')); } /** * Prepare the package for export * * @return stored_file object */ public function prepare_package() { global $DB; $leapwriter = null; $content = ''; $filename = ''; $uid = $this->exporter->get('user')->id; $users = array(); //cache $onlymine = $this->get_export_config('mineonly'); if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) { $leapwriter = $this->exporter->get('format')->leap2a_writer(); $ids = array(); } if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) { return $this->get('exporter')->copy_existing_file($this->singlefile); } foreach ($this->records as $key => $record) { if ($onlymine && $record->userid != $uid) { unset($this->records[$key]); // sha1 continue; } list($tmpcontent, $files) = $this->exportentry($record); $content .= $tmpcontent; if ($leapwriter) { $entry = new portfolio_format_leap2a_entry('dataentry' . $record->id, $this->data->name, 'resource', $tmpcontent); $entry->published = $record->timecreated; $entry->updated = $record->timemodified; if ($record->userid != $uid) { if (!array_key_exists($record->userid, $users)) { $users[$record->userid] = $DB->get_record('user', array('id' => $record->userid), 'id,firstname,lastname'); } $entry->author = $users[$record->userid]; } $ids[] = $entry->id; $leapwriter->link_files($entry, $files, 'dataentry' . $record->id . 'file'); $leapwriter->add_entry($entry); } } if ($leapwriter) { if (count($this->records) > 1) { // make a selection element to tie them all together $selection = new portfolio_format_leap2a_entry('datadb' . $this->data->id, get_string('entries', 'data') . ': ' . $this->data->name, 'selection'); $leapwriter->add_entry($selection); $leapwriter->make_selection($selection, $ids, 'Grouping'); } $filename = $this->exporter->get('format')->manifest_name(); $content = $leapwriter->to_xml(); } else { if (count($this->records) == 1) { $filename = clean_filename($this->cm->name . '-entry.html'); } else { $filename = clean_filename($this->cm->name . '-full.html'); } } return $this->exporter->write_new_file( $content, $filename, ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH) // if we have associate files, this is a 'manifest' ); } /** * Verify the user can still export this entry * * @return bool */ public function check_permissions() { if ($this->recordid) { if (data_isowner($this->recordid)) { return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id)); } return has_capability('mod/data:exportentry', context_module::instance($this->cm->id)); } if ($this->has_export_config() && !$this->get_export_config('mineonly')) { return has_capability('mod/data:exportallentries', context_module::instance($this->cm->id)); } return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id)); } /** * @return string */ public static function display_name() { return get_string('modulename', 'data'); } /** * @global object * @return bool|void */ public function __wakeup() { global $CFG; if (empty($CFG)) { return true; // too early yet } foreach ($this->fieldtypes as $key => $field) { $filepath = $CFG->dirroot . '/mod/data/field/' . $field .'/field.class.php'; if (!file_exists($filepath)) { continue; } require_once($filepath); $this->fields[$key] = unserialize(serialize($this->fields[$key])); } } /** * Prepare a single entry for export, replacing all the content etc * * @param stdclass $record the entry to export * * @return array with key 0 = the html content, key 1 = array of attachments */ private function exportentry($record) { // Replacing tags $patterns = array(); $replacement = array(); $files = array(); // Then we generate strings to replace for normal tags $format = $this->get('exporter')->get('format'); foreach ($this->fields as $field) { $patterns[]='[['.$field->field->name.']]'; if (is_callable(array($field, 'get_file'))) { if (!$file = $field->get_file($record->id)) { $replacement[] = ''; continue; // probably left empty } $replacement[] = $format->file_output($file); $this->get('exporter')->copy_existing_file($file); $files[] = $file; } else { $replacement[] = $field->display_browse_field($record->id, 'singletemplate'); } } // Replacing special tags (##Edit##, ##Delete##, ##More##) $patterns[]='##edit##'; $patterns[]='##delete##'; $patterns[]='##export##'; $patterns[]='##more##'; $patterns[]='##moreurl##'; $patterns[]='##user##'; $patterns[]='##approve##'; $patterns[]='##disapprove##'; $patterns[]='##comments##'; $patterns[] = '##timeadded##'; $patterns[] = '##timemodified##'; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = ''; $replacement[] = userdate($record->timecreated); $replacement[] = userdate($record->timemodified); if (empty($this->data->singletemplate)) { // Use default template if the template is not created. $this->data->singletemplate = data_generate_default_template($this->data, 'singletemplate', 0, false, false); } // actual replacement of the tags return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files); } /** * Given the fields being exported, and the single record, * work out which export format(s) we can use * * @param array $fields array of field objects * @param object $record The data record object * * @uses PORTFOLIO_FORMAT_PLAINHTML * @uses PORTFOLIO_FORMAT_RICHHTML * * @return array of PORTFOLIO_XX constants */ public static function formats($fields, $record) { $formats = array(PORTFOLIO_FORMAT_PLAINHTML); $includedfiles = array(); foreach ($fields as $singlefield) { if (is_callable(array($singlefield, 'get_file'))) { if ($file = $singlefield->get_file($record->id)) { $includedfiles[] = $file; } } } if (count($includedfiles) == 1 && count($fields) == 1) { $formats = array(portfolio_format_from_mimetype($includedfiles[0]->get_mimetype())); } else if (count($includedfiles) > 0) { $formats = array(PORTFOLIO_FORMAT_RICHHTML); } return array($formats, $includedfiles); } public static function has_files($data) { global $DB; $fieldrecords = $DB->get_records('data_fields', array('dataid' => $data->id), 'id'); // populate objets for this databases fields foreach ($fieldrecords as $fieldrecord) { $field = data_get_field($fieldrecord, $data); if (is_callable(array($field, 'get_file'))) { return true; } } return false; } /** * base supported formats before we know anything about the export */ public static function base_supported_formats() { return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A); } public function has_export_config() { // if we're exporting more than just a single entry, // and we have the capability to export all entries, // then ask whether we want just our own, or all of them return (empty($this->recordid) // multi-entry export && $this->minecount > 0 // some of them are mine && $this->minecount != count($this->records) // not all of them are mine && has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))); // they actually have a choice in the matter } public function export_config_form(&$mform, $instance) { if (!$this->has_export_config()) { return; } $mform->addElement('selectyesno', 'mineonly', get_string('exportownentries', 'data', (object)array('mine' => $this->minecount, 'all' => count($this->records)))); $mform->setDefault('mineonly', 1); } public function get_allowed_export_config() { return array('mineonly'); } } /** * Class representing the virtual node with all itemids in the file browser * * @category files * @copyright 2012 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class data_file_info_container extends file_info { /** @var file_browser */ protected $browser; /** @var stdClass */ protected $course; /** @var stdClass */ protected $cm; /** @var string */ protected $component; /** @var stdClass */ protected $context; /** @var array */ protected $areas; /** @var string */ protected $filearea; /** * Constructor (in case you did not realize it ;-) * * @param file_browser $browser * @param stdClass $course * @param stdClass $cm * @param stdClass $context * @param array $areas * @param string $filearea */ public function __construct($browser, $course, $cm, $context, $areas, $filearea) { parent::__construct($browser, $context); $this->browser = $browser; $this->course = $course; $this->cm = $cm; $this->component = 'mod_data'; $this->context = $context; $this->areas = $areas; $this->filearea = $filearea; } /** * @return array with keys contextid, filearea, itemid, filepath and filename */ public function get_params() { return array( 'contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => null, 'filepath' => null, 'filename' => null, ); } /** * Can new files or directories be added via the file browser * * @return bool */ public function is_writable() { return false; } /** * Should this node be considered as a folder in the file browser * * @return bool */ public function is_directory() { return true; } /** * Returns localised visible name of this node * * @return string */ public function get_visible_name() { return $this->areas[$this->filearea]; } /** * Returns list of children nodes * * @return array of file_info instances */ public function get_children() { return $this->get_filtered_children('*', false, true); } /** * Help function to return files matching extensions or their count * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @param bool|int $countonly if false returns the children, if an int returns just the * count of children but stops counting when $countonly number of children is reached * @param bool $returnemptyfolders if true returns items that don't have matching files inside * @return array|int array of file_info instances or the count */ private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) { global $DB; $params = array('contextid' => $this->context->id, 'component' => $this->component, 'filearea' => $this->filearea); $sql = 'SELECT DISTINCT itemid FROM {files} WHERE contextid = :contextid AND component = :component AND filearea = :filearea'; if (!$returnemptyfolders) { $sql .= ' AND filename <> :emptyfilename'; $params['emptyfilename'] = '.'; } list($sql2, $params2) = $this->build_search_files_sql($extensions); $sql .= ' '.$sql2; $params = array_merge($params, $params2); if ($countonly === false) { $sql .= ' ORDER BY itemid DESC'; } $rs = $DB->get_recordset_sql($sql, $params); $children = array(); foreach ($rs as $record) { if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) { $children[] = $child; } if ($countonly !== false && count($children) >= $countonly) { break; } } $rs->close(); if ($countonly !== false) { return count($children); } return $children; } /** * Returns list of children which are either files matching the specified extensions * or folders that contain at least one such file. * * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg') * @return array of file_info instances */ public function get_non_empty_children($extensions = '*') { return $this->get_filtered_children($extensions, false); } /** * Returns the number of children which are either files matching the specified extensions * or folders containing at least one such file. * * @param string|array $extensions, for example '*' or array('.gif','.jpg') * @param int $limit stop counting after at least $limit non-empty children are found * @return int */ public function count_non_empty_children($extensions = '*', $limit = 1) { return $this->get_filtered_children($extensions, $limit); } /** * Returns parent file_info instance * * @return file_info or null for root */ public function get_parent() { return $this->browser->get_file_info($this->context); } } /** * This creates new calendar events given as timeavailablefrom and timeclose by $data. * * @param stdClass $data * @return void */ function data_set_events($data) { global $DB, $CFG; require_once($CFG->dirroot.'/calendar/lib.php'); // Get CMID if not sent as part of $data. if (!isset($data->coursemodule)) { $cm = get_coursemodule_from_instance('data', $data->id, $data->course); $data->coursemodule = $cm->id; } // Data start calendar events. $event = new stdClass(); $event->eventtype = DATA_EVENT_TYPE_OPEN; // The DATA_EVENT_TYPE_OPEN event should only be an action event if no close time was specified. $event->type = empty($data->timeavailableto) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) { if ($data->timeavailablefrom > 0) { // Calendar event exists so update it. $event->name = get_string('calendarstart', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $data->timeavailablefrom; $event->timesort = $data->timeavailablefrom; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) { $event->name = get_string('calendarstart', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->courseid = $data->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'data'; $event->instance = $data->id; $event->timestart = $data->timeavailablefrom; $event->timesort = $data->timeavailablefrom; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; calendar_event::create($event, false); } } // Data end calendar events. $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = DATA_EVENT_TYPE_CLOSE; if ($event->id = $DB->get_field('event', 'id', array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) { if ($data->timeavailableto > 0) { // Calendar event exists so update it. $event->name = get_string('calendarend', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->timestart = $data->timeavailableto; $event->timesort = $data->timeavailableto; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; $calendarevent = calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is on longer needed. $calendarevent = calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if (isset($data->timeavailableto) && $data->timeavailableto > 0) { $event->name = get_string('calendarend', 'data', $data->name); $event->description = format_module_intro('data', $data, $data->coursemodule, false); $event->format = FORMAT_HTML; $event->courseid = $data->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'data'; $event->instance = $data->id; $event->timestart = $data->timeavailableto; $event->timesort = $data->timeavailableto; $event->visible = instance_is_visible('data', $data); $event->timeduration = 0; calendar_event::create($event, false); } } } /** * Check if a database is available for the current user. * * @param stdClass $data database record * @param boolean $canmanageentries optional, if the user can manage entries * @param stdClass $context Module context, required if $canmanageentries is not set * @return array status (available or not and possible warnings) * @since Moodle 3.3 */ function data_get_time_availability_status($data, $canmanageentries = null, $context = null) { $open = true; $closed = false; $warnings = array(); if ($canmanageentries === null) { $canmanageentries = has_capability('mod/data:manageentries', $context); } if (!$canmanageentries) { $timenow = time(); if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) { $open = false; } if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) { $closed = true; } if (!$open or $closed) { if (!$open) { $warnings['notopenyet'] = userdate($data->timeavailablefrom); } if ($closed) { $warnings['expired'] = userdate($data->timeavailableto); } return array(false, $warnings); } } // Database is available. return array(true, $warnings); } /** * Requires a database to be available for the current user. * * @param stdClass $data database record * @param boolean $canmanageentries optional, if the user can manage entries * @param stdClass $context Module context, required if $canmanageentries is not set * @throws moodle_exception * @since Moodle 3.3 */ function data_require_time_available($data, $canmanageentries = null, $context = null) { list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context); if (!$available) { $reason = current(array_keys($warnings)); throw new moodle_exception($reason, 'data', '', $warnings[$reason]); } } /** * Return the number of entries left to add to complete the activity. * * @param stdClass $data database object * @param int $numentries the number of entries the current user has created * @param bool $canmanageentries whether the user can manage entries (teachers, managers) * @return int the number of entries left, 0 if no entries left or if is not required * @since Moodle 3.3 */ function data_get_entries_left_to_add($data, $numentries, $canmanageentries) { if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) { return $data->requiredentries - $numentries; } return 0; } /** * Return the number of entires left to add to view other users entries.. * * @param stdClass $data database object * @param int $numentries the number of entries the current user has created * @param bool $canmanageentries whether the user can manage entries (teachers, managers) * @return int the number of entries left, 0 if no entries left or if is not required * @since Moodle 3.3 */ function data_get_entries_left_to_view($data, $numentries, $canmanageentries) { if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) { return $data->requiredentriestoview - $numentries; } 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 (!$cm->uservisible) { $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]); if (empty($template)) { $data = $DB->get_record('data', ['id' => $dataid]); $template = data_generate_default_template($data, 'addtemplate', 0, false, false); } $filteredfields = []; foreach ($fields as $field) { if (!in_array($field->type, $validfieldtypes)) { continue; } $field->addtemplateposition = strpos($template, '[['.$field->name.']]'); if ($field->addtemplateposition === false) { continue; } $field->type = clean_param($field->type, PARAM_ALPHA); $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php'; if (!file_exists($filepath)) { continue; } require_once($filepath); $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|null It will return the title of the entry or null if the field type is not available. */ function data_get_tag_title_for_entry($field, $entry) { global $CFG, $DB; if (!isset($field->type)) { return null; } $field->type = clean_param($field->type, PARAM_ALPHA); $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php'; if (!file_exists($filepath)) { return null; } require_once($filepath); $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. * * @param stdClass $data database object * @param stdClass $cm course module object * @param stdClass $context context object * @param stdClass $mode in which mode we are viewing the database (list, single) * @param int $currentgroup the current group being used * @param str $search search for this text in the entry data * @param str $sort the field to sort by * @param str $order the order to use when sorting * @param int $page for pagination, the current page * @param int $perpage entries per page * @param bool $advanced whether we are using or not advanced search * @param array $searcharray when using advanced search, the advanced data to use * @param stdClass $record if we jsut want this record after doing all the access checks * @return array the entries found among other data related to the search * @since Moodle 3.3 */ function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0, $perpage = 0, $advanced = null, $searcharray = null, $record = null) { global $DB, $USER; if ($sort === null) { $sort = $data->defaultsort; } if ($order === null) { $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC'; } if ($searcharray === null) { $searcharray = array(); } if (core_text::strlen($search) < 2) { $search = ''; } $approvecap = has_capability('mod/data:approve', $context); $canmanageentries = has_capability('mod/data:manageentries', $context); // If a student is not part of a group and seperate groups is enabled, we don't // want them seeing all records. $groupmode = groups_get_activity_groupmode($cm); $canviewallrecords = $groupmode != SEPARATEGROUPS || has_capability('moodle/site:accessallgroups', $context); $numentries = data_numentries($data); $requiredentriesallowed = true; if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) { $requiredentriesallowed = false; } // Initialise the first group of params for advanced searches. $initialparams = array(); $params = array(); // Named params array. // Setup group and approve restrictions. if (!$approvecap && $data->approval) { if (isloggedin()) { $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) '; $params['myid1'] = $USER->id; $initialparams['myid1'] = $params['myid1']; } else { $approveselect = ' AND r.approved=1 '; } } else { $approveselect = ' '; } if ($currentgroup) { $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)"; $params['currentgroup'] = $currentgroup; $initialparams['currentgroup'] = $params['currentgroup']; } else { if ($canviewallrecords) { $groupselect = ' '; } else { // If separate groups are enabled and the user isn't in a group or // a teacher, manager, admin etc, then just show them entries for 'All participants'. $groupselect = " AND r.groupid = 0"; } } // Init some variables to be used by advanced search. $advsearchselect = ''; $advwhere = ''; $advtables = ''; $advparams = array(); // This is used for the initial reduction of advanced search results with required entries. $entrysql = ''; $userfieldsapi = \core_user\fields::for_userpic()->excluding('id'); $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; // Find the field we are sorting on. if ($sort <= 0 || !($sortfield = data_get_field_from_id($sort, $data))) { switch ($sort) { case DATA_LASTNAME: $ordering = "u.lastname $order, u.firstname $order"; break; case DATA_FIRSTNAME: $ordering = "u.firstname $order, u.lastname $order"; break; case DATA_APPROVED: $ordering = "r.approved $order, r.timecreated $order"; break; case DATA_TIMEMODIFIED: $ordering = "r.timemodified $order"; break; case DATA_TIMEADDED: default: $sort = 0; $ordering = "r.timecreated $order"; } $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields; $count = ' COUNT(DISTINCT c.recordid) '; $tables = '{data_content} c,{data_records} r, {user} u '; $where = 'WHERE c.recordid = r.id AND r.dataid = :dataid AND r.userid = u.id '; $params['dataid'] = $data->id; $sortorder = " ORDER BY $ordering, r.id $order"; $searchselect = ''; // If requiredentries is not reached, only show current user's entries. if (!$requiredentriesallowed) { $where .= ' AND u.id = :myid2 '; $entrysql = ' AND r.userid = :myid3 '; $params['myid2'] = $USER->id; $initialparams['myid3'] = $params['myid2']; } if ($search) { $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR ".$DB->sql_like('u.firstname', ':search2', false)." OR ".$DB->sql_like('u.lastname', ':search3', false)." ) "; $params['search1'] = "%$search%"; $params['search2'] = "%$search%"; $params['search3'] = "%$search%"; } else { $searchselect = ' '; } } else { $sortcontent = $DB->sql_compare_text('s.' . $sortfield->get_sort_field()); $sortcontentfull = $sortfield->get_sort_sql($sortcontent); $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ', ' . $sortcontentfull . ' AS sortorder '; $count = ' COUNT(DISTINCT c.recordid) '; $tables = '{data_content} c, {data_records} r, {user} u '; $where = 'WHERE c.recordid = r.id AND r.dataid = :dataid AND r.userid = u.id '; if (!$advanced) { $where .= 'AND s.fieldid = :sort AND s.recordid = r.id'; $tables .= ',{data_content} s '; } $params['dataid'] = $data->id; $params['sort'] = $sort; $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC '; $searchselect = ''; // If requiredentries is not reached, only show current user's entries. if (!$requiredentriesallowed) { $where .= ' AND u.id = :myid2'; $entrysql = ' AND r.userid = :myid3'; $params['myid2'] = $USER->id; $initialparams['myid3'] = $params['myid2']; } if ($search) { $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR ".$DB->sql_like('u.firstname', ':search2', false)." OR ".$DB->sql_like('u.lastname', ':search3', false)." ) "; $params['search1'] = "%$search%"; $params['search2'] = "%$search%"; $params['search3'] = "%$search%"; } else { $searchselect = ' '; } } // To actually fetch the records. $fromsql = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect"; $allparams = array_merge($params, $advparams); // Provide initial sql statements and parameters to reduce the number of total records. $initialselect = $groupselect . $approveselect . $entrysql; $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams); $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id); $selectdata = $where . $groupselect . $approveselect; if (!empty($advanced)) { $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder); $sqlselect = $advancedsearchsql['sql']; $allparams = array_merge($allparams, $advancedsearchsql['params']); $totalcount = count($newrecordids); } else { $sqlselect = "SELECT $what $fromsql $sortorder"; $sqlcountselect = "SELECT $count $fromsql"; $totalcount = $DB->count_records_sql($sqlcountselect, $allparams); } // Work out the paging numbers and counts. if (empty($searchselect) && empty($advsearchselect)) { $maxcount = $totalcount; } else { $maxcount = count($recordids); } if ($record) { // We need to just show one, so where is it in context? $nowperpage = 1; $mode = 'single'; $page = 0; // TODO MDL-33797 - Reduce this or consider redesigning the paging system. if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) { $page = (int)array_search($record->id, $allrecordids); unset($allrecordids); } } else if ($mode == 'single') { // We rely on ambient $page settings $nowperpage = 1; } else { $nowperpage = $perpage; } // Get the actual records. if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) { // Nothing to show! if ($record) { // Something was requested so try to show that at least (bug 5132) if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) { // OK, we can show this one $records = array($record->id => $record); $totalcount = 1; } } } return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode]; } /** * Check if the current user can view the given record. * * @param stdClass $data database record * @param stdClass $record the record (entry) to check * @param int $currentgroup current group * @param bool $canmanageentries if the user can manage entries * @return bool true if the user can view the entry * @since Moodle 3.3 */ function data_can_view_record($data, $record, $currentgroup, $canmanageentries) { global $USER; if ($canmanageentries || empty($data->approval) || $record->approved || (isloggedin() && $record->userid == $USER->id)) { if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) { return true; } } return false; } /** * Return all the field instances for a given database. * * @param stdClass $data database object * @return array field instances * @since Moodle 3.3 */ function data_get_field_instances($data) { global $DB; $instances = []; if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) { foreach ($fields as $field) { $instances[] = data_get_field($field, $data); } } return $instances; } /** * Build the search array. * * @param stdClass $data the database object * @param bool $paging if paging is being used * @param array $searcharray the current search array (saved by session) * @param array $defaults default values for the searchable fields * @param str $fn the first name to search (optional) * @param str $ln the last name to search (optional) * @return array the search array and plain search build based on the different elements * @since Moodle 3.3 */ function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') { global $DB; $search = ''; $vals = array(); $fields = $DB->get_records('data_fields', array('dataid' => $data->id)); if (!empty($fields)) { foreach ($fields as $field) { $searchfield = data_get_field_from_id($field->id, $data); // Get field data to build search sql with. If paging is false, get from user. // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116). if (!$paging && $searchfield->type != 'unknown') { $val = $searchfield->parse_search_field($defaults); } else { // Set value from session if there is a value @ the required index. if (isset($searcharray[$field->id])) { $val = $searcharray[$field->id]->data; } else { // If there is not an entry @ the required index, set value to blank. $val = ''; } } if (!empty($val)) { $searcharray[$field->id] = new stdClass(); list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val); $searcharray[$field->id]->data = $val; $vals[] = $val; } else { // Clear it out. unset($searcharray[$field->id]); } } } $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST); if ($rawtagnames) { $searcharray[DATA_TAGS] = new stdClass(); $searcharray[DATA_TAGS]->params = []; $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames; $searcharray[DATA_TAGS]->sql = ''; } else { unset($searcharray[DATA_TAGS]); } if (!$paging) { // Name searching. $fn = optional_param('u_fn', $fn, PARAM_NOTAGS); $ln = optional_param('u_ln', $ln, PARAM_NOTAGS); } else { $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : ''; $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : ''; } if (!empty($fn)) { $searcharray[DATA_FIRSTNAME] = new stdClass(); $searcharray[DATA_FIRSTNAME]->sql = ''; $searcharray[DATA_FIRSTNAME]->params = array(); $searcharray[DATA_FIRSTNAME]->field = 'u.firstname'; $searcharray[DATA_FIRSTNAME]->data = $fn; $vals[] = $fn; } else { unset($searcharray[DATA_FIRSTNAME]); } if (!empty($ln)) { $searcharray[DATA_LASTNAME] = new stdClass(); $searcharray[DATA_LASTNAME]->sql = ''; $searcharray[DATA_LASTNAME]->params = array(); $searcharray[DATA_LASTNAME]->field = 'u.lastname'; $searcharray[DATA_LASTNAME]->data = $ln; $vals[] = $ln; } else { unset($searcharray[DATA_LASTNAME]); } // In case we want to switch to simple search later - there might be multiple values there ;-). if ($vals) { $val = reset($vals); if (is_string($val)) { $search = $val; } } return [$searcharray, $search]; } /** * Approves or unapproves an entry. * * @param int $entryid the entry to approve or unapprove. * @param bool $approve Whether to approve or unapprove (true for approve false otherwise). * @since Moodle 3.3 */ function data_approve_entry($entryid, $approve) { global $DB; $newrecord = new stdClass(); $newrecord->id = $entryid; $newrecord->approved = $approve ? 1 : 0; $DB->update_record('data_records', $newrecord); } /** * Populate the field contents of a new record with the submitted data. * An event has been previously triggered upon the creation of the new record in data_add_record(). * * @param stdClass $data database object * @param stdClass $context context object * @param int $recordid the new record id * @param array $fields list of fields of the database * @param stdClass $datarecord the submitted data * @param stdClass $processeddata pre-processed submitted fields * @since Moodle 3.3 */ function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) { global $DB; // Insert a whole lot of empty records to make sure we have them. $records = array(); foreach ($fields as $field) { $content = new stdClass(); $content->recordid = $recordid; $content->fieldid = $field->id; $records[] = $content; } // Bulk insert the records now. Some records may have no data but all must exist. $DB->insert_records('data_content', $records); // Add all provided content. foreach ($processeddata->fields as $fieldname => $field) { $field->update_content($recordid, $datarecord->$fieldname, $fieldname); } } /** * Updates the fields contents of an existing record. * * @param stdClass $data database object * @param stdClass $record record to update object * @param stdClass $context context object * @param stdClass $datarecord the submitted data * @param stdClass $processeddata pre-processed submitted fields * @since Moodle 3.3 */ function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) { global $DB; // Reset the approved flag after edit if the user does not have permission to approve their own entries. if (!has_capability('mod/data:approve', $context)) { $record->approved = 0; } // Update the parent record. $record->timemodified = time(); $DB->update_record('data_records', $record); // Update all content. foreach ($processeddata->fields as $fieldname => $field) { $field->update_content($record->id, $datarecord->$fieldname, $fieldname); } // Trigger an event for updating this record. $event = \mod_data\event\record_updated::create(array( 'objectid' => $record->id, 'context' => $context, 'courseid' => $data->course, 'other' => array( 'dataid' => $data->id ) )); $event->add_record_snapshot('data', $data); $event->trigger(); } edit.php000064400000017363152265517740006232 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\manager; require_once('../../config.php'); require_once('locallib.php'); require_once("$CFG->libdir/rsslib.php"); require_once("$CFG->libdir/form/filemanager.php"); $id = optional_param('id', 0, PARAM_INT); // Course module id. $d = optional_param('d', 0, PARAM_INT); // Database id. $rid = optional_param('rid', 0, PARAM_INT); // Record id. $mode = 'addtemplate'; // Define the mode for this page, only 1 mode available. $tags = optional_param_array('tags', [], PARAM_TAGLIST); $redirectbackto = optional_param('backto', '', PARAM_LOCALURL); // The location to redirect back. $url = new moodle_url('/mod/data/edit.php'); $record = null; if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); } else { // We must have $d. $data = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($data); $cm = $manager->get_coursemodule(); $course = get_course($cm->course); } $data = $manager->get_instance(); $context = $manager->get_context(); $url->param('id', $cm->id); if ($rid !== 0) { $record = $DB->get_record( 'data_records', ['id' => $rid, 'dataid' => $data->id], '*', MUST_EXIST ); $url->param('rid', $rid); } $PAGE->set_url($url); require_login($course, false, $cm); $url->param('backto', $redirectbackto); if (isguestuser()) { redirect('view.php?d='.$data->id); } /// Can't use this if there are no fields if ($manager->can_manage_templates()) { if (!$manager->has_fields()) { redirect($CFG->wwwroot.'/mod/data/field.php?d='.$data->id); // Redirect to field entry. } } // Get Group information for permission testing and record creation. $currentgroup = groups_get_activity_group($cm); $groupmode = groups_get_activity_groupmode($cm); if (!has_capability('mod/data:manageentries', $context)) { if ($rid) { // User is editing an existing record. if (!data_user_can_manage_entry($record, $data, $context)) { throw new \moodle_exception('noaccess', 'data'); } } else if (!data_user_can_add_entry($data, $currentgroup, $groupmode, $context)) { // User is trying to create a new record. throw new \moodle_exception('noaccess', 'data'); } } // RSS and CSS and JS meta. if (!empty($CFG->enablerssfeeds) && !empty($CFG->data_enablerssfeeds) && $data->rssarticles > 0) { $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); $rsstitle = $courseshortname . ': ' . format_string($data->name); rss_add_http_header($context, 'mod_data', $data, $rsstitle); } if ($data->csstemplate) { $PAGE->requires->css('/mod/data/css.php?d='.$data->id); } if ($data->jstemplate) { $PAGE->requires->js('/mod/data/js.php?d='.$data->id, true); } // Define page variables. $strdata = get_string('modulenameplural','data'); if ($rid) { $PAGE->navbar->add(get_string('editentry', 'data')); } $PAGE->add_body_class('limitedwidth'); if ($rid) { $pagename = get_string('editentry', 'data'); } else { $pagename = get_string('newentry', 'data'); } $PAGE->navbar->add($pagename); $titleparts = [ $pagename, format_string($data->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->force_settings_menu(true); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); // Process incoming data for adding/updating records. // Keep track of any notifications ad submitted data. $processeddata = null; $datarecord = data_submitted() ?: null; // Process the submitted form. if ($datarecord && confirm_sesskey()) { // Validate the form to ensure that enough data was submitted. $fields = $manager->get_field_records(); $processeddata = data_process_submission($data, $fields, $datarecord); if ($processeddata->validated) { if ($rid) { $recordid = $rid; // Updating an existing record. data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata); } else { // Add instance to data_record. $recordid = data_add_record($data, $currentgroup); if ($recordid) { // Now populate the fields contents of the new record. data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata); } } if ($recordid) { core_tag_tag::set_item_tags('mod_data', 'data_records', $recordid, $context, $tags); if (!empty($datarecord->saveandadd)) { // User has clicked "Save and add another". Reset all of the fields. $datarecord = null; } else { $viewurl = new moodle_url('/mod/data/view.php', [ 'd' => $data->id, 'rid' => $recordid, ]); redirect($viewurl); } } } } // End of form processing. echo $OUTPUT->header(); groups_print_activity_menu($cm, $CFG->wwwroot.'/mod/data/edit.php?d='.$data->id); // Form goes here first in case add template is empty. echo '
'; echo '
'; echo ''; echo ''; echo ''; echo $OUTPUT->box_start('generalbox boxaligncenter boxwidthwide'); echo $OUTPUT->heading($pagename); $template = $manager->get_template($mode); echo $template->parse_add_entry($processeddata, $rid, $datarecord); if (empty($redirectbackto)) { $redirectbackto = new \moodle_url('/mod/data/view.php', ['id' => $cm->id]); } $actionbuttons = html_writer::link( $redirectbackto, get_string('cancel'), ['class' => 'btn btn-secondary mx-1', 'role' => 'button'] ); $actionbuttons .= html_writer::empty_tag('input', [ 'type' => 'submit', 'name' => 'saveandview', 'value' => get_string('save'), 'class' => 'btn btn-primary mx-1' ]); if (!$rid && ((!$data->maxentries) || has_capability('mod/data:manageentries', $context) || (data_numentries($data) < ($data->maxentries - 1)))) { $actionbuttons .= html_writer::empty_tag('input', [ 'type' => 'submit', 'name' => 'saveandadd', 'value' => get_string('saveandadd', 'data'), 'class' => 'btn btn-primary mx-1' ]); } $stickyfooter = new core\output\sticky_footer($actionbuttons); echo $OUTPUT->render($stickyfooter); echo $OUTPUT->box_end(); echo '
'; $possiblefields = $manager->get_fields(); foreach ($possiblefields as $field) { $field->print_after_form(); } // Finish the page. if (empty($possiblefields)) { throw new \moodle_exception('nofieldindatabase', 'data'); } echo $OUTPUT->footer(); index.php000064400000012004152265517740006377 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 1990 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ require_once("../../config.php"); require_once("lib.php"); $id = required_param('id', PARAM_INT); // course $PAGE->set_url('/mod/data/index.php', array('id'=>$id)); if (!$course = $DB->get_record('course', array('id'=>$id))) { throw new \moodle_exception('invalidcourseid'); } require_course_login($course); $PAGE->set_pagelayout('incourse'); $context = context_course::instance($course->id); $params = array( 'context' => context_course::instance($course->id) ); $event = \mod_data\event\course_module_instance_list_viewed::create($params); $event->add_record_snapshot('course', $course); $event->trigger(); $strname = get_string('name'); $strdata = get_string('modulename','data'); $strdataplural = get_string('modulenameplural','data'); $PAGE->navbar->add($strdata, new moodle_url('/mod/data/index.php', array('id'=>$course->id))); $titleparts = [ $strdataplural, format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); echo $OUTPUT->heading($strdataplural, 2); if (! $datas = get_all_instances_in_course("data", $course)) { notice(get_string('thereareno', 'moodle',$strdataplural) , "$CFG->wwwroot/course/view.php?id=$course->id"); } $usesections = course_format_uses_sections($course->format); $timenow = time(); $strname = get_string('name'); $strdescription = get_string("description"); $strentries = get_string('entries', 'data'); $strnumnotapproved = get_string('numnotapproved', 'data'); $table = new html_table(); if ($usesections) { $strsectionname = course_get_format($course)->get_generic_section_name(); $table->head = array ($strsectionname, $strname, $strdescription, $strentries, $strnumnotapproved); $table->align = array ('center', 'center', 'center', 'center', 'center'); } else { $table->head = array ($strname, $strdescription, $strentries, $strnumnotapproved); $table->align = array ('center', 'center', 'center', 'center'); } $rss = (!empty($CFG->enablerssfeeds) && !empty($CFG->data_enablerssfeeds)); if ($rss) { require_once($CFG->libdir."/rsslib.php"); array_push($table->head, 'RSS'); array_push($table->align, 'center'); } $currentsection = ""; foreach ($datas as $data) { $printsection = ""; //Calculate the href if (!$data->visible) { //Show dimmed if the mod is hidden $link = "coursemodule\">".format_string($data->name,true).""; } else { //Show normal if the mod is visible $link = "coursemodule\">".format_string($data->name,true).""; } // TODO: add group restricted counts here, and limit unapproved to ppl with approve cap only + link to approval page $numrecords = $DB->count_records_sql('SELECT COUNT(r.id) FROM {data_records} r WHERE r.dataid =?', array($data->id)); if ($data->approval == 1) { $numunapprovedrecords = $DB->count_records_sql('SELECT COUNT(r.id) FROM {data_records} r WHERE r.dataid =? AND r.approved <> 1', array($data->id)); } else { $numunapprovedrecords = '-'; } $rsslink = ''; if ($rss && $data->rssarticles > 0) { $rsslink = rss_get_link($context->id, $USER->id, 'mod_data', $data->id, 'RSS'); } if ($usesections) { if ($data->section !== $currentsection) { if ($data->section) { $printsection = get_section_name($course, $data->section); } if ($currentsection !== '') { $table->data[] = 'hr'; } $currentsection = $data->section; } $row = array($printsection, $link, format_module_intro('data', $data, $data->coursemodule), $numrecords, $numunapprovedrecords); } else { $row = array($link, format_module_intro('data', $data, $data->coursemodule), $numrecords, $numunapprovedrecords); } if ($rss) { array_push($row, $rsslink); } $table->data[] = $row; } echo "
"; echo html_writer::tag('div', html_writer::table($table), ['class' => 'table-responsive']); echo $OUTPUT->footer(); preset.php000064400000014750152265517740006604 0ustar00. /** * Preset Menu * * This is the page that is the menu item in the config database * pages. * * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\local\importer\preset_importer; use mod_data\local\importer\preset_upload_importer; use mod_data\manager; use mod_data\preset; use mod_data\output\action_bar; use mod_data\output\preset_preview; require_once('../../config.php'); require_once($CFG->dirroot.'/mod/data/lib.php'); require_once($CFG->dirroot.'/mod/data/preset_form.php'); // The course module id. $id = optional_param('id', 0, PARAM_INT); $manager = null; if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); $data = $manager->get_instance(); } else { // We must have the database activity id. $d = required_param('d', PARAM_INT); $data = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($data); $cm = $manager->get_coursemodule(); $course = get_course($cm->course); } $action = optional_param('action', 'view', PARAM_ALPHA); // The page action. $allowedactions = ['view', 'importzip', 'finishimport', 'export', 'preview']; if (!in_array($action, $allowedactions)) { throw new moodle_exception('invalidaccess'); } $context = $manager->get_context(); require_login($course, false, $cm); require_capability('mod/data:managetemplates', $context); $url = new moodle_url('/mod/data/preset.php', array('d' => $data->id)); $PAGE->add_body_class('limitedwidth'); $PAGE->set_url($url); $titleparts = [ get_string('presets', 'data'), format_string($cm->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->set_heading($course->fullname); $PAGE->force_settings_menu(true); $PAGE->activityheader->disable(); $PAGE->requires->js_call_amd('mod_data/deletepreset', 'init'); // fill in missing properties needed for updating of instance $data->course = $cm->course; $data->cmidnumber = $cm->idnumber; $data->instance = $cm->instance; $renderer = $manager->get_renderer(); $presets = $manager->get_available_presets(); if ($action === 'export') { if (headers_sent()) { throw new \moodle_exception('headersent'); } // Check if we should export a given preset or the current one. $presetname = optional_param('presetname', $data->name, PARAM_FILE); $preset = preset::create_from_instance($manager, $presetname); $exportfile = $preset->export(); $exportfilename = basename($exportfile); header("Content-Type: application/download\n"); header("Content-Disposition: attachment; filename=\"$exportfilename\""); header('Expires: 0'); header('Cache-Control: must-revalidate,post-check=0,pre-check=0'); header('Pragma: public'); // If this file was requested from a form, then mark download as complete. \core_form\util::form_download_complete(); $exportfilehandler = fopen($exportfile, 'rb'); print fread($exportfilehandler, filesize($exportfile)); fclose($exportfilehandler); unlink($exportfile); exit(0); } if ($action == 'importzip') { $filepath = optional_param('filepath', '', PARAM_PATH); $importer = new preset_upload_importer($manager, $CFG->tempdir . $filepath); if ($importer->needs_mapping()) { echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('fieldmappings', 'data'), 2, 'mb-4'); echo $renderer->importing_preset($data, $importer); echo $OUTPUT->footer(); exit(0); } $importer->import(false); core\notification::success(get_string('importsuccess', 'mod_data')); redirect(new moodle_url('/mod/data/field.php', ['id' => $cm->id])); exit(0); } // Preset preview injects CSS and JS to the page and should be done before the page header. if ($action === 'preview') { $fullname = optional_param('fullname', '', PARAM_PATH); // The directory the preset is in. $templatename = optional_param('template', 'listtemplate', PARAM_ALPHA); // Find out preset owner userid and shortname. $preset = preset::create_from_fullname($manager, $fullname); // Validate if the user can view this preset. if (!$manager->can_view_preset($preset)) { throw new \moodle_exception('cannotaccesspresentsother', manager::PLUGINNAME); } $preview = new preset_preview($manager, $preset, $templatename); $preview->prepare_page($PAGE); $url->params([ 'fullname' => $fullname, 'template' => $templatename, ]); $PAGE->set_url($url); $titleparts = [ get_string('preview', 'data', $preset->name), format_string($cm->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); // Print the preview screen. echo $OUTPUT->header(); $actionbar = new action_bar($data->id, $url); echo $actionbar->get_presets_preview_action_bar($manager, $fullname, $templatename); echo $renderer->render($preview); echo $OUTPUT->footer(); exit(0); } if ($action === 'finishimport') { if (!confirm_sesskey()) { throw new moodle_exception('invalidsesskey'); } $overwritesettings = optional_param('overwritesettings', false, PARAM_BOOL); $importer = preset_importer::create_from_parameters($manager); $importer->finish_import_process($overwritesettings, $data); } echo $OUTPUT->header(); $actionbar = new \mod_data\output\action_bar($data->id, $url); echo $actionbar->get_presets_action_bar(); $presets = new \mod_data\output\presets($manager, $presets, new \moodle_url('/mod/data/field.php'), true); echo $renderer->render_presets($presets); echo $OUTPUT->footer(); import.php000064400000011567152265517740006617 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ require_once('../../config.php'); require_once('lib.php'); require_once($CFG->libdir.'/csvlib.class.php'); require_once('import_form.php'); $id = optional_param('id', 0, PARAM_INT); // course module id $d = optional_param('d', 0, PARAM_INT); // database id $rid = optional_param('rid', 0, PARAM_INT); // record id $fielddelimiter = optional_param('fielddelimiter', ',', PARAM_CLEANHTML); // characters used as field delimiters for csv file import $fieldenclosure = optional_param('fieldenclosure', '', PARAM_CLEANHTML); // characters used as record delimiters for csv file import $redirectbackto = optional_param('backto', '', PARAM_LOCALURL); // The location to redirect back to. $url = new moodle_url('/mod/data/import.php'); if ($rid !== 0) { $url->param('rid', $rid); } if ($fielddelimiter !== '') { $url->param('fielddelimiter', $fielddelimiter); } if ($fieldenclosure !== '') { $url->param('fieldenclosure', $fieldenclosure); } if ($id) { $url->param('id', $id); $PAGE->set_url($url); $cm = get_coursemodule_from_id('data', $id, 0, false, MUST_EXIST); $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); $data = $DB->get_record('data', array('id'=>$cm->instance), '*', MUST_EXIST); } else { $url->param('d', $d); $PAGE->set_url($url); $data = $DB->get_record('data', array('id'=>$d), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$data->course), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('data', $data->id, $course->id, false, MUST_EXIST); } require_login($course, false, $cm); $context = context_module::instance($cm->id); require_capability('mod/data:manageentries', $context); $form = new mod_data_import_form(new moodle_url('/mod/data/import.php'), ['dataid' => $data->id, 'backtourl' => $redirectbackto]); if ($form->is_cancelled()) { $redirectbackto = !empty($redirectbackto) ? $redirectbackto : new \moodle_url('/mod/data/view.php', ['d' => $data->id]); redirect($redirectbackto); } /// Print the page header $pagename = get_string('uploadrecords', 'data'); $PAGE->navbar->add($pagename); $PAGE->add_body_class('limitedwidth'); $titleparts = [ $pagename, format_string($data->name), format_string($course->fullname), ]; $PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('modulepage'); $PAGE->activityheader->disable(); echo $OUTPUT->header(); echo $OUTPUT->heading_with_help($pagename, 'uploadrecords', 'mod_data'); if ($formdata = $form->get_data()) { $uploadedfilepath = $form->save_temp_file('recordsfile'); $filestempdir = null; if (!$uploadedfilepath) { throw new coding_exception('No file uploaded.'); } $importer = new \mod_data\local\importer\csv_entries_importer($uploadedfilepath, $form->get_new_filename('recordsfile')); if (!$importer->get_data_file_content()) { echo $OUTPUT->notification(get_string('errordatafilenotfound', 'data'), \core\output\notification::NOTIFY_ERROR); } else { $importer->import_csv($cm, $data, $formdata->encoding, $formdata->fielddelimiter); unlink($uploadedfilepath); $addedrecordsmessages = $importer->get_added_records_messages(); echo html_writer::div(implode('
', $addedrecordsmessages)); if (count($addedrecordsmessages) > 0) { echo $OUTPUT->notification(count($addedrecordsmessages) . ' ' . get_string('recordssaved', 'data'), \core\output\notification::NOTIFY_SUCCESS); } else { echo $OUTPUT->notification(get_string('recordsnotsaved', 'data'), \core\output\notification::NOTIFY_ERROR); } } } else { /// Upload records section. Only for teachers and the admin. echo $OUTPUT->box_start('generalbox boxaligncenter boxwidthwide'); $form->display(); echo $OUTPUT->box_end(); } /// Finish the page echo $OUTPUT->footer(); amd/src/selectpreset.js000064400000005654152265517740011164 0ustar00// 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 . /** * Javascript module to control the form responsible for selecting a preset. * * @module mod_data/selectpreset * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const selectors = { presetRadioButton: 'input[name="fullname"]', selectPresetButton: 'input[name="selectpreset"]', selectedPresetRadioButton: 'input[name="fullname"]:checked', }; /** * Initialize module. */ export const init = () => { const radioButton = document.querySelectorAll(selectors.presetRadioButton); // Initialize the "Use a preset" button properly. disableUsePresetButton(); radioButton.forEach((elem) => { elem.addEventListener('change', function(event) { event.preventDefault(); // Enable the "Use a preset" button when any of the radio buttons in the presets list is checked. disableUsePresetButton(); }); }); }; /** * Decide whether to disable or not the "Use a preset" button. * When there is no preset selected, the button should be displayed disabled; otherwise, it will appear enabled as a primary button. * * @method * @private */ const disableUsePresetButton = () => { let selectPresetButton = document.querySelector(selectors.selectPresetButton); const selectedRadioButton = document.querySelector(selectors.selectedPresetRadioButton); if (selectedRadioButton) { // There is one preset selected, so the button should be enabled. selectPresetButton.removeAttribute('disabled'); selectPresetButton.classList.remove('btn-secondary'); selectPresetButton.classList.add('btn-primary'); selectPresetButton.setAttribute('data-presetname', selectedRadioButton.getAttribute('value')); selectPresetButton.setAttribute('data-cmid', selectedRadioButton.getAttribute('data-cmid')); } else { // There is no any preset selected, so the button should be disabled. selectPresetButton.setAttribute('disabled', true); selectPresetButton.classList.remove('btn-primary'); selectPresetButton.classList.add('btn-secondary'); selectPresetButton.removeAttribute('data-presetname'); selectPresetButton.removeAttribute('data-cmid'); } }; amd/src/editpreset.js000064400000005066152265517740010627 0ustar00// 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 . /** * Javascript module for editing a database preset. * * @module mod_data/editpreset * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import Notification from 'core/notification'; import {getString} from 'core/str'; const selectors = { editPresetButton: '[data-action="editpreset"]', }; /** * Initialize module */ export const init = () => { registerEventListeners(); }; /** * Register events for update/delete links. */ const registerEventListeners = () => { document.addEventListener('click', (event) => { const editAction = event.target.closest(selectors.editPresetButton); if (editAction) { event.preventDefault(); showEditPresetModal(editAction); } }); }; /** * Show the edit preset modal. * * @param {HTMLElement} editAction the edit action element. */ const showEditPresetModal = (editAction) => { const modalForm = new ModalForm({ modalConfig: { title: getString('editpreset', 'mod_data'), }, formClass: 'mod_data\\form\\save_as_preset', args: { d: editAction.getAttribute('data-dataid'), action: editAction.getAttribute('data-action'), presetname: editAction.getAttribute('data-presetname'), presetdescription: editAction.getAttribute('data-presetdescription') }, saveButtonText: getString('save'), returnFocus: editAction, }); modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { if (event.detail.result) { window.location.reload(); } else { Notification.addNotification({ type: 'error', message: event.detail.errors.join('
') }); } }); modalForm.show(); }; amd/src/deletepreset.js000064400000006072152265517740011142 0ustar00// 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 . /** * Javascript module for deleting a database as a preset. * * @module mod_data/deletepreset * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Notification from 'core/notification'; import {prefetchStrings} from 'core/prefetch'; import {getString} from 'core/str'; import Ajax from 'core/ajax'; import Url from 'core/url'; const selectors = { deletePresetButton: '[data-action="deletepreset"]', }; /** * Initialize module */ export const init = () => { prefetchStrings('mod_data', [ 'deleteconfirm', 'deletewarning', ]); prefetchStrings('core', [ 'delete', ]); registerEventListeners(); }; /** * Register events for delete preset option in action menu. */ const registerEventListeners = () => { document.addEventListener('click', (event) => { const deleteOption = event.target.closest(selectors.deletePresetButton); if (deleteOption) { event.preventDefault(); deletePresetConfirm(deleteOption); } }); }; /** * Show the confirmation modal to delete the preset. * * @param {HTMLElement} deleteOption the element to delete. */ const deletePresetConfirm = (deleteOption) => { const presetName = deleteOption.getAttribute('data-presetname'); const dataId = deleteOption.getAttribute('data-dataid'); Notification.deleteCancelPromise( getString('deleteconfirm', 'mod_data', presetName), getString('deletewarning', 'mod_data'), ).then(() => { return deletePreset(dataId, presetName); }).catch(() => { return; }); }; /** * Delete site user preset. * * @param {int} dataId The id of the current database activity. * @param {string} presetName The preset name to delete. * @return {promise} Resolved with the result and warnings of deleting a preset. */ async function deletePreset(dataId, presetName) { var request = { methodname: 'mod_data_delete_saved_preset', args: { dataid: dataId, presetnames: {presetname: presetName}, } }; try { await Ajax.call([request])[0]; window.location.href = Url.relativeUrl( 'mod/data/preset.php', { d: dataId, }, false ); } catch (error) { Notification.exception(error); } } amd/src/importpresets.js000064400000004211152265517740011366 0ustar00// 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 . /** * Javascript module for importing presets. * * @module mod_data/importpresets * @copyright 2022 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import Notification from 'core/notification'; import {getString} from 'core/str'; const selectors = { importPresetButton: '[data-action="importpresets"]', }; /** * Initialize module */ export const init = () => { document.addEventListener('click', (event) => { const importPresetButton = event.target.closest(selectors.importPresetButton); if (!importPresetButton) { return; } event.preventDefault(); const modalForm = new ModalForm({ modalConfig: { title: getString('importpreset', 'mod_data'), }, formClass: 'mod_data\\form\\import_presets', args: {cmid: importPresetButton.dataset.dataid}, saveButtonText: getString('importandapply', 'mod_data'), }); modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { if (event.detail.result) { window.location.assign(event.detail.url); } else { Notification.addNotification({ type: 'error', message: event.detail.errors.join('
') }); } }); modalForm.show(); }); }; amd/src/importmappingdialogue.js000064400000011234152265517740013051 0ustar00// 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 . /** * Javascript module for deleting a database as a preset. * * @module mod_data/importmappingdialogue * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Notification from 'core/notification'; import Ajax from 'core/ajax'; import Url from 'core/url'; import Templates from 'core/templates'; import Modal from 'core/modal'; import {prefetchStrings} from 'core/prefetch'; import {getString} from 'core/str'; // Load global strings. prefetchStrings('mod_data', ['mapping:dialogtitle:usepreset']); const selectors = { selectPreset: '[data-action="selectpreset"]', }; /** * Initialize module */ export const init = () => { registerEventListeners(); }; /** * Register events. */ const registerEventListeners = () => { document.addEventListener('click', (event) => { const preset = event.target.closest(selectors.selectPreset); if (preset) { event.preventDefault(); showMappingDialogue(preset); } }); }; /** * Show the confirmation modal for uploading a preset. * * @param {HTMLElement} usepreset the preset to import. */ const showMappingDialogue = (usepreset) => { const presetName = usepreset.dataset.presetname; const cmId = usepreset.dataset.cmid; getMappingInformation(cmId, presetName).then((result) => { if (result.data && result.data.needsmapping) { buildModal({ title: getString('mapping:dialogtitle:usepreset', 'mod_data', result.data.presetname), body: Templates.render('mod_data/fields_mapping_body', result.data), footer: Templates.render('mod_data/fields_mapping_footer', getMappingButtons(cmId, presetName)), large: true, show: true, }); } else { window.location.href = Url.relativeUrl( 'mod/data/field.php', { id: cmId, mode: 'usepreset', fullname: presetName, }, false ); } return true; }).catch(Notification.exception); }; /** * Given an object we want to build a modal ready to show * * @method buildModal * @param {Object} params the modal params * @param {Promise} params.title * @param {Promise} params.body * @param {Promise} params.footer * @return {Object} The modal ready to display immediately and render body in later. */ const buildModal = (params) => { return Modal.create({ ...params, }).then(modal => { modal.showFooter(); modal.registerCloseOnCancel(); return modal; }).catch(Notification.exception); }; /** * Add buttons to render on mapping modal. * * @param {int} cmId The id of the current database activity. * @param {string} presetName The preset name to delete. * @return {array} Same data with buttons. */ const getMappingButtons = (cmId, presetName) => { const data = {}; data.mapfieldsbutton = Url.relativeUrl( 'mod/data/field.php', { id: cmId, fullname: presetName, mode: 'usepreset', action: 'select', }, false ); data.applybutton = Url.relativeUrl( 'mod/data/field.php', { id: cmId, fullname: presetName, mode: 'usepreset', action: 'notmapping' }, false ); return data; }; /** * Check whether we should show the mapping dialogue or not. * * @param {int} cmId The id of the current database activity. * @param {string} presetName The preset name to delete. * @return {promise} Resolved with the result and warnings of deleting a preset. */ const getMappingInformation = (cmId, presetName) => { const request = { methodname: 'mod_data_get_mapping_information', args: { cmid: cmId, importedpreset: presetName, } }; return Ajax.call([request])[0]; }; amd/src/templateseditor.js000064400000011365152265517740011663 0ustar00// 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 . /** * Javascript module to control the template editor. * * @module mod_data/templateseditor * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {getString} from 'core/str'; import {prefetchStrings} from 'core/prefetch'; import {relativeUrl} from 'core/url'; import {saveCancel, deleteCancel} from 'core/notification'; import Templates from 'core/templates'; prefetchStrings('admin', ['confirmation']); prefetchStrings('mod_data', [ 'resettemplateconfirmtitle', 'enabletemplateeditorcheck', 'editorenable' ]); prefetchStrings('core', [ 'reset', ]); /** * Template editor constants. */ const selectors = { toggleTemplateEditor: 'input[name="useeditor"]', resetTemplateAction: '[data-action="resettemplate"]', resetTemplate: 'input[name="defaultform"]', resetAllTemplates: 'input[name="resetall"]', resetAllCheck: 'input[name="resetallcheck"]', editForm: '#edittemplateform', }; /** * Register event listeners for the module. * * @param {Number} instanceId The database ID * @param {string} mode The template mode */ const registerEventListeners = (instanceId, mode) => { registerResetButton(mode); registerEditorToggler(instanceId, mode); }; const registerResetButton = (mode) => { const editForm = document.querySelector(selectors.editForm); const resetTemplate = document.querySelector(selectors.resetTemplate); const resetAllTemplates = document.querySelector(selectors.resetAllTemplates); const resetTemplateAction = document.querySelector(selectors.resetTemplateAction); if (!resetTemplateAction || !resetTemplate || !editForm) { return; } prefetchStrings('mod_data', [ mode ]); resetTemplateAction.addEventListener('click', async(event) => { event.preventDefault(); const params = { resetallname: "resetallcheck", templatename: await getString(mode, 'mod_data'), }; deleteCancel( getString('resettemplateconfirmtitle', 'mod_data'), Templates.render('mod_data/template_editor_resetmodal', params), getString('reset', 'core'), () => { resetTemplate.value = "true"; editForm.submit(); }, null, {triggerElement: event.target} ); }); // The reset all checkbox is inside a modal so we need to capture at document level. if (!resetAllTemplates) { return; } document.addEventListener('change', (event) => { if (event.target.matches(selectors.resetAllCheck)) { resetAllTemplates.value = (event.target.checked) ? "true" : ""; } }); }; const registerEditorToggler = (instanceId, mode) => { const toggleTemplateEditor = document.querySelector(selectors.toggleTemplateEditor); if (!toggleTemplateEditor) { return; } toggleTemplateEditor.addEventListener('click', async(event) => { event.preventDefault(); // Whether the event action attempts to enable or disable the template editor. const enableTemplateEditor = event.target.checked; if (enableTemplateEditor) { // Display a confirmation dialog before enabling the template editor. saveCancel( getString('confirmation', 'admin'), getString('enabletemplateeditorcheck', 'mod_data'), getString('editorenable', 'mod_data'), () => { window.location = relativeUrl('/mod/data/templates.php', {d: instanceId, mode: mode, useeditor: true}); }, null, {triggerElement: event.target} ); } else { window.location = relativeUrl('/mod/data/templates.php', {d: instanceId, mode: mode, useeditor: false}); } }); }; /** * Initialize the module. * * @param {int} instanceId The database ID * @param {string} mode The template mode */ export const init = (instanceId, mode) => { registerEventListeners(instanceId, mode); }; amd/src/resetalltemplates.js000064400000004364152265517740012211 0ustar00// 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 . /** * Javascript module for reseting all templates. * * @module mod_data/resetalltemplates * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Notification from 'core/notification'; import {prefetchStrings} from 'core/prefetch'; import {getString} from 'core/str'; const selectors = { resetAllTemplatesAction: '[data-action="resetalltemplates"]', }; /** * Initialize module */ export const init = () => { prefetchStrings('mod_data', [ 'resetalltemplatesconfirmtitle', 'resetalltemplatesconfirm', ]); prefetchStrings('core', [ 'reset', ]); registerEventListeners(); }; /** * Register events for option in action menu. */ const registerEventListeners = () => { document.addEventListener('click', (event) => { const actionLink = event.target.closest(selectors.resetAllTemplatesAction); if (actionLink) { event.preventDefault(); resetAllTemplatesConfirm(actionLink); } }); }; /** * Show the confirmation modal to reset all the templates. * * @param {HTMLElement} actionLink the element that triggers the action. */ const resetAllTemplatesConfirm = async(actionLink) => { try { await Notification.deleteCancelPromise( getString('resetalltemplatesconfirmtitle', 'mod_data'), getString('resetalltemplatesconfirm', 'mod_data'), getString('reset', 'core'), ); window.location = actionLink.href; } catch (error) { return; } }; amd/src/saveaspreset.js000064400000004336152265517740011163 0ustar00// 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 . /** * Javascript module for saving a database as a preset. * * @module mod_data/saveaspreset * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import ModalForm from 'core_form/modalform'; import Notification from 'core/notification'; import {getString} from 'core/str'; const selectors = { saveAsPresetButton: '[data-action="saveaspreset"]', }; /** * Initialize module. */ export const init = () => { document.addEventListener('click', (event) => { const saveAsPresetButton = event.target.closest(selectors.saveAsPresetButton); if (!saveAsPresetButton) { return; } event.preventDefault(); const modalForm = new ModalForm({ modalConfig: { title: getString('savedataaspreset', 'mod_data'), }, formClass: 'mod_data\\form\\save_as_preset', args: {d: saveAsPresetButton.dataset.dataid}, saveButtonText: getString('save'), returnFocus: saveAsPresetButton, }); // Show a toast notification when the form is submitted. modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => { if (event.detail.result) { window.location.reload(); } else { Notification.addNotification({ type: 'error', message: event.detail.errors.join('
') }); } }); modalForm.show(); }); }; amd/build/templateseditor.min.js.map000064400000015036152265517740013530 0ustar00{"version":3,"file":"templateseditor.min.js","sources":["../src/templateseditor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module to control the template editor.\n *\n * @module mod_data/templateseditor\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\nimport {relativeUrl} from 'core/url';\nimport {saveCancel, deleteCancel} from 'core/notification';\nimport Templates from 'core/templates';\n\nprefetchStrings('admin', ['confirmation']);\nprefetchStrings('mod_data', [\n 'resettemplateconfirmtitle',\n 'enabletemplateeditorcheck',\n 'editorenable'\n]);\nprefetchStrings('core', [\n 'reset',\n]);\n\n/**\n * Template editor constants.\n */\nconst selectors = {\n toggleTemplateEditor: 'input[name=\"useeditor\"]',\n resetTemplateAction: '[data-action=\"resettemplate\"]',\n resetTemplate: 'input[name=\"defaultform\"]',\n resetAllTemplates: 'input[name=\"resetall\"]',\n resetAllCheck: 'input[name=\"resetallcheck\"]',\n editForm: '#edittemplateform',\n};\n\n/**\n * Register event listeners for the module.\n *\n * @param {Number} instanceId The database ID\n * @param {string} mode The template mode\n */\nconst registerEventListeners = (instanceId, mode) => {\n registerResetButton(mode);\n registerEditorToggler(instanceId, mode);\n};\n\nconst registerResetButton = (mode) => {\n const editForm = document.querySelector(selectors.editForm);\n const resetTemplate = document.querySelector(selectors.resetTemplate);\n const resetAllTemplates = document.querySelector(selectors.resetAllTemplates);\n const resetTemplateAction = document.querySelector(selectors.resetTemplateAction);\n\n if (!resetTemplateAction || !resetTemplate || !editForm) {\n return;\n }\n prefetchStrings('mod_data', [\n mode\n ]);\n resetTemplateAction.addEventListener('click', async(event) => {\n event.preventDefault();\n const params = {\n resetallname: \"resetallcheck\",\n templatename: await getString(mode, 'mod_data'),\n };\n deleteCancel(\n getString('resettemplateconfirmtitle', 'mod_data'),\n Templates.render('mod_data/template_editor_resetmodal', params),\n getString('reset', 'core'),\n () => {\n resetTemplate.value = \"true\";\n editForm.submit();\n },\n null,\n {triggerElement: event.target}\n );\n });\n\n // The reset all checkbox is inside a modal so we need to capture at document level.\n if (!resetAllTemplates) {\n return;\n }\n document.addEventListener('change', (event) => {\n if (event.target.matches(selectors.resetAllCheck)) {\n resetAllTemplates.value = (event.target.checked) ? \"true\" : \"\";\n }\n });\n};\n\nconst registerEditorToggler = (instanceId, mode) => {\n const toggleTemplateEditor = document.querySelector(selectors.toggleTemplateEditor);\n\n if (!toggleTemplateEditor) {\n return;\n }\n\n toggleTemplateEditor.addEventListener('click', async(event) => {\n event.preventDefault();\n // Whether the event action attempts to enable or disable the template editor.\n const enableTemplateEditor = event.target.checked;\n\n if (enableTemplateEditor) {\n // Display a confirmation dialog before enabling the template editor.\n saveCancel(\n getString('confirmation', 'admin'),\n getString('enabletemplateeditorcheck', 'mod_data'),\n getString('editorenable', 'mod_data'),\n () => {\n window.location = relativeUrl('/mod/data/templates.php', {d: instanceId, mode: mode, useeditor: true});\n },\n null,\n {triggerElement: event.target}\n );\n } else {\n window.location = relativeUrl('/mod/data/templates.php', {d: instanceId, mode: mode, useeditor: false});\n }\n });\n};\n\n/**\n * Initialize the module.\n *\n * @param {int} instanceId The database ID\n * @param {string} mode The template mode\n */\nexport const init = (instanceId, mode) => {\n registerEventListeners(instanceId, mode);\n};\n"],"names":["selectors","registerResetButton","mode","editForm","document","querySelector","resetTemplate","resetAllTemplates","resetTemplateAction","addEventListener","async","event","preventDefault","params","resetallname","templatename","Templates","render","value","submit","triggerElement","target","matches","checked","registerEditorToggler","instanceId","toggleTemplateEditor","window","location","d","useeditor","registerEventListeners"],"mappings":";;;;;;;8KA6BgB,QAAS,CAAC,+CACV,WAAY,CACxB,4BACA,4BACA,+CAEY,OAAQ,CACpB,gBAMEA,+BACoB,0BADpBA,8BAEmB,gCAFnBA,wBAGa,4BAHbA,4BAIiB,yBAJjBA,wBAKa,8BALbA,mBAMQ,oBAcRC,oBAAuBC,aACnBC,SAAWC,SAASC,cAAcL,oBAClCM,cAAgBF,SAASC,cAAcL,yBACvCO,kBAAoBH,SAASC,cAAcL,6BAC3CQ,oBAAsBJ,SAASC,cAAcL,+BAE9CQ,qBAAwBF,eAAkBH,yCAG/B,WAAY,CACxBD,OAEJM,oBAAoBC,iBAAiB,SAASC,MAAAA,QAC1CC,MAAMC,uBACAC,OAAS,CACXC,aAAc,gBACdC,mBAAoB,kBAAUb,KAAM,6CAGpC,kBAAU,4BAA6B,YACvCc,mBAAUC,OAAO,sCAAuCJ,SACxD,kBAAU,QAAS,SACnB,KACIP,cAAcY,MAAQ,OACtBf,SAASgB,WAEb,KACA,CAACC,eAAgBT,MAAMU,YAK1Bd,mBAGLH,SAASK,iBAAiB,UAAWE,QAC7BA,MAAMU,OAAOC,QAAQtB,2BACrBO,kBAAkBW,MAASP,MAAMU,OAAOE,QAAW,OAAS,SAKlEC,sBAAwB,CAACC,WAAYvB,cACjCwB,qBAAuBtB,SAASC,cAAcL,gCAE/C0B,sBAILA,qBAAqBjB,iBAAiB,SAASC,MAAAA,QAC3CC,MAAMC,iBAEuBD,MAAMU,OAAOE,sCAKlC,kBAAU,eAAgB,UAC1B,kBAAU,4BAA6B,aACvC,kBAAU,eAAgB,aAC1B,KACII,OAAOC,UAAW,oBAAY,0BAA2B,CAACC,EAAGJ,WAAYvB,KAAMA,KAAM4B,WAAW,MAEpG,KACA,CAACV,eAAgBT,MAAMU,SAG3BM,OAAOC,UAAW,oBAAY,0BAA2B,CAACC,EAAGJ,WAAYvB,KAAMA,KAAM4B,WAAW,sBAWxF,CAACL,WAAYvB,QAnFF,EAACuB,WAAYvB,QACxCD,oBAAoBC,MACpBsB,sBAAsBC,WAAYvB,OAkFlC6B,CAAuBN,WAAYvB"}amd/build/editpreset.min.js000064400000003242152265517740011713 0ustar00define("mod_data/editpreset",["exports","core_form/modalform","core/notification","core/str"],(function(_exports,_modalform,_notification,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for editing a database preset. * * @module mod_data/editpreset * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification);const selectors_editPresetButton='[data-action="editpreset"]';_exports.init=()=>{registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const editAction=event.target.closest(selectors_editPresetButton);editAction&&(event.preventDefault(),showEditPresetModal(editAction))}))},showEditPresetModal=editAction=>{const modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)("editpreset","mod_data")},formClass:"mod_data\\form\\save_as_preset",args:{d:editAction.getAttribute("data-dataid"),action:editAction.getAttribute("data-action"),presetname:editAction.getAttribute("data-presetname"),presetdescription:editAction.getAttribute("data-presetdescription")},saveButtonText:(0,_str.getString)("save"),returnFocus:editAction});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{event.detail.result?window.location.reload():_notification.default.addNotification({type:"error",message:event.detail.errors.join("
")})})),modalForm.show()}})); //# sourceMappingURL=editpreset.min.js.mapamd/build/selectpreset.min.js000064400000003237152265517740012251 0ustar00define("mod_data/selectpreset",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Javascript module to control the form responsible for selecting a preset. * * @module mod_data/selectpreset * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const selectors_presetRadioButton='input[name="fullname"]',selectors_selectPresetButton='input[name="selectpreset"]',selectors_selectedPresetRadioButton='input[name="fullname"]:checked';_exports.init=()=>{const radioButton=document.querySelectorAll(selectors_presetRadioButton);disableUsePresetButton(),radioButton.forEach((elem=>{elem.addEventListener("change",(function(event){event.preventDefault(),disableUsePresetButton()}))}))};const disableUsePresetButton=()=>{let selectPresetButton=document.querySelector(selectors_selectPresetButton);const selectedRadioButton=document.querySelector(selectors_selectedPresetRadioButton);selectedRadioButton?(selectPresetButton.removeAttribute("disabled"),selectPresetButton.classList.remove("btn-secondary"),selectPresetButton.classList.add("btn-primary"),selectPresetButton.setAttribute("data-presetname",selectedRadioButton.getAttribute("value")),selectPresetButton.setAttribute("data-cmid",selectedRadioButton.getAttribute("data-cmid"))):(selectPresetButton.setAttribute("disabled",!0),selectPresetButton.classList.remove("btn-primary"),selectPresetButton.classList.add("btn-secondary"),selectPresetButton.removeAttribute("data-presetname"),selectPresetButton.removeAttribute("data-cmid"))}})); //# sourceMappingURL=selectpreset.min.js.mapamd/build/deletepreset.min.js000064400000003530152265517740012230 0ustar00define("mod_data/deletepreset",["exports","core/notification","core/prefetch","core/str","core/ajax","core/url"],(function(_exports,_notification,_prefetch,_str,_ajax,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for deleting a database as a preset. * * @module mod_data/deletepreset * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_ajax=_interopRequireDefault(_ajax),_url=_interopRequireDefault(_url);const selectors_deletePresetButton='[data-action="deletepreset"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("mod_data",["deleteconfirm","deletewarning"]),(0,_prefetch.prefetchStrings)("core",["delete"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const deleteOption=event.target.closest(selectors_deletePresetButton);deleteOption&&(event.preventDefault(),deletePresetConfirm(deleteOption))}))},deletePresetConfirm=deleteOption=>{const presetName=deleteOption.getAttribute("data-presetname"),dataId=deleteOption.getAttribute("data-dataid");_notification.default.deleteCancelPromise((0,_str.getString)("deleteconfirm","mod_data",presetName),(0,_str.getString)("deletewarning","mod_data")).then((()=>async function(dataId,presetName){var request={methodname:"mod_data_delete_saved_preset",args:{dataid:dataId,presetnames:{presetname:presetName}}};try{await _ajax.default.call([request])[0],window.location.href=_url.default.relativeUrl("mod/data/preset.php",{d:dataId},!1)}catch(error){_notification.default.exception(error)}}(dataId,presetName))).catch((()=>{}))}})); //# sourceMappingURL=deletepreset.min.js.mapamd/build/selectpreset.min.js.map000064400000007631152265517740013027 0ustar00{"version":3,"file":"selectpreset.min.js","sources":["../src/selectpreset.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module to control the form responsible for selecting a preset.\n *\n * @module mod_data/selectpreset\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst selectors = {\n presetRadioButton: 'input[name=\"fullname\"]',\n selectPresetButton: 'input[name=\"selectpreset\"]',\n selectedPresetRadioButton: 'input[name=\"fullname\"]:checked',\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n const radioButton = document.querySelectorAll(selectors.presetRadioButton);\n\n // Initialize the \"Use a preset\" button properly.\n disableUsePresetButton();\n\n radioButton.forEach((elem) => {\n elem.addEventListener('change', function(event) {\n event.preventDefault();\n // Enable the \"Use a preset\" button when any of the radio buttons in the presets list is checked.\n disableUsePresetButton();\n });\n });\n\n};\n\n/**\n * Decide whether to disable or not the \"Use a preset\" button.\n * When there is no preset selected, the button should be displayed disabled; otherwise, it will appear enabled as a primary button.\n *\n * @method\n * @private\n */\nconst disableUsePresetButton = () => {\n let selectPresetButton = document.querySelector(selectors.selectPresetButton);\n const selectedRadioButton = document.querySelector(selectors.selectedPresetRadioButton);\n\n if (selectedRadioButton) {\n // There is one preset selected, so the button should be enabled.\n selectPresetButton.removeAttribute('disabled');\n selectPresetButton.classList.remove('btn-secondary');\n selectPresetButton.classList.add('btn-primary');\n selectPresetButton.setAttribute('data-presetname', selectedRadioButton.getAttribute('value'));\n selectPresetButton.setAttribute('data-cmid', selectedRadioButton.getAttribute('data-cmid'));\n } else {\n // There is no any preset selected, so the button should be disabled.\n selectPresetButton.setAttribute('disabled', true);\n selectPresetButton.classList.remove('btn-primary');\n selectPresetButton.classList.add('btn-secondary');\n selectPresetButton.removeAttribute('data-presetname');\n selectPresetButton.removeAttribute('data-cmid');\n }\n};\n"],"names":["selectors","radioButton","document","querySelectorAll","disableUsePresetButton","forEach","elem","addEventListener","event","preventDefault","selectPresetButton","querySelector","selectedRadioButton","removeAttribute","classList","remove","add","setAttribute","getAttribute"],"mappings":";;;;;;;;MAuBMA,4BACiB,yBADjBA,6BAEkB,6BAFlBA,oCAGyB,+CAMX,WACVC,YAAcC,SAASC,iBAAiBH,6BAG9CI,yBAEAH,YAAYI,SAASC,OACjBA,KAAKC,iBAAiB,UAAU,SAASC,OACrCA,MAAMC,iBAENL,sCAaNA,uBAAyB,SACvBM,mBAAqBR,SAASS,cAAcX,oCAC1CY,oBAAsBV,SAASS,cAAcX,qCAE/CY,qBAEAF,mBAAmBG,gBAAgB,YACnCH,mBAAmBI,UAAUC,OAAO,iBACpCL,mBAAmBI,UAAUE,IAAI,eACjCN,mBAAmBO,aAAa,kBAAmBL,oBAAoBM,aAAa,UACpFR,mBAAmBO,aAAa,YAAaL,oBAAoBM,aAAa,gBAG9ER,mBAAmBO,aAAa,YAAY,GAC5CP,mBAAmBI,UAAUC,OAAO,eACpCL,mBAAmBI,UAAUE,IAAI,iBACjCN,mBAAmBG,gBAAgB,mBACnCH,mBAAmBG,gBAAgB"}amd/build/importpresets.min.js000064400000002662152265517740012470 0ustar00define("mod_data/importpresets",["exports","core_form/modalform","core/notification","core/str"],(function(_exports,_modalform,_notification,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for importing presets. * * @module mod_data/importpresets * @copyright 2022 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification);const selectors_importPresetButton='[data-action="importpresets"]';_exports.init=()=>{document.addEventListener("click",(event=>{const importPresetButton=event.target.closest(selectors_importPresetButton);if(!importPresetButton)return;event.preventDefault();const modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)("importpreset","mod_data")},formClass:"mod_data\\form\\import_presets",args:{cmid:importPresetButton.dataset.dataid},saveButtonText:(0,_str.getString)("importandapply","mod_data")});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{event.detail.result?window.location.assign(event.detail.url):_notification.default.addNotification({type:"error",message:event.detail.errors.join("
")})})),modalForm.show()}))}})); //# sourceMappingURL=importpresets.min.js.mapamd/build/editpreset.min.js.map000064400000007316152265517740012475 0ustar00{"version":3,"file":"editpreset.min.js","sources":["../src/editpreset.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for editing a database preset.\n *\n * @module mod_data/editpreset\n * @copyright 2022 Sara Arjona \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport {getString} from 'core/str';\n\nconst selectors = {\n editPresetButton: '[data-action=\"editpreset\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n registerEventListeners();\n};\n\n/**\n * Register events for update/delete links.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const editAction = event.target.closest(selectors.editPresetButton);\n if (editAction) {\n event.preventDefault();\n showEditPresetModal(editAction);\n }\n });\n};\n\n/**\n * Show the edit preset modal.\n *\n * @param {HTMLElement} editAction the edit action element.\n */\nconst showEditPresetModal = (editAction) => {\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('editpreset', 'mod_data'),\n },\n formClass: 'mod_data\\\\form\\\\save_as_preset',\n args: {\n d: editAction.getAttribute('data-dataid'),\n action: editAction.getAttribute('data-action'),\n presetname: editAction.getAttribute('data-presetname'),\n presetdescription: editAction.getAttribute('data-presetdescription')\n },\n saveButtonText: getString('save'),\n returnFocus: editAction,\n });\n\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n if (event.detail.result) {\n window.location.reload();\n } else {\n Notification.addNotification({\n type: 'error',\n message: event.detail.errors.join('
')\n });\n }\n });\n\n modalForm.show();\n};\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","editAction","target","closest","preventDefault","showEditPresetModal","modalForm","ModalForm","modalConfig","title","formClass","args","d","getAttribute","action","presetname","presetdescription","saveButtonText","returnFocus","events","FORM_SUBMITTED","detail","result","window","location","reload","addNotification","type","message","errors","join","show"],"mappings":";;;;;;;0LA2BMA,2BACgB,2CAMF,KAChBC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,WAAaD,MAAME,OAAOC,QAAQP,4BACpCK,aACAD,MAAMI,iBACNC,oBAAoBJ,iBAU1BI,oBAAuBJ,mBACnBK,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,kBAAU,aAAc,aAEnCC,UAAW,iCACXC,KAAM,CACFC,EAAGX,WAAWY,aAAa,eAC3BC,OAAQb,WAAWY,aAAa,eAChCE,WAAYd,WAAWY,aAAa,mBACpCG,kBAAmBf,WAAWY,aAAa,2BAE/CI,gBAAgB,kBAAU,QAC1BC,YAAajB,aAGjBK,UAAUP,iBAAiBO,UAAUa,OAAOC,gBAAgBpB,QACpDA,MAAMqB,OAAOC,OACbC,OAAOC,SAASC,+BAEHC,gBAAgB,CACzBC,KAAM,QACNC,QAAS5B,MAAMqB,OAAOQ,OAAOC,KAAK,aAK9CxB,UAAUyB"}amd/build/importmappingdialogue.min.js000064400000005120152265517740014140 0ustar00define("mod_data/importmappingdialogue",["exports","core/notification","core/ajax","core/url","core/templates","core/modal","core/prefetch","core/str"],(function(_exports,_notification,_ajax,_url,_templates,_modal,_prefetch,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for deleting a database as a preset. * * @module mod_data/importmappingdialogue * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_ajax=_interopRequireDefault(_ajax),_url=_interopRequireDefault(_url),_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),(0,_prefetch.prefetchStrings)("mod_data",["mapping:dialogtitle:usepreset"]);const selectors_selectPreset='[data-action="selectpreset"]';_exports.init=()=>{registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const preset=event.target.closest(selectors_selectPreset);preset&&(event.preventDefault(),showMappingDialogue(preset))}))},showMappingDialogue=usepreset=>{const presetName=usepreset.dataset.presetname,cmId=usepreset.dataset.cmid;getMappingInformation(cmId,presetName).then((result=>(result.data&&result.data.needsmapping?buildModal({title:(0,_str.getString)("mapping:dialogtitle:usepreset","mod_data",result.data.presetname),body:_templates.default.render("mod_data/fields_mapping_body",result.data),footer:_templates.default.render("mod_data/fields_mapping_footer",getMappingButtons(cmId,presetName)),large:!0,show:!0}):window.location.href=_url.default.relativeUrl("mod/data/field.php",{id:cmId,mode:"usepreset",fullname:presetName},!1),!0))).catch(_notification.default.exception)},buildModal=params=>_modal.default.create({...params}).then((modal=>(modal.showFooter(),modal.registerCloseOnCancel(),modal))).catch(_notification.default.exception),getMappingButtons=(cmId,presetName)=>{const data={};return data.mapfieldsbutton=_url.default.relativeUrl("mod/data/field.php",{id:cmId,fullname:presetName,mode:"usepreset",action:"select"},!1),data.applybutton=_url.default.relativeUrl("mod/data/field.php",{id:cmId,fullname:presetName,mode:"usepreset",action:"notmapping"},!1),data},getMappingInformation=(cmId,presetName)=>{const request={methodname:"mod_data_get_mapping_information",args:{cmid:cmId,importedpreset:presetName}};return _ajax.default.call([request])[0]}})); //# sourceMappingURL=importmappingdialogue.min.js.mapamd/build/deletepreset.min.js.map000064400000010311152265517740012777 0ustar00{"version":3,"file":"deletepreset.min.js","sources":["../src/deletepreset.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for deleting a database as a preset.\n *\n * @module mod_data/deletepreset\n * @copyright 2022 Amaia Anabitarte \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport Ajax from 'core/ajax';\nimport Url from 'core/url';\n\nconst selectors = {\n deletePresetButton: '[data-action=\"deletepreset\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n prefetchStrings('mod_data', [\n 'deleteconfirm',\n 'deletewarning',\n ]);\n prefetchStrings('core', [\n 'delete',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register events for delete preset option in action menu.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const deleteOption = event.target.closest(selectors.deletePresetButton);\n if (deleteOption) {\n event.preventDefault();\n deletePresetConfirm(deleteOption);\n }\n });\n};\n\n/**\n * Show the confirmation modal to delete the preset.\n *\n * @param {HTMLElement} deleteOption the element to delete.\n */\nconst deletePresetConfirm = (deleteOption) => {\n const presetName = deleteOption.getAttribute('data-presetname');\n const dataId = deleteOption.getAttribute('data-dataid');\n\n Notification.deleteCancelPromise(\n getString('deleteconfirm', 'mod_data', presetName),\n getString('deletewarning', 'mod_data'),\n ).then(() => {\n return deletePreset(dataId, presetName);\n }).catch(() => {\n return;\n });\n};\n\n/**\n * Delete site user preset.\n *\n * @param {int} dataId The id of the current database activity.\n * @param {string} presetName The preset name to delete.\n * @return {promise} Resolved with the result and warnings of deleting a preset.\n */\nasync function deletePreset(dataId, presetName) {\n var request = {\n methodname: 'mod_data_delete_saved_preset',\n args: {\n dataid: dataId,\n presetnames: {presetname: presetName},\n }\n };\n try {\n await Ajax.call([request])[0];\n window.location.href = Url.relativeUrl(\n 'mod/data/preset.php',\n {\n d: dataId,\n },\n false\n );\n } catch (error) {\n Notification.exception(error);\n }\n}\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","deleteOption","target","closest","preventDefault","deletePresetConfirm","presetName","getAttribute","dataId","deleteCancelPromise","then","request","methodname","args","dataid","presetnames","presetname","Ajax","call","window","location","href","Url","relativeUrl","d","error","exception","deletePreset","catch"],"mappings":";;;;;;;kNA6BMA,6BACkB,6CAMJ,mCACA,WAAY,CACxB,gBACA,gDAEY,OAAQ,CACpB,WAGJC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,aAAeD,MAAME,OAAOC,QAAQP,8BACtCK,eACAD,MAAMI,iBACNC,oBAAoBJ,mBAU1BI,oBAAuBJ,qBACnBK,WAAaL,aAAaM,aAAa,mBACvCC,OAASP,aAAaM,aAAa,qCAE5BE,qBACT,kBAAU,gBAAiB,WAAYH,aACvC,kBAAU,gBAAiB,aAC7BI,MAAK,mBAciBF,OAAQF,gBAC5BK,QAAU,CACVC,WAAY,+BACZC,KAAM,CACFC,OAAQN,OACRO,YAAa,CAACC,WAAYV,wBAIxBW,cAAKC,KAAK,CAACP,UAAU,GAC3BQ,OAAOC,SAASC,KAAOC,aAAIC,YACvB,sBACA,CACIC,EAAGhB,SAEP,GAEN,MAAOiB,6BACQC,UAAUD,QA/BhBE,CAAanB,OAAQF,cAC7BsB,OAAM"}amd/build/resetalltemplates.min.js000064400000002570152265517740013300 0ustar00define("mod_data/resetalltemplates",["exports","core/notification","core/prefetch","core/str"],(function(_exports,_notification,_prefetch,_str){var obj; /** * Javascript module for reseting all templates. * * @module mod_data/resetalltemplates * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const selectors_resetAllTemplatesAction='[data-action="resetalltemplates"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("mod_data",["resetalltemplatesconfirmtitle","resetalltemplatesconfirm"]),(0,_prefetch.prefetchStrings)("core",["reset"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const actionLink=event.target.closest(selectors_resetAllTemplatesAction);actionLink&&(event.preventDefault(),resetAllTemplatesConfirm(actionLink))}))},resetAllTemplatesConfirm=async actionLink=>{try{await _notification.default.deleteCancelPromise((0,_str.getString)("resetalltemplatesconfirmtitle","mod_data"),(0,_str.getString)("resetalltemplatesconfirm","mod_data"),(0,_str.getString)("reset","core")),window.location=actionLink.href}catch(error){return}}})); //# sourceMappingURL=resetalltemplates.min.js.mapamd/build/resetalltemplates.min.js.map000064400000005743152265517740014061 0ustar00{"version":3,"file":"resetalltemplates.min.js","sources":["../src/resetalltemplates.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for reseting all templates.\n *\n * @module mod_data/resetalltemplates\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\n\nconst selectors = {\n resetAllTemplatesAction: '[data-action=\"resetalltemplates\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n prefetchStrings('mod_data', [\n 'resetalltemplatesconfirmtitle',\n 'resetalltemplatesconfirm',\n ]);\n prefetchStrings('core', [\n 'reset',\n ]);\n registerEventListeners();\n};\n\n/**\n * Register events for option in action menu.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const actionLink = event.target.closest(selectors.resetAllTemplatesAction);\n if (actionLink) {\n event.preventDefault();\n resetAllTemplatesConfirm(actionLink);\n }\n });\n};\n\n/**\n * Show the confirmation modal to reset all the templates.\n *\n * @param {HTMLElement} actionLink the element that triggers the action.\n */\nconst resetAllTemplatesConfirm = async(actionLink) => {\n try {\n await Notification.deleteCancelPromise(\n getString('resetalltemplatesconfirmtitle', 'mod_data'),\n getString('resetalltemplatesconfirm', 'mod_data'),\n getString('reset', 'core'),\n );\n window.location = actionLink.href;\n } catch (error) {\n return;\n }\n};\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","actionLink","target","closest","preventDefault","resetAllTemplatesConfirm","async","Notification","deleteCancelPromise","window","location","href","error"],"mappings":";;;;;;;4JA2BMA,kCACuB,kDAMT,mCACA,WAAY,CACxB,gCACA,2DAEY,OAAQ,CACpB,UAEJC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,WAAaD,MAAME,OAAOC,QAAQP,mCACpCK,aACAD,MAAMI,iBACNC,yBAAyBJ,iBAU/BI,yBAA2BC,MAAAA,uBAEnBC,sBAAaC,qBACf,kBAAU,gCAAiC,aAC3C,kBAAU,2BAA4B,aACtC,kBAAU,QAAS,SAEvBC,OAAOC,SAAWT,WAAWU,KAC/B,MAAOC"}amd/build/importmappingdialogue.min.js.map000064400000015107152265517740014722 0ustar00{"version":3,"file":"importmappingdialogue.min.js","sources":["../src/importmappingdialogue.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for deleting a database as a preset.\n *\n * @module mod_data/importmappingdialogue\n * @copyright 2022 Amaia Anabitarte \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport Ajax from 'core/ajax';\nimport Url from 'core/url';\nimport Templates from 'core/templates';\nimport Modal from 'core/modal';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\n\n// Load global strings.\nprefetchStrings('mod_data', ['mapping:dialogtitle:usepreset']);\n\nconst selectors = {\n selectPreset: '[data-action=\"selectpreset\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n registerEventListeners();\n};\n\n/**\n * Register events.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const preset = event.target.closest(selectors.selectPreset);\n if (preset) {\n event.preventDefault();\n showMappingDialogue(preset);\n }\n });\n};\n\n/**\n * Show the confirmation modal for uploading a preset.\n *\n * @param {HTMLElement} usepreset the preset to import.\n */\nconst showMappingDialogue = (usepreset) => {\n const presetName = usepreset.dataset.presetname;\n const cmId = usepreset.dataset.cmid;\n\n getMappingInformation(cmId, presetName).then((result) => {\n if (result.data && result.data.needsmapping) {\n buildModal({\n title: getString('mapping:dialogtitle:usepreset', 'mod_data', result.data.presetname),\n body: Templates.render('mod_data/fields_mapping_body', result.data),\n footer: Templates.render('mod_data/fields_mapping_footer', getMappingButtons(cmId, presetName)),\n large: true,\n show: true,\n });\n } else {\n window.location.href = Url.relativeUrl(\n 'mod/data/field.php',\n {\n id: cmId,\n mode: 'usepreset',\n fullname: presetName,\n },\n false\n );\n }\n return true;\n }).catch(Notification.exception);\n};\n\n/**\n * Given an object we want to build a modal ready to show\n *\n * @method buildModal\n * @param {Object} params the modal params\n * @param {Promise} params.title\n * @param {Promise} params.body\n * @param {Promise} params.footer\n * @return {Object} The modal ready to display immediately and render body in later.\n */\nconst buildModal = (params) => {\n return Modal.create({\n ...params,\n }).then(modal => {\n modal.showFooter();\n modal.registerCloseOnCancel();\n return modal;\n }).catch(Notification.exception);\n};\n\n/**\n * Add buttons to render on mapping modal.\n *\n * @param {int} cmId The id of the current database activity.\n * @param {string} presetName The preset name to delete.\n * @return {array} Same data with buttons.\n */\nconst getMappingButtons = (cmId, presetName) => {\n const data = {};\n\n data.mapfieldsbutton = Url.relativeUrl(\n 'mod/data/field.php',\n {\n id: cmId,\n fullname: presetName,\n mode: 'usepreset',\n action: 'select',\n },\n false\n );\n\n data.applybutton = Url.relativeUrl(\n 'mod/data/field.php',\n {\n id: cmId,\n fullname: presetName,\n mode: 'usepreset',\n action: 'notmapping'\n },\n false\n );\n\n return data;\n};\n\n/**\n * Check whether we should show the mapping dialogue or not.\n *\n * @param {int} cmId The id of the current database activity.\n * @param {string} presetName The preset name to delete.\n * @return {promise} Resolved with the result and warnings of deleting a preset.\n */\nconst getMappingInformation = (cmId, presetName) => {\n const request = {\n methodname: 'mod_data_get_mapping_information',\n args: {\n cmid: cmId,\n importedpreset: presetName,\n }\n };\n return Ajax.call([request])[0];\n};\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","preset","target","closest","preventDefault","showMappingDialogue","usepreset","presetName","dataset","presetname","cmId","cmid","getMappingInformation","then","result","data","needsmapping","buildModal","title","body","Templates","render","footer","getMappingButtons","large","show","window","location","href","Url","relativeUrl","id","mode","fullname","catch","Notification","exception","params","Modal","create","modal","showFooter","registerCloseOnCancel","mapfieldsbutton","action","applybutton","request","methodname","args","importedpreset","Ajax","call"],"mappings":";;;;;;;8TAgCgB,WAAY,CAAC,wCAEvBA,uBACY,6CAME,KAChBC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,OAASD,MAAME,OAAOC,QAAQP,wBAChCK,SACAD,MAAMI,iBACNC,oBAAoBJ,aAU1BI,oBAAuBC,kBACnBC,WAAaD,UAAUE,QAAQC,WAC/BC,KAAOJ,UAAUE,QAAQG,KAE/BC,sBAAsBF,KAAMH,YAAYM,MAAMC,SACtCA,OAAOC,MAAQD,OAAOC,KAAKC,aAC3BC,WAAW,CACPC,OAAO,kBAAU,gCAAiC,WAAYJ,OAAOC,KAAKN,YAC1EU,KAAMC,mBAAUC,OAAO,+BAAgCP,OAAOC,MAC9DO,OAAQF,mBAAUC,OAAO,iCAAkCE,kBAAkBb,KAAMH,aACnFiB,OAAO,EACPC,MAAM,IAGVC,OAAOC,SAASC,KAAOC,aAAIC,YACvB,qBACA,CACIC,GAAIrB,KACJsB,KAAM,YACNC,SAAU1B,aAEd,IAGD,KACR2B,MAAMC,sBAAaC,YAapBnB,WAAcoB,QACTC,eAAMC,OAAO,IACbF,SACJxB,MAAK2B,QACJA,MAAMC,aACND,MAAME,wBACCF,SACRN,MAAMC,sBAAaC,WAUpBb,kBAAoB,CAACb,KAAMH,oBACvBQ,KAAO,UAEbA,KAAK4B,gBAAkBd,aAAIC,YACvB,qBACA,CACIC,GAAIrB,KACJuB,SAAU1B,WACVyB,KAAM,YACNY,OAAQ,WAEZ,GAGJ7B,KAAK8B,YAAchB,aAAIC,YACnB,qBACA,CACIC,GAAIrB,KACJuB,SAAU1B,WACVyB,KAAM,YACNY,OAAQ,eAEZ,GAGG7B,MAULH,sBAAwB,CAACF,KAAMH,oBAC3BuC,QAAU,CACZC,WAAY,mCACZC,KAAM,CACFrC,KAAMD,KACNuC,eAAgB1C,oBAGjB2C,cAAKC,KAAK,CAACL,UAAU"}amd/build/saveaspreset.min.js.map000064400000006244152265517740013031 0ustar00{"version":3,"file":"saveaspreset.min.js","sources":["../src/saveaspreset.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for saving a database as a preset.\n *\n * @module mod_data/saveaspreset\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport {getString} from 'core/str';\n\nconst selectors = {\n saveAsPresetButton: '[data-action=\"saveaspreset\"]',\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n\n document.addEventListener('click', (event) => {\n const saveAsPresetButton = event.target.closest(selectors.saveAsPresetButton);\n\n if (!saveAsPresetButton) {\n return;\n }\n\n event.preventDefault();\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('savedataaspreset', 'mod_data'),\n },\n formClass: 'mod_data\\\\form\\\\save_as_preset',\n args: {d: saveAsPresetButton.dataset.dataid},\n saveButtonText: getString('save'),\n returnFocus: saveAsPresetButton,\n });\n\n // Show a toast notification when the form is submitted.\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n if (event.detail.result) {\n window.location.reload();\n } else {\n Notification.addNotification({\n type: 'error',\n message: event.detail.errors.join('
')\n });\n }\n });\n\n modalForm.show();\n });\n};\n"],"names":["selectors","document","addEventListener","event","saveAsPresetButton","target","closest","preventDefault","modalForm","ModalForm","modalConfig","title","formClass","args","d","dataset","dataid","saveButtonText","returnFocus","events","FORM_SUBMITTED","detail","result","window","location","reload","addNotification","type","message","errors","join","show"],"mappings":";;;;;;;0LA2BMA,6BACkB,6CAMJ,KAEhBC,SAASC,iBAAiB,SAAUC,cAC1BC,mBAAqBD,MAAME,OAAOC,QAAQN,kCAE3CI,0BAILD,MAAMI,uBACAC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,kBAAU,mBAAoB,aAEzCC,UAAW,iCACXC,KAAM,CAACC,EAAGV,mBAAmBW,QAAQC,QACrCC,gBAAgB,kBAAU,QAC1BC,YAAad,qBAIjBI,UAAUN,iBAAiBM,UAAUW,OAAOC,gBAAgBjB,QACpDA,MAAMkB,OAAOC,OACbC,OAAOC,SAASC,+BAEHC,gBAAgB,CACzBC,KAAM,QACNC,QAAUzB,MAAMkB,OAAOQ,OAAOC,KAAK,aAK/CtB,UAAUuB"}amd/build/saveaspreset.min.js000064400000002663152265517740012256 0ustar00define("mod_data/saveaspreset",["exports","core_form/modalform","core/notification","core/str"],(function(_exports,_modalform,_notification,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Javascript module for saving a database as a preset. * * @module mod_data/saveaspreset * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modalform=_interopRequireDefault(_modalform),_notification=_interopRequireDefault(_notification);const selectors_saveAsPresetButton='[data-action="saveaspreset"]';_exports.init=()=>{document.addEventListener("click",(event=>{const saveAsPresetButton=event.target.closest(selectors_saveAsPresetButton);if(!saveAsPresetButton)return;event.preventDefault();const modalForm=new _modalform.default({modalConfig:{title:(0,_str.getString)("savedataaspreset","mod_data")},formClass:"mod_data\\form\\save_as_preset",args:{d:saveAsPresetButton.dataset.dataid},saveButtonText:(0,_str.getString)("save"),returnFocus:saveAsPresetButton});modalForm.addEventListener(modalForm.events.FORM_SUBMITTED,(event=>{event.detail.result?window.location.reload():_notification.default.addNotification({type:"error",message:event.detail.errors.join("
")})})),modalForm.show()}))}})); //# sourceMappingURL=saveaspreset.min.js.mapamd/build/templateseditor.min.js000064400000005717152265517740012761 0ustar00define("mod_data/templateseditor",["exports","core/str","core/prefetch","core/url","core/notification","core/templates"],(function(_exports,_str,_prefetch,_url,_notification,_templates){var obj; /** * Javascript module to control the template editor. * * @module mod_data/templateseditor * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj},(0,_prefetch.prefetchStrings)("admin",["confirmation"]),(0,_prefetch.prefetchStrings)("mod_data",["resettemplateconfirmtitle","enabletemplateeditorcheck","editorenable"]),(0,_prefetch.prefetchStrings)("core",["reset"]);const selectors_toggleTemplateEditor='input[name="useeditor"]',selectors_resetTemplateAction='[data-action="resettemplate"]',selectors_resetTemplate='input[name="defaultform"]',selectors_resetAllTemplates='input[name="resetall"]',selectors_resetAllCheck='input[name="resetallcheck"]',selectors_editForm="#edittemplateform",registerResetButton=mode=>{const editForm=document.querySelector(selectors_editForm),resetTemplate=document.querySelector(selectors_resetTemplate),resetAllTemplates=document.querySelector(selectors_resetAllTemplates),resetTemplateAction=document.querySelector(selectors_resetTemplateAction);resetTemplateAction&&resetTemplate&&editForm&&((0,_prefetch.prefetchStrings)("mod_data",[mode]),resetTemplateAction.addEventListener("click",(async event=>{event.preventDefault();const params={resetallname:"resetallcheck",templatename:await(0,_str.getString)(mode,"mod_data")};(0,_notification.deleteCancel)((0,_str.getString)("resettemplateconfirmtitle","mod_data"),_templates.default.render("mod_data/template_editor_resetmodal",params),(0,_str.getString)("reset","core"),(()=>{resetTemplate.value="true",editForm.submit()}),null,{triggerElement:event.target})})),resetAllTemplates&&document.addEventListener("change",(event=>{event.target.matches(selectors_resetAllCheck)&&(resetAllTemplates.value=event.target.checked?"true":"")})))},registerEditorToggler=(instanceId,mode)=>{const toggleTemplateEditor=document.querySelector(selectors_toggleTemplateEditor);toggleTemplateEditor&&toggleTemplateEditor.addEventListener("click",(async event=>{event.preventDefault();event.target.checked?(0,_notification.saveCancel)((0,_str.getString)("confirmation","admin"),(0,_str.getString)("enabletemplateeditorcheck","mod_data"),(0,_str.getString)("editorenable","mod_data"),(()=>{window.location=(0,_url.relativeUrl)("/mod/data/templates.php",{d:instanceId,mode:mode,useeditor:!0})}),null,{triggerElement:event.target}):window.location=(0,_url.relativeUrl)("/mod/data/templates.php",{d:instanceId,mode:mode,useeditor:!1})}))};_exports.init=(instanceId,mode)=>{((instanceId,mode)=>{registerResetButton(mode),registerEditorToggler(instanceId,mode)})(instanceId,mode)}})); //# sourceMappingURL=templateseditor.min.js.mapamd/build/importpresets.min.js.map000064400000006125152265517740013242 0ustar00{"version":3,"file":"importpresets.min.js","sources":["../src/importpresets.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for importing presets.\n *\n * @module mod_data/importpresets\n * @copyright 2022 Laurent David \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalForm from 'core_form/modalform';\nimport Notification from 'core/notification';\nimport {getString} from 'core/str';\n\nconst selectors = {\n importPresetButton: '[data-action=\"importpresets\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n document.addEventListener('click', (event) => {\n const importPresetButton = event.target.closest(selectors.importPresetButton);\n\n if (!importPresetButton) {\n return;\n }\n\n event.preventDefault();\n const modalForm = new ModalForm({\n modalConfig: {\n title: getString('importpreset', 'mod_data'),\n },\n formClass: 'mod_data\\\\form\\\\import_presets',\n args: {cmid: importPresetButton.dataset.dataid},\n saveButtonText: getString('importandapply', 'mod_data'),\n });\n\n modalForm.addEventListener(modalForm.events.FORM_SUBMITTED, event => {\n if (event.detail.result) {\n window.location.assign(event.detail.url);\n } else {\n Notification.addNotification({\n type: 'error',\n message: event.detail.errors.join('
')\n });\n }\n });\n modalForm.show();\n });\n};\n"],"names":["selectors","document","addEventListener","event","importPresetButton","target","closest","preventDefault","modalForm","ModalForm","modalConfig","title","formClass","args","cmid","dataset","dataid","saveButtonText","events","FORM_SUBMITTED","detail","result","window","location","assign","url","addNotification","type","message","errors","join","show"],"mappings":";;;;;;;0LA2BMA,6BACkB,8CAMJ,KAChBC,SAASC,iBAAiB,SAAUC,cAC1BC,mBAAqBD,MAAME,OAAOC,QAAQN,kCAE3CI,0BAILD,MAAMI,uBACAC,UAAY,IAAIC,mBAAU,CAC5BC,YAAa,CACTC,OAAO,kBAAU,eAAgB,aAErCC,UAAW,iCACXC,KAAM,CAACC,KAAMV,mBAAmBW,QAAQC,QACxCC,gBAAgB,kBAAU,iBAAkB,cAGhDT,UAAUN,iBAAiBM,UAAUU,OAAOC,gBAAgBhB,QACpDA,MAAMiB,OAAOC,OACbC,OAAOC,SAASC,OAAOrB,MAAMiB,OAAOK,2BAEvBC,gBAAgB,CACzBC,KAAM,QACNC,QAASzB,MAAMiB,OAAOS,OAAOC,KAAK,aAI9CtB,UAAUuB"}css.php000064400000004666152265517740006077 0ustar00. /** * This file is part of the Database module for Moodle * * @copyright 2005 Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package mod_data */ use mod_data\manager; use mod_data\preset; define('NO_MOODLE_COOKIES', true); // Session not used here. require_once('../../config.php'); $id = optional_param('id', 0, PARAM_INT); // Course module id. $d = optional_param('d', 0, PARAM_INT); // Database id. $presetfullname = optional_param('preset', '', PARAM_PATH); // The directory the preset is in. $lifetime = 600; // Seconds to cache this stylesheet. $url = new moodle_url('/mod/data/css.php'); $manager = null; if ($id) { list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); $manager = manager::create_from_coursemodule($cm); $instance = $manager->get_instance(); } else { // We must have the database activity id. $d = required_param('d', PARAM_INT); $instance = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); $manager = manager::create_from_instance($instance); } $url->param('d', $instance->id); // Get the content. if ($presetfullname) { $url->param('preset', $presetfullname); $preset = preset::create_from_fullname($manager, $presetfullname); $content = $preset->get_template_content('csstemplate'); $lifetime = 60; // Preset preview does not need a long cache. } else { $content = $instance->csstemplate; } $PAGE->set_url($url); header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); header('Expires: ' . gmdate("D, d M Y H:i:s", time() + $lifetime) . ' GMT'); header('Cache-control: max_age = '. $lifetime); header('Pragma: '); header('Content-type: text/css; charset=utf-8'); // Correct MIME type. echo $content; UPGRADING.md000064400000004350152265517740006426 0ustar00# mod_data Upgrade notes ## 5.0 ### Deprecated - The following unused capabilities have been deprecated: * `mod/data:comment` * `mod/data:managecomments` For more information see [MDL-84267](https://tracker.moodle.org/browse/MDL-84267) ### Removed - Final deprecation and removal of the following classes: - data_preset_importer - data_preset_existing_importer - data_preset_upload_importer - data_import_preset_zip_form For more information see [MDL-75189](https://tracker.moodle.org/browse/MDL-75189) - - Final deprecation of \mod_data_renderer::import_setting_mappings(). Please use \mod_data_renderer::importing_preset() instead. - Final deprecation of data_print_template() function. Please use mod_data\manager::get_template and mod_data\template::parse_entries instead. - Final deprecation of data_preset_name(). Please use preset::get_name_from_plugin() instead. - Final deprecation of data_get_available_presets(). Please use manager::get_available_presets() instead. - Final deprecation of data_get_available_site_presets(). Please use manager::get_available_saved_presets() instead. - Final deprecation of data_delete_site_preset(). Please use preset::delete() instead. - Final deprecation of is_directory_a_preset(). Please use preset::is_directory_a_preset() instead. - Final deprecation of data_presets_save(). Please use preset::save() instead. - Final deprecation of data_presets_generate_xml(). Please use preset::generate_preset_xml() instead. - Final deprecation of data_presets_export(). Please use preset::export() instead. - Final deprecation of data_user_can_delete_preset(). Please use preset::can_manage() instead. - Final deprecation of data_view(). Please use mod_data\manager::set_module_viewed() instead. For more information see [MDL-75189](https://tracker.moodle.org/browse/MDL-75189) ## 4.5 ### Added - The `\data_add_record()` method accepts a new `$approved` parameter to set the corresponding state of the new record. For more information see [MDL-81274](https://tracker.moodle.org/browse/MDL-81274) ### Deprecated - The `\mod_data_renderer::render_fields_footer()` method has been deprecated as it's no longer used. For more information see [MDL-81321](https://tracker.moodle.org/browse/MDL-81321) tests/fixtures/image_gallery_preset.zip000064400000007557152265517740014522 0ustar00PK\U addtemplate.htmlUT  cq c cux I,SHI,.UMLOMOI-MLII+)TR$uiEEPIĤRttIfINrfJl]mI*Gh*ء,.Z bbRR2 J2󠖺 DpY P PK,9PK\Ux asearchtemplate.htmlUT  cq c cux  0 >E粃n'! dH[GaSċ|۞n3XkaQ"aJ@nv*$UNGs('t2Auηx- C2SM;ӫղol'ջέ?o =F/4' S1r\'lM'PK7xPK\U csstemplate.cssUT  cq c cux u1k0wҥTŞBnuEeIUPs1I{|zyܐu ZshH'ل! Xr$=NrȅT5k3+MJx aj2+d]9U{9ǞaÊ5~P`]L1ܠ~2icxs͘-` ,'3a=1R/-^7_jjv.Do]jxPK֘.1PK\U jstemplate.jsUT  cq c cux TAk0+Ba4vГ]w;F &%e/3 =mdL(Z2|Nŭ.xyR!`<g<+TPD]_2< JJ 2}>b?>"MTՂHZBZ1#4R[R<1NU%aXE1tCP U0+|'N xKs;dFKg0Ƹ%vjcȐjOIّGs49Jk{,9J9tE7WAyΑdkK>w4ӳ1߯ż\二 k ,ۀ3ʓ̘H7PKA1jPK\UJ listtemplate.htmlUT  cq c cux eO90 #BK44=ںh<v=ҤKi ʇ/N} igTզgݺ$:0꾸cV.U8$Z?uA9!@|vD"W昕RB.4`xKyjqPKWʫJPK\U listtemplatefooter.htmlUT  cq c cux O,PK>( PK\UX listtemplateheader.htmlUT  cq c cux I,SLUMLOMOI-,.QRHI,.UJN,JMIMV(/ab]C37ED IUkPK=OKXPK\U preset.xmlUT  cq c cux RN0 |WIc $}Kzo')ٖvx9؞}pB4ɬVPRbhFN8B>&ꅇHB3xk@_k#12.&N?fAw°@+BM/+7<\kzo[[%VsN fֲУo;S#쒩|hGjzB zђ׃~ʕr|e+cAgٺd y{6u)XH%xy$!cts[kھ*SL3stmf)PK(PK\U rsstemplate.htmlUT  cq c cux  D~Nz#M+dDK e73# ޘ&EY"B|8c9Ivs_mYqfi3<;{QR( 嵧M6J{9Z]zd WϺPKg`PK\U  rsstitletemplate.htmlUT  cq c cux .,IPK PK\U  singletemplate.htmlUT  cq c cux Rj!+D2@bكΌcv7Lfj7UZխB3QVD3Z-遬Ct,,|4)5GPQ ر2ccR >911܅Vk-{b*XN>iH'0?O= 1ro*V=7"~i5?:$זǵJ.ACRTrիo_tZضOPKP'F PK\U,9 addtemplate.htmlUT  cq c cux PK\U7x asearchtemplate.htmlUT  cq c cux PK\U֘.1 csstemplate.cssUT  cq c cux PK\UA1j .jstemplate.jsUT  cq c cux PK\UWʫJ listtemplate.htmlUT  cq c cux PK\U>(  listtemplatefooter.htmlUT  cq c cux PK\U=OKX klisttemplateheader.htmlUT  cq c cux PK\U( preset.xmlUT  cq c cux PK\Ug` rsstemplate.htmlUT  cq c cux PK\U  rsstitletemplate.htmlUT  cq c cux PK\UP'F  singletemplate.htmlUT  cq c cux PK > tests/fixtures/test_data_import_with_files.zip000064400000006004152265517740016100 0ustar00PK ldV"test_data_import_with_files/files/UT <dDdux PKldV(]3test_data_import_with_files/files/samplepicture.pngUT <dDdux  sb``p  $<{ H1;1;test_data_import_with_files/test_data_import_with_files.csvUT dDdux tIIMLI)L.)-p us @r9zyP.TH PK ldVfiles/UT <dDdux PKldV(]files/samplepicture.pngUT <dDdux  sb``p  $<{ H1;1test_data_import_with_files.csvUT dDdux tIIMLI)L.)-p us @r9zyP.TH PK ldV"Atest_data_import_with_files/files/UT<dux PKldV(]3\test_data_import_with_files/files/samplepicture.pngUT<dux PKjdV(]0&test_data_import_with_files/files/samplefile.pngUT7dux PKMdV1W ->;test_data_import_with_files/test_data_import_with_files.csvUTdux PK ldVAfiles/UT<dux PKldV(]files/samplepicture.pngUT<dux PKjdV(]}files/samplefile.pngUT7dux PKMdV1W ->(test_data_import_with_files.csvUTdux PK@tests/fixtures/test_data_import_with_approved.csv000064400000000076152265517740016612 0ustar00ID,Param2,Approved 1,"My first entry",1 2,"My second entry",0 tests/fixtures/test_data_content.csv000064400000302122152265517740014014 0ustar00id,fieldid,recordid,content,content1,content2,content3,content4 1,1,1,52.207,20.9763,NULL,NULL,NULL 2,2,1,Yebisu,NULL,NULL,NULL,NULL 3,3,1,One month,NULL,NULL,NULL,NULL 4,4,1,97394157,NULL,NULL,NULL,NULL 5,5,1,Male,NULL,NULL,NULL,NULL 6,6,1,www.penny-arcade.com,NULL,NULL,NULL,NULL 7,7,1,Flavia,NULL,NULL,NULL,NULL 8,8,1,arcu imperdiet ullamcorper. Duis at lacus. Quisque purus sapien gravida non sollicitudin a malesuada id erat. Etiam vestibulum massa rutrum magna. Cras convallis convallis dolor. Quisque tincidunt pede ac urna. Ut tincidunt vehicula risus. Nulla eget metus eu erat semper rutrum. Fusce dolor quam elementum at egestas a scelerisque sed,NULL,NULL,NULL,NULL 9,9,1,ACT,NULL,NULL,NULL,NULL 10,10,1,P.O. Box 708 4120 Ipsum. Rd.,NULL,NULL,NULL,NULL 11,11,1,Soccer,NULL,NULL,NULL,NULL 12,12,1,YT,NULL,NULL,NULL,NULL 13,13,1,(06) 9404 7347,NULL,NULL,NULL,NULL 14,14,1,(06) 1005 1944,NULL,NULL,NULL,NULL 15,15,1,Maecenas@example.com,NULL,NULL,NULL,NULL 16,16,1,44771,NULL,NULL,NULL,NULL 17,17,1,H2252199,NULL,NULL,NULL,NULL 18,18,1,Safari,NULL,NULL,NULL,NULL 19,19,1,Windows,NULL,NULL,NULL,NULL 20,20,1,Veda,NULL,NULL,NULL,NULL 21,1,2,47.959,2.3965,NULL,NULL,NULL 22,2,2,Hahn Premium,NULL,NULL,NULL,NULL 23,3,2,two months,NULL,NULL,NULL,NULL 24,4,2,22120697,NULL,NULL,NULL,NULL 25,5,2,Female,NULL,NULL,NULL,NULL 26,6,2,www.penny-arcade.com,NULL,NULL,NULL,NULL 27,7,2,Orlando,NULL,NULL,NULL,NULL 28,8,2,mattis. Cras eget nisi dictum augue malesuada malesuada. Integer id magna et ipsum cursus vestibulum. Mauris magna. Duis dignissim tempor arcu. Vestibulum ut eros non enim commodo hendrerit. Donec porttitor tellus non magna. Nam ligula elit pretium et,NULL,NULL,NULL,NULL 29,9,2,WA,NULL,NULL,NULL,NULL 30,10,2,3155 Odio Street,NULL,NULL,NULL,NULL 31,11,2,Water polo,NULL,NULL,NULL,NULL 32,12,2,AB,NULL,NULL,NULL,NULL 33,13,2,(04) 1472 1097,NULL,NULL,NULL,NULL 34,14,2,(06) 7312 6517,NULL,NULL,NULL,NULL 35,15,2,adipiscing.lacus.Ut@example.com,NULL,NULL,NULL,NULL 36,16,2,607398,NULL,NULL,NULL,NULL 37,17,2,T727721,NULL,NULL,NULL,NULL 38,18,2,Firefox,NULL,NULL,NULL,NULL 39,19,2,Red Hat,NULL,NULL,NULL,NULL 40,20,2,Quintessa,NULL,NULL,NULL,NULL 41,1,3,7.202,2.2142,NULL,NULL,NULL 42,2,3,Hahn Premium,NULL,NULL,NULL,NULL 43,3,3,two months,NULL,NULL,NULL,NULL 44,4,3,2500297,NULL,NULL,NULL,NULL 45,5,3,Male,NULL,NULL,NULL,NULL 46,6,3,www.imdb.com,NULL,NULL,NULL,NULL 47,7,3,Andrew,NULL,NULL,NULL,NULL 48,8,3,nec eleifend non dapibus rutrum justo. Praesent luctus. Curabitur egestas nunc sed libero. Proin sed turpis nec mauris blandit mattis. Cras eget nisi dictum augue malesuada malesuada. Integer id magna et ipsum cursus vestibulum. Mauris magna. Duis,NULL,NULL,NULL,NULL 49,9,3,NT,NULL,NULL,NULL,NULL 50,10,3,Ap #703-3636 Eu Street,NULL,NULL,NULL,NULL 51,11,3,Water polo,NULL,NULL,NULL,NULL 52,12,3,NT,NULL,NULL,NULL,NULL 53,13,3,(05) 6465 9615,NULL,NULL,NULL,NULL 54,14,3,(06) 4743 22,NULL,NULL,NULL,NULL 55,15,3,luctus.sit@example.com,NULL,NULL,NULL,NULL 56,16,3,15296,NULL,NULL,NULL,NULL 57,17,3,Z143440,NULL,NULL,NULL,NULL 58,18,3,Internet Explorer,NULL,NULL,NULL,NULL 59,19,3,MacOS,NULL,NULL,NULL,NULL 60,20,3,Henry,NULL,NULL,NULL,NULL 61,1,4,8.365,8.3472,NULL,NULL,NULL 62,2,4,Carlsberg,NULL,NULL,NULL,NULL 63,3,4,two months,NULL,NULL,NULL,NULL 64,4,4,436095,NULL,NULL,NULL,NULL 65,5,4,Female,NULL,NULL,NULL,NULL 66,6,4,www.google.com,NULL,NULL,NULL,NULL 67,7,4,Lev,NULL,NULL,NULL,NULL 68,8,4,Fusce aliquet magna a neque. Nullam ut nisi a odio semper cursus. Integer mollis. Integer tincidunt aliquam arcu. Aliquam ultrices iaculis odio. Nam interdum enim non nisi. Aenean eget metus. In nec orci. Donec nibh. Quisque nonummy ipsum non arcu. Vivamus sit amet risus. Donec egestas. Aliquam nec,NULL,NULL,NULL,NULL 69,9,4,NSW,NULL,NULL,NULL,NULL 70,10,4,152 Dignissim Av.,NULL,NULL,NULL,NULL 71,11,4,Football,NULL,NULL,NULL,NULL 72,12,4,PE,NULL,NULL,NULL,NULL 73,13,4,(01) 84 3002,NULL,NULL,NULL,NULL 74,14,4,(00) 511 1633,NULL,NULL,NULL,NULL 75,15,4,magna.Sed@example.com,NULL,NULL,NULL,NULL 76,16,4,964251,NULL,NULL,NULL,NULL 77,17,4,N3416104,NULL,NULL,NULL,NULL 78,18,4,Opera,NULL,NULL,NULL,NULL 79,19,4,MacOS,NULL,NULL,NULL,NULL 80,20,4,Yuli,NULL,NULL,NULL,NULL 81,1,5,37.661,73.4739,NULL,NULL,NULL 82,2,5,Yebisu,NULL,NULL,NULL,NULL 83,3,5,One month,NULL,NULL,NULL,NULL 84,4,5,9143174,NULL,NULL,NULL,NULL 85,5,5,Female,NULL,NULL,NULL,NULL 86,6,5,www.google.com,NULL,NULL,NULL,NULL 87,7,5,Emma,NULL,NULL,NULL,NULL 88,8,5,sed libero. Proin sed turpis nec mauris blandit mattis. Cras eget nisi dictum augue malesuada malesuada. Integer id magna,NULL,NULL,NULL,NULL 89,9,5,NT,NULL,NULL,NULL,NULL 90,10,5,126 Turpis Avenue,NULL,NULL,NULL,NULL 91,11,5,Water polo,NULL,NULL,NULL,NULL 92,12,5,NU,NULL,NULL,NULL,NULL 93,13,5,(02) 5233 3164,NULL,NULL,NULL,NULL 94,14,5,(07) 4113 5465,NULL,NULL,NULL,NULL 95,15,5,Suspendisse.tristique@example.com,NULL,NULL,NULL,NULL 96,16,5,341238,NULL,NULL,NULL,NULL 97,17,5,Y1436768,NULL,NULL,NULL,NULL 98,18,5,Firefox,NULL,NULL,NULL,NULL 99,19,5,MacOS,NULL,NULL,NULL,NULL 100,20,5,Demetria,NULL,NULL,NULL,NULL 101,1,6,3.721,46.6126,NULL,NULL,NULL 102,2,6,Hahn Premium,NULL,NULL,NULL,NULL 103,3,6,One month,NULL,NULL,NULL,NULL 104,4,6,57655446,NULL,NULL,NULL,NULL 105,5,6,Female,NULL,NULL,NULL,NULL 106,6,6,www.penny-arcade.com,NULL,NULL,NULL,NULL 107,7,6,Kellie,NULL,NULL,NULL,NULL 108,8,6,sit amet metus. Aliquam erat volutpat. Nulla facilisis. Suspendisse commodo tincidunt nibh. Phasellus nulla. Integer vulputate risus a ultricies adipiscing enim mi tempor lorem eget mollis lectus pede et risus. Quisque libero lacus varius,NULL,NULL,NULL,NULL 109,9,6,VIC,NULL,NULL,NULL,NULL 110,10,6,345-2147 Eleifend St.,NULL,NULL,NULL,NULL 111,11,6,Football,NULL,NULL,NULL,NULL 112,12,6,NT,NULL,NULL,NULL,NULL 113,13,6,(06) 916 3114,NULL,NULL,NULL,NULL 114,14,6,(09) 2142 5065,NULL,NULL,NULL,NULL 115,15,6,commodo.tincidunt@example.com,NULL,NULL,NULL,NULL 116,16,6,63999,NULL,NULL,NULL,NULL 117,17,6,S6095452,NULL,NULL,NULL,NULL 118,18,6,Internet Explorer,NULL,NULL,NULL,NULL 119,19,6,Linux,NULL,NULL,NULL,NULL 120,20,6,Hammett,NULL,NULL,NULL,NULL 121,1,7,15.4707,67.9436,NULL,NULL,NULL 122,2,7,Yebisu,NULL,NULL,NULL,NULL 123,3,7,One month,NULL,NULL,NULL,NULL 124,4,7,3120232,NULL,NULL,NULL,NULL 125,5,7,Male,NULL,NULL,NULL,NULL 126,6,7,www.google.com,NULL,NULL,NULL,NULL 127,7,7,Bryar,NULL,NULL,NULL,NULL 128,8,7,Nulla semper tellus id nunc interdum feugiat. Sed nec metus facilisis lorem tristique aliquet. Phasellus fermentum convallis ligula. Donec luctus aliquet odio.,NULL,NULL,NULL,NULL 129,9,7,NSW,NULL,NULL,NULL,NULL 130,10,7,Ap #773-2155 Orci Rd.,NULL,NULL,NULL,NULL 131,11,7,Table tennis,NULL,NULL,NULL,NULL 132,12,7,NS,NULL,NULL,NULL,NULL 133,13,7,(02) 7564 4771,NULL,NULL,NULL,NULL 134,14,7,(06) 100 1673,NULL,NULL,NULL,NULL 135,15,7,ipsum@example.com,NULL,NULL,NULL,NULL 136,16,7,151656,NULL,NULL,NULL,NULL 137,17,7,G1012374,NULL,NULL,NULL,NULL 138,18,7,Opera,NULL,NULL,NULL,NULL 139,19,7,Net-BSD,NULL,NULL,NULL,NULL 140,20,7,Veronica,NULL,NULL,NULL,NULL 141,1,8,6.333,26.56,NULL,NULL,NULL 142,2,8,Asahi,NULL,NULL,NULL,NULL 143,3,8,One month,NULL,NULL,NULL,NULL 144,4,8,91404025,NULL,NULL,NULL,NULL 145,5,8,Male,NULL,NULL,NULL,NULL 146,6,8,www.google.com,NULL,NULL,NULL,NULL 147,7,8,Geoffrey,NULL,NULL,NULL,NULL 148,8,8,ac turpis egestas. Aliquam fringilla cursus purus. Nullam scelerisque neque sed sem egestas,NULL,NULL,NULL,NULL 149,9,8,NT,NULL,NULL,NULL,NULL 150,10,8,369-9968 Orci Rd.,NULL,NULL,NULL,NULL 151,11,8,Basketball,NULL,NULL,NULL,NULL 152,12,8,BC,NULL,NULL,NULL,NULL 153,13,8,(07) 1364 4952,NULL,NULL,NULL,NULL 154,14,8,(05) 5511 975,NULL,NULL,NULL,NULL 155,15,8,nulla@example.com,NULL,NULL,NULL,NULL 156,16,8,31696,NULL,NULL,NULL,NULL 157,17,8,B3123271,NULL,NULL,NULL,NULL 158,18,8,Safari,NULL,NULL,NULL,NULL 159,19,8,Red Hat,NULL,NULL,NULL,NULL 160,20,8,Germane,NULL,NULL,NULL,NULL 161,1,9,15.1474,12.5955,NULL,NULL,NULL 162,2,9,Victoria Bitter,NULL,NULL,NULL,NULL 163,3,9,One month,NULL,NULL,NULL,NULL 164,4,9,636265,NULL,NULL,NULL,NULL 165,5,9,Male,NULL,NULL,NULL,NULL 166,6,9,www.google.com,NULL,NULL,NULL,NULL 167,7,9,Brent,NULL,NULL,NULL,NULL 168,8,9,pede ultrices a auctor non feugiat nec diam. Duis mi enim condimentum eget volutpat ornare facilisis eget ipsum. Donec sollicitudin,NULL,NULL,NULL,NULL 169,9,9,TAS,NULL,NULL,NULL,NULL 170,10,9,Ap #275-5070 Sagittis St.,NULL,NULL,NULL,NULL 171,11,9,Tennis,NULL,NULL,NULL,NULL 172,12,9,NT,NULL,NULL,NULL,NULL 173,13,9,(06) 1697 3005,NULL,NULL,NULL,NULL 174,14,9,(05) 145 1370,NULL,NULL,NULL,NULL 175,15,9,sed.hendrerit.a@example.com,NULL,NULL,NULL,NULL 176,16,9,59498,NULL,NULL,NULL,NULL 177,17,9,M2169545,NULL,NULL,NULL,NULL 178,18,9,Chrome,NULL,NULL,NULL,NULL 179,19,9,Ubuntu,NULL,NULL,NULL,NULL 180,20,9,Willa,NULL,NULL,NULL,NULL 181,1,10,50.0307,36.4619,NULL,NULL,NULL 182,2,10,Victoria Bitter,NULL,NULL,NULL,NULL 183,3,10,One month,NULL,NULL,NULL,NULL 184,4,10,99951721,NULL,NULL,NULL,NULL 185,5,10,Male,NULL,NULL,NULL,NULL 186,6,10,www.imdb.com,NULL,NULL,NULL,NULL 187,7,10,Nayda,NULL,NULL,NULL,NULL 188,8,10,Aenean eget magna. Suspendisse tristique neque venenatis lacus. Etiam bibendum fermentum metus. Aenean sed pede nec ante blandit viverra. Donec tempus lorem fringilla ornare placerat orci lacus vestibulum lorem,NULL,NULL,NULL,NULL 189,9,10,NT,NULL,NULL,NULL,NULL 190,10,10,P.O. Box 569 5699 Cras Av.,NULL,NULL,NULL,NULL 191,11,10,Football,NULL,NULL,NULL,NULL 192,12,10,NS,NULL,NULL,NULL,NULL 193,13,10,(04) 568 7503,NULL,NULL,NULL,NULL 194,14,10,(05) 4111 9737,NULL,NULL,NULL,NULL 195,15,10,sagittis@example.com,NULL,NULL,NULL,NULL 196,16,10,355277,NULL,NULL,NULL,NULL 197,17,10,U2209614,NULL,NULL,NULL,NULL 198,18,10,Safari,NULL,NULL,NULL,NULL 199,19,10,Free-BSD,NULL,NULL,NULL,NULL 200,20,10,Fredericka,NULL,NULL,NULL,NULL 201,1,11,9.1461,29.6214,NULL,NULL,NULL 202,2,11,Yebisu,NULL,NULL,NULL,NULL 203,3,11,two months,NULL,NULL,NULL,NULL 204,4,11,66601112,NULL,NULL,NULL,NULL 205,5,11,Male,NULL,NULL,NULL,NULL 206,6,11,www.penny-arcade.com,NULL,NULL,NULL,NULL 207,7,11,Wendy,NULL,NULL,NULL,NULL 208,8,11,sapien gravida non sollicitudin a malesuada id erat. Etiam vestibulum massa rutrum magna. Cras convallis convallis dolor. Quisque tincidunt pede ac urna. Ut tincidunt vehicula risus. Nulla eget metus eu erat semper rutrum. Fusce dolor quam elementum at egestas a scelerisque sed sapien. Nunc pulvinar arcu,NULL,NULL,NULL,NULL 209,9,11,SA,NULL,NULL,NULL,NULL 210,10,11,314-175 Nulla. Avenue,NULL,NULL,NULL,NULL 211,11,11,Running,NULL,NULL,NULL,NULL 212,12,11,ON,NULL,NULL,NULL,NULL 213,13,11,(08) 3706 1359,NULL,NULL,NULL,NULL 214,14,11,(07) 3369 2750,NULL,NULL,NULL,NULL 215,15,11,eu@example.com,NULL,NULL,NULL,NULL 216,16,11,774163,NULL,NULL,NULL,NULL 217,17,11,S3140509,NULL,NULL,NULL,NULL 218,18,11,Internet Explorer,NULL,NULL,NULL,NULL 219,19,11,Ubuntu,NULL,NULL,NULL,NULL 220,20,11,Inga,NULL,NULL,NULL,NULL 221,1,12,30.1142,72.028,NULL,NULL,NULL 222,2,12,Hahn Premium,NULL,NULL,NULL,NULL 223,3,12,two months,NULL,NULL,NULL,NULL 224,4,12,95553636,NULL,NULL,NULL,NULL 225,5,12,Female,NULL,NULL,NULL,NULL 226,6,12,www.moodle.org,NULL,NULL,NULL,NULL 227,7,12,Maxwell,NULL,NULL,NULL,NULL 228,8,12,eros. Nam consequat dolor vitae dolor. Donec fringilla. Donec feugiat metus sit amet ante. Vivamus non lorem vitae odio sagittis semper. Nam tempor diam,NULL,NULL,NULL,NULL 229,9,12,SA,NULL,NULL,NULL,NULL 230,10,12,165-7333 Tincidunt Rd.,NULL,NULL,NULL,NULL 231,11,12,Table tennis,NULL,NULL,NULL,NULL 232,12,12,NU,NULL,NULL,NULL,NULL 233,13,12,(05) 5172 3353,NULL,NULL,NULL,NULL 234,14,12,(00) 5213 5429,NULL,NULL,NULL,NULL 235,15,12,amet.ornare.lectus@example.com,NULL,NULL,NULL,NULL 236,16,12,5852,NULL,NULL,NULL,NULL 237,17,12,O439744,NULL,NULL,NULL,NULL 238,18,12,Opera,NULL,NULL,NULL,NULL 239,19,12,Red Hat,NULL,NULL,NULL,NULL 240,20,12,Oren,NULL,NULL,NULL,NULL 241,1,13,2.1012,36.112,NULL,NULL,NULL 242,2,13,Victoria Bitter,NULL,NULL,NULL,NULL 243,3,13,two months,NULL,NULL,NULL,NULL 244,4,13,1545732,NULL,NULL,NULL,NULL 245,5,13,Female,NULL,NULL,NULL,NULL 246,6,13,www.penny-arcade.com,NULL,NULL,NULL,NULL 247,7,13,Noel,NULL,NULL,NULL,NULL 248,8,13,euismod ac fermentum vel mauris. Integer sem elit pharetra ut pharetra sed hendrerit a arcu. Sed et libero. Proin mi. Aliquam gravida mauris ut mi. Duis risus,NULL,NULL,NULL,NULL 249,9,13,NSW,NULL,NULL,NULL,NULL 250,10,13,7345 Pede Rd.,NULL,NULL,NULL,NULL 251,11,13,Water polo,NULL,NULL,NULL,NULL 252,12,13,QC,NULL,NULL,NULL,NULL 253,13,13,(06) 54 55,NULL,NULL,NULL,NULL 254,14,13,(02) 5810 476,NULL,NULL,NULL,NULL 255,15,13,quis.arcu.vel@example.com,NULL,NULL,NULL,NULL 256,16,13,732454,NULL,NULL,NULL,NULL 257,17,13,I1171097,NULL,NULL,NULL,NULL 258,18,13,Safari,NULL,NULL,NULL,NULL 259,19,13,Red Hat,NULL,NULL,NULL,NULL 260,20,13,Sandra,NULL,NULL,NULL,NULL 261,1,14,9.9756,49.746,NULL,NULL,NULL 262,2,14,Victoria Bitter,NULL,NULL,NULL,NULL 263,3,14,One month,NULL,NULL,NULL,NULL 264,4,14,2494252,NULL,NULL,NULL,NULL 265,5,14,Male,NULL,NULL,NULL,NULL 266,6,14,www.moodle.org,NULL,NULL,NULL,NULL 267,7,14,Leandra,NULL,NULL,NULL,NULL 268,8,14,a ultricies adipiscing enim mi tempor lorem eget mollis lectus pede et risus. Quisque libero lacus varius et euismod et commodo at libero. Morbi accumsan laoreet ipsum. Curabitur consequat lectus sit amet luctus vulputate nisi sem semper erat in consectetuer ipsum nunc id enim. Curabitur,NULL,NULL,NULL,NULL 269,9,14,WA,NULL,NULL,NULL,NULL 270,10,14,754-3754 Nulla Ave,NULL,NULL,NULL,NULL 271,11,14,Soccer,NULL,NULL,NULL,NULL 272,12,14,BC,NULL,NULL,NULL,NULL 273,13,14,(08) 647 5063,NULL,NULL,NULL,NULL 274,14,14,(03) 600 1727,NULL,NULL,NULL,NULL 275,15,14,non@example.com,NULL,NULL,NULL,NULL 276,16,14,371168,NULL,NULL,NULL,NULL 277,17,14,L85167,NULL,NULL,NULL,NULL 278,18,14,Chrome,NULL,NULL,NULL,NULL 279,19,14,Windows,NULL,NULL,NULL,NULL 280,20,14,Miranda,NULL,NULL,NULL,NULL 281,1,15,2.3075,8.561,NULL,NULL,NULL 282,2,15,Asahi,NULL,NULL,NULL,NULL 283,3,15,two months,NULL,NULL,NULL,NULL 284,4,15,8102371,NULL,NULL,NULL,NULL 285,5,15,Female,NULL,NULL,NULL,NULL 286,6,15,www.penny-arcade.com,NULL,NULL,NULL,NULL 287,7,15,Carol,NULL,NULL,NULL,NULL 288,8,15,dolor. Fusce mi lorem vehicula et rutrum eu ultrices sit amet risus. Donec nibh enim gravida sit amet dapibus id blandit at nisi. Cum sociis natoque penatibus et magnis dis parturient montes nascetur ridiculus mus. Proin vel nisl. Quisque fringilla euismod enim.,NULL,NULL,NULL,NULL 289,9,15,QLD,NULL,NULL,NULL,NULL 290,10,15,Ap #148-7424 Nec St.,NULL,NULL,NULL,NULL 291,11,15,Badminton,NULL,NULL,NULL,NULL 292,12,15,QC,NULL,NULL,NULL,NULL 293,13,15,(00) 7216 5110,NULL,NULL,NULL,NULL 294,14,15,(02) 5207 1615,NULL,NULL,NULL,NULL 295,15,15,In@example.com,NULL,NULL,NULL,NULL 296,16,15,37745,NULL,NULL,NULL,NULL 297,17,15,J6613433,NULL,NULL,NULL,NULL 298,18,15,Firefox,NULL,NULL,NULL,NULL 299,19,15,MacOS,NULL,NULL,NULL,NULL 300,20,15,Meghan,NULL,NULL,NULL,NULL 301,1,16,7.3776,67.62,NULL,NULL,NULL 302,2,16,Hahn Premium,NULL,NULL,NULL,NULL 303,3,16,two months,NULL,NULL,NULL,NULL 304,4,16,59495628,NULL,NULL,NULL,NULL 305,5,16,Male,NULL,NULL,NULL,NULL 306,6,16,www.penny-arcade.com,NULL,NULL,NULL,NULL 307,7,16,Scarlett,NULL,NULL,NULL,NULL 308,8,16,iaculis enim sit amet ornare lectus justo eu arcu. Morbi sit amet massa. Quisque porttitor eros nec tellus. Nunc lectus pede ultrices a,NULL,NULL,NULL,NULL 309,9,16,NSW,NULL,NULL,NULL,NULL 310,10,16,111-5312 Eu Avenue,NULL,NULL,NULL,NULL 311,11,16,Squash,NULL,NULL,NULL,NULL 312,12,16,NT,NULL,NULL,NULL,NULL 313,13,16,(05) 141 6749,NULL,NULL,NULL,NULL 314,14,16,(02) 1419 5233,NULL,NULL,NULL,NULL 315,15,16,libero.Integer@example.com,NULL,NULL,NULL,NULL 316,16,16,177160,NULL,NULL,NULL,NULL 317,17,16,R0274613,NULL,NULL,NULL,NULL 318,18,16,Chrome,NULL,NULL,NULL,NULL 319,19,16,Red Hat,NULL,NULL,NULL,NULL 320,20,16,Montana,NULL,NULL,NULL,NULL 321,1,17,6.1605,2.0097,NULL,NULL,NULL 322,2,17,Yebisu,NULL,NULL,NULL,NULL 323,3,17,One month,NULL,NULL,NULL,NULL 324,4,17,42513570,NULL,NULL,NULL,NULL 325,5,17,Male,NULL,NULL,NULL,NULL 326,6,17,www.imdb.com,NULL,NULL,NULL,NULL 327,7,17,Denise,NULL,NULL,NULL,NULL 328,8,17,est congue a aliquet vel vulputate eu odio. Phasellus at augue id ante dictum cursus. Nunc mauris elit dictum eu eleifend nec malesuada ut sem. Nulla interdum.,NULL,NULL,NULL,NULL 329,9,17,ACT,NULL,NULL,NULL,NULL 330,10,17,P.O. Box 595 267 Luctus. Ave,NULL,NULL,NULL,NULL 331,11,17,Water polo,NULL,NULL,NULL,NULL 332,12,17,NS,NULL,NULL,NULL,NULL 333,13,17,(06) 1218 5436,NULL,NULL,NULL,NULL 334,14,17,(08) 1649 4050,NULL,NULL,NULL,NULL 335,15,17,augue@example.com,NULL,NULL,NULL,NULL 336,16,17,696167,NULL,NULL,NULL,NULL 337,17,17,R965616,NULL,NULL,NULL,NULL 338,18,17,Chrome,NULL,NULL,NULL,NULL 339,19,17,Free-BSD,NULL,NULL,NULL,NULL 340,20,17,Dieter,NULL,NULL,NULL,NULL 341,1,18,29.3703,73.2208,NULL,NULL,NULL 342,2,18,Victoria Bitter,NULL,NULL,NULL,NULL 343,3,18,One month,NULL,NULL,NULL,NULL 344,4,18,35503206,NULL,NULL,NULL,NULL 345,5,18,Female,NULL,NULL,NULL,NULL 346,6,18,www.penny-arcade.com,NULL,NULL,NULL,NULL 347,7,18,Raja,NULL,NULL,NULL,NULL 348,8,18,dis parturient montes nascetur ridiculus mus. Aenean eget magna. Suspendisse tristique neque venenatis lacus. Etiam bibendum fermentum metus. Aenean sed pede nec ante blandit viverra. Donec,NULL,NULL,NULL,NULL 349,9,18,VIC,NULL,NULL,NULL,NULL 350,10,18,P.O. Box 130 4394 Neque St.,NULL,NULL,NULL,NULL 351,11,18,Running,NULL,NULL,NULL,NULL 352,12,18,SK,NULL,NULL,NULL,NULL 353,13,18,(00) 2315 1677,NULL,NULL,NULL,NULL 354,14,18,(06) 1212 1370,NULL,NULL,NULL,NULL 355,15,18,quis.pede@example.com,NULL,NULL,NULL,NULL 356,16,18,23220,NULL,NULL,NULL,NULL 357,17,18,M266503,NULL,NULL,NULL,NULL 358,18,18,Other,NULL,NULL,NULL,NULL 359,19,18,Windows,NULL,NULL,NULL,NULL 360,20,18,Kylynn,NULL,NULL,NULL,NULL 361,1,19,65.763,1.6151,NULL,NULL,NULL 362,2,19,Victoria Bitter,NULL,NULL,NULL,NULL 363,3,19,two months,NULL,NULL,NULL,NULL 364,4,19,14021029,NULL,NULL,NULL,NULL 365,5,19,Female,NULL,NULL,NULL,NULL 366,6,19,www.penny-arcade.com,NULL,NULL,NULL,NULL 367,7,19,Declan,NULL,NULL,NULL,NULL 368,8,19,nascetur ridiculus mus. Proin vel arcu eu odio tristique pharetra. Quisque,NULL,NULL,NULL,NULL 369,9,19,ACT,NULL,NULL,NULL,NULL 370,10,19,9712 Iaculis Rd.,NULL,NULL,NULL,NULL 371,11,19,Squash,NULL,NULL,NULL,NULL 372,12,19,NL,NULL,NULL,NULL,NULL 373,13,19,(07) 16 2121,NULL,NULL,NULL,NULL 374,14,19,(04) 3440 3046,NULL,NULL,NULL,NULL 375,15,19,Morbi.vehicula@example.com,NULL,NULL,NULL,NULL 376,16,19,71509,NULL,NULL,NULL,NULL 377,17,19,T0131499,NULL,NULL,NULL,NULL 378,18,19,Firefox,NULL,NULL,NULL,NULL 379,19,19,Ubuntu,NULL,NULL,NULL,NULL 380,20,19,Shafira,NULL,NULL,NULL,NULL 381,1,20,10.543,51.666,NULL,NULL,NULL 382,2,20,Yebisu,NULL,NULL,NULL,NULL 383,3,20,two months,NULL,NULL,NULL,NULL 384,4,20,72201406,NULL,NULL,NULL,NULL 385,5,20,Male,NULL,NULL,NULL,NULL 386,6,20,www.imdb.com,NULL,NULL,NULL,NULL 387,7,20,Ulysses,NULL,NULL,NULL,NULL 388,8,20,non justo. Proin non massa non ante bibendum ullamcorper. Duis cursus diam at,NULL,NULL,NULL,NULL 389,9,20,NT,NULL,NULL,NULL,NULL 390,10,20,763-125 Morbi Ave,NULL,NULL,NULL,NULL 391,11,20,Football,NULL,NULL,NULL,NULL 392,12,20,QC,NULL,NULL,NULL,NULL 393,13,20,(01) 5111 54,NULL,NULL,NULL,NULL 394,14,20,(02) 9444 3570,NULL,NULL,NULL,NULL 395,15,20,vitae.sodales.nisi@example.com,NULL,NULL,NULL,NULL 396,16,20,62310,NULL,NULL,NULL,NULL 397,17,20,E071069,NULL,NULL,NULL,NULL 398,18,20,Firefox,NULL,NULL,NULL,NULL 399,19,20,Red Hat,NULL,NULL,NULL,NULL 400,20,20,Demetrius,NULL,NULL,NULL,NULL 401,1,21,39.7095,50.21,NULL,NULL,NULL 402,2,21,Victoria Bitter,NULL,NULL,NULL,NULL 403,3,21,two months,NULL,NULL,NULL,NULL 404,4,21,51236016,NULL,NULL,NULL,NULL 405,5,21,Male,NULL,NULL,NULL,NULL 406,6,21,www.google.com,NULL,NULL,NULL,NULL 407,7,21,Odessa,NULL,NULL,NULL,NULL 408,8,21,aliquet sem ut cursus luctus ipsum leo elementum sem vitae aliquam eros turpis,NULL,NULL,NULL,NULL 409,9,21,VIC,NULL,NULL,NULL,NULL 410,10,21,576 Convallis Avenue,NULL,NULL,NULL,NULL 411,11,21,Water polo,NULL,NULL,NULL,NULL 412,12,21,NS,NULL,NULL,NULL,NULL 413,13,21,(00) 1577 1640,NULL,NULL,NULL,NULL 414,14,21,(04) 1346 3651,NULL,NULL,NULL,NULL 415,15,21,risus.Donec@example.com,NULL,NULL,NULL,NULL 416,16,21,1497,NULL,NULL,NULL,NULL 417,17,21,Y4517117,NULL,NULL,NULL,NULL 418,18,21,Other,NULL,NULL,NULL,NULL 419,19,21,Free-BSD,NULL,NULL,NULL,NULL 420,20,21,Roth,NULL,NULL,NULL,NULL 421,1,22,4.455,5.2032,NULL,NULL,NULL 422,2,22,Yebisu,NULL,NULL,NULL,NULL 423,3,22,two months,NULL,NULL,NULL,NULL 424,4,22,51534101,NULL,NULL,NULL,NULL 425,5,22,Male,NULL,NULL,NULL,NULL 426,6,22,www.penny-arcade.com,NULL,NULL,NULL,NULL 427,7,22,Belle,NULL,NULL,NULL,NULL 428,8,22,vel est tempor bibendum. Donec felis orci adipiscing non luctus sit amet faucibus,NULL,NULL,NULL,NULL 429,9,22,TAS,NULL,NULL,NULL,NULL 430,10,22,194-196 Placerat. St.,NULL,NULL,NULL,NULL 431,11,22,Badminton,NULL,NULL,NULL,NULL 432,12,22,YT,NULL,NULL,NULL,NULL 433,13,22,(00) 5128 6314,NULL,NULL,NULL,NULL 434,14,22,(07) 1259 6246,NULL,NULL,NULL,NULL 435,15,22,suscipit.est@example.com,NULL,NULL,NULL,NULL 436,16,22,36066,NULL,NULL,NULL,NULL 437,17,22,L2161952,NULL,NULL,NULL,NULL 438,18,22,Chrome,NULL,NULL,NULL,NULL 439,19,22,MacOS,NULL,NULL,NULL,NULL 440,20,22,Daquan,NULL,NULL,NULL,NULL 441,1,23,9.5551,14.1475,NULL,NULL,NULL 442,2,23,Victoria Bitter,NULL,NULL,NULL,NULL 443,3,23,One month,NULL,NULL,NULL,NULL 444,4,23,4571469,NULL,NULL,NULL,NULL 445,5,23,Female,NULL,NULL,NULL,NULL 446,6,23,www.penny-arcade.com,NULL,NULL,NULL,NULL 447,7,23,Giacomo,NULL,NULL,NULL,NULL 448,8,23,mus. Proin vel nisl. Quisque fringilla euismod enim. Etiam gravida molestie arcu. Sed eu nibh vulputate mauris sagittis placerat. Cras dictum ultricies ligula.,NULL,NULL,NULL,NULL 449,9,23,NT,NULL,NULL,NULL,NULL 450,10,23,607-124 Mi. St.,NULL,NULL,NULL,NULL 451,11,23,Table tennis,NULL,NULL,NULL,NULL 452,12,23,QC,NULL,NULL,NULL,NULL 453,13,23,(00) 2502 2456,NULL,NULL,NULL,NULL 454,14,23,(00) 3103 4633,NULL,NULL,NULL,NULL 455,15,23,semper@example.com,NULL,NULL,NULL,NULL 456,16,23,1195,NULL,NULL,NULL,NULL 457,17,23,R6731571,NULL,NULL,NULL,NULL 458,18,23,Other,NULL,NULL,NULL,NULL 459,19,23,Red Hat,NULL,NULL,NULL,NULL 460,20,23,Juliet,NULL,NULL,NULL,NULL 461,1,24,23.1671,53.715,NULL,NULL,NULL 462,2,24,Hahn Premium,NULL,NULL,NULL,NULL 463,3,24,two months,NULL,NULL,NULL,NULL 464,4,24,65042666,NULL,NULL,NULL,NULL 465,5,24,Female,NULL,NULL,NULL,NULL 466,6,24,www.imdb.com,NULL,NULL,NULL,NULL 467,7,24,Christopher,NULL,NULL,NULL,NULL 468,8,24,malesuada augue ut lacus. Nulla tincidunt neque vitae semper egestas urna justo faucibus lectus a sollicitudin orci sem eget massa. Suspendisse eleifend. Cras sed,NULL,NULL,NULL,NULL 469,9,24,TAS,NULL,NULL,NULL,NULL 470,10,24,Ap #515-1667 Sodales Ave,NULL,NULL,NULL,NULL 471,11,24,Soccer,NULL,NULL,NULL,NULL 472,12,24,BC,NULL,NULL,NULL,NULL 473,13,24,(04) 7566 337,NULL,NULL,NULL,NULL 474,14,24,(06) 568 4361,NULL,NULL,NULL,NULL 475,15,24,orci.luctus.et@example.com,NULL,NULL,NULL,NULL 476,16,24,143319,NULL,NULL,NULL,NULL 477,17,24,T1151255,NULL,NULL,NULL,NULL 478,18,24,Firefox,NULL,NULL,NULL,NULL 479,19,24,Ubuntu,NULL,NULL,NULL,NULL 480,20,24,Abraham,NULL,NULL,NULL,NULL 481,1,25,94.3662,97.4061,NULL,NULL,NULL 482,2,25,Carlsberg,NULL,NULL,NULL,NULL 483,3,25,two months,NULL,NULL,NULL,NULL 484,4,25,32413945,NULL,NULL,NULL,NULL 485,5,25,Male,NULL,NULL,NULL,NULL 486,6,25,www.penny-arcade.com,NULL,NULL,NULL,NULL 487,7,25,Reagan,NULL,NULL,NULL,NULL 488,8,25,est mauris rhoncus id mollis nec cursus a enim. Suspendisse aliquet sem ut cursus luctus ipsum leo elementum sem vitae aliquam eros turpis non enim. Mauris quis turpis vitae purus gravida sagittis. Duis gravida. Praesent eu nulla at sem molestie sodales. Mauris blandit enim consequat,NULL,NULL,NULL,NULL 489,9,25,NT,NULL,NULL,NULL,NULL 490,10,25,P.O. Box 134 4569 Laoreet St.,NULL,NULL,NULL,NULL 491,11,25,Water polo,NULL,NULL,NULL,NULL 492,12,25,ON,NULL,NULL,NULL,NULL 493,13,25,(03) 9553 6317,NULL,NULL,NULL,NULL 494,14,25,(07) 2532 495,NULL,NULL,NULL,NULL 495,15,25,mus@example.com,NULL,NULL,NULL,NULL 496,16,25,4634,NULL,NULL,NULL,NULL 497,17,25,K8101535,NULL,NULL,NULL,NULL 498,18,25,Opera,NULL,NULL,NULL,NULL 499,19,25,Red Hat,NULL,NULL,NULL,NULL 500,20,25,Iliana,NULL,NULL,NULL,NULL 501,1,26,32.206,1.616,NULL,NULL,NULL 502,2,26,Hahn Premium,NULL,NULL,NULL,NULL 503,3,26,two months,NULL,NULL,NULL,NULL 504,4,26,27560570,NULL,NULL,NULL,NULL 505,5,26,Male,NULL,NULL,NULL,NULL 506,6,26,www.imdb.com,NULL,NULL,NULL,NULL 507,7,26,Elvis,NULL,NULL,NULL,NULL 508,8,26,blandit enim consequat purus. Maecenas libero est congue a aliquet vel vulputate eu odio. Phasellus at augue id ante dictum cursus. Nunc mauris elit dictum eu eleifend nec malesuada ut sem. Nulla interdum. Curabitur dictum.,NULL,NULL,NULL,NULL 509,9,26,WA,NULL,NULL,NULL,NULL 510,10,26,962 Orci St.,NULL,NULL,NULL,NULL 511,11,26,Football,NULL,NULL,NULL,NULL 512,12,26,YT,NULL,NULL,NULL,NULL 513,13,26,(09) 1175 3511,NULL,NULL,NULL,NULL 514,14,26,(07) 124 5726,NULL,NULL,NULL,NULL 515,15,26,enim.non.nisi@example.com,NULL,NULL,NULL,NULL 516,16,26,237275,NULL,NULL,NULL,NULL 517,17,26,E3520420,NULL,NULL,NULL,NULL 518,18,26,Internet Explorer,NULL,NULL,NULL,NULL 519,19,26,Linux,NULL,NULL,NULL,NULL 520,20,26,Aimee,NULL,NULL,NULL,NULL 521,1,27,47.7634,1.5396,NULL,NULL,NULL 522,2,27,Victoria Bitter,NULL,NULL,NULL,NULL 523,3,27,One month,NULL,NULL,NULL,NULL 524,4,27,2594126,NULL,NULL,NULL,NULL 525,5,27,Male,NULL,NULL,NULL,NULL 526,6,27,www.google.com,NULL,NULL,NULL,NULL 527,7,27,Hoyt,NULL,NULL,NULL,NULL 528,8,27,fringilla cursus purus. Nullam scelerisque neque sed sem egestas blandit. Nam nulla magna malesuada,NULL,NULL,NULL,NULL 529,9,27,VIC,NULL,NULL,NULL,NULL 530,10,27,150-575 Ante Rd.,NULL,NULL,NULL,NULL 531,11,27,Tennis,NULL,NULL,NULL,NULL 532,12,27,MB,NULL,NULL,NULL,NULL 533,13,27,(02) 4669 2000,NULL,NULL,NULL,NULL 534,14,27,(02) 6943 394,NULL,NULL,NULL,NULL 535,15,27,egestas@example.com,NULL,NULL,NULL,NULL 536,16,27,14334,NULL,NULL,NULL,NULL 537,17,27,T338567,NULL,NULL,NULL,NULL 538,18,27,Opera,NULL,NULL,NULL,NULL 539,19,27,MacOS,NULL,NULL,NULL,NULL 540,20,27,Daphne,NULL,NULL,NULL,NULL 541,1,28,4.2756,50.1417,NULL,NULL,NULL 542,2,28,Yebisu,NULL,NULL,NULL,NULL 543,3,28,One month,NULL,NULL,NULL,NULL 544,4,28,32660098,NULL,NULL,NULL,NULL 545,5,28,Male,NULL,NULL,NULL,NULL 546,6,28,www.penny-arcade.com,NULL,NULL,NULL,NULL 547,7,28,Moses,NULL,NULL,NULL,NULL 548,8,28,pede blandit congue. In scelerisque scelerisque dui. Suspendisse ac metus vitae velit egestas lacinia. Sed congue elit sed consequat auctor nunc nulla vulputate dui nec tempus mauris erat eget ipsum. Suspendisse sagittis. Nullam vitae diam. Proin dolor. Nulla semper tellus id nunc interdum feugiat. Sed nec metus facilisis lorem tristique,NULL,NULL,NULL,NULL 549,9,28,NT,NULL,NULL,NULL,NULL 550,10,28,Ap #609-6728 Imperdiet St.,NULL,NULL,NULL,NULL 551,11,28,Table tennis,NULL,NULL,NULL,NULL 552,12,28,SK,NULL,NULL,NULL,NULL 553,13,28,(00) 227 1668,NULL,NULL,NULL,NULL 554,14,28,(01) 2329 2114,NULL,NULL,NULL,NULL 555,15,28,Donec.tempor.est@example.com,NULL,NULL,NULL,NULL 556,16,28,523677,NULL,NULL,NULL,NULL 557,17,28,I70701,NULL,NULL,NULL,NULL 558,18,28,Opera,NULL,NULL,NULL,NULL 559,19,28,Free-BSD,NULL,NULL,NULL,NULL 560,20,28,Beatrice,NULL,NULL,NULL,NULL 561,1,29,31.1239,71.1262,NULL,NULL,NULL 562,2,29,Victoria Bitter,NULL,NULL,NULL,NULL 563,3,29,two months,NULL,NULL,NULL,NULL 564,4,29,51224618,NULL,NULL,NULL,NULL 565,5,29,Male,NULL,NULL,NULL,NULL 566,6,29,www.penny-arcade.com,NULL,NULL,NULL,NULL 567,7,29,Mechelle,NULL,NULL,NULL,NULL 568,8,29,ridiculus mus. Aenean eget magna. Suspendisse tristique neque venenatis lacus. Etiam bibendum fermentum metus. Aenean sed pede,NULL,NULL,NULL,NULL 569,9,29,WA,NULL,NULL,NULL,NULL 570,10,29,P.O. Box 405 7532 Cras Av.,NULL,NULL,NULL,NULL 571,11,29,Squash,NULL,NULL,NULL,NULL 572,12,29,NB,NULL,NULL,NULL,NULL 573,13,29,(07) 7212 2418,NULL,NULL,NULL,NULL 574,14,29,(04) 132 1598,NULL,NULL,NULL,NULL 575,15,29,hymenaeos.Mauris@example.com,NULL,NULL,NULL,NULL 576,16,29,215562,NULL,NULL,NULL,NULL 577,17,29,E364302,NULL,NULL,NULL,NULL 578,18,29,Safari,NULL,NULL,NULL,NULL 579,19,29,Free-BSD,NULL,NULL,NULL,NULL 580,20,29,Naida,NULL,NULL,NULL,NULL 581,1,30,6.2957,20.2033,NULL,NULL,NULL 582,2,30,Asahi,NULL,NULL,NULL,NULL 583,3,30,One month,NULL,NULL,NULL,NULL 584,4,30,1512640,NULL,NULL,NULL,NULL 585,5,30,Male,NULL,NULL,NULL,NULL 586,6,30,www.imdb.com,NULL,NULL,NULL,NULL 587,7,30,Mason,NULL,NULL,NULL,NULL 588,8,30,vitae odio sagittis semper. Nam tempor diam dictum sapien. Aenean massa. Integer vitae nibh. Donec est mauris rhoncus id mollis nec cursus a enim. Suspendisse aliquet sem ut cursus luctus ipsum leo elementum sem vitae aliquam eros turpis non enim.,NULL,NULL,NULL,NULL 589,9,30,SA,NULL,NULL,NULL,NULL 590,10,30,P.O. Box 154 7467 Semper Avenue,NULL,NULL,NULL,NULL 591,11,30,Water polo,NULL,NULL,NULL,NULL 592,12,30,NT,NULL,NULL,NULL,NULL 593,13,30,(03) 5207 940,NULL,NULL,NULL,NULL 594,14,30,(04) 5042 767,NULL,NULL,NULL,NULL 595,15,30,malesuada.malesuada@example.com,NULL,NULL,NULL,NULL 596,16,30,410215,NULL,NULL,NULL,NULL 597,17,30,A1025198,NULL,NULL,NULL,NULL 598,18,30,Firefox,NULL,NULL,NULL,NULL 599,19,30,Free-BSD,NULL,NULL,NULL,NULL 600,20,30,Ira,NULL,NULL,NULL,NULL 601,1,31,68.563,3.035,NULL,NULL,NULL 602,2,31,Victoria Bitter,NULL,NULL,NULL,NULL 603,3,31,two months,NULL,NULL,NULL,NULL 604,4,31,33433604,NULL,NULL,NULL,NULL 605,5,31,Male,NULL,NULL,NULL,NULL 606,6,31,www.imdb.com,NULL,NULL,NULL,NULL 607,7,31,Iola,NULL,NULL,NULL,NULL 608,8,31,sem ut cursus luctus ipsum leo elementum sem vitae aliquam eros turpis non enim. Mauris quis turpis vitae purus gravida sagittis. Duis gravida. Praesent eu nulla at sem,NULL,NULL,NULL,NULL 609,9,31,VIC,NULL,NULL,NULL,NULL 610,10,31,Ap #226-439 Blandit Rd.,NULL,NULL,NULL,NULL 611,11,31,Tennis,NULL,NULL,NULL,NULL 612,12,31,NL,NULL,NULL,NULL,NULL 613,13,31,(05) 6559 2043,NULL,NULL,NULL,NULL 614,14,31,(03) 9405 1305,NULL,NULL,NULL,NULL 615,15,31,risus.In.mi@example.com,NULL,NULL,NULL,NULL 616,16,31,324145,NULL,NULL,NULL,NULL 617,17,31,E2222517,NULL,NULL,NULL,NULL 618,18,31,Firefox,NULL,NULL,NULL,NULL 619,19,31,MacOS,NULL,NULL,NULL,NULL 620,20,31,Nigel,NULL,NULL,NULL,NULL 621,1,32,7.0029,26.324,NULL,NULL,NULL 622,2,32,Hahn Premium,NULL,NULL,NULL,NULL 623,3,32,One month,NULL,NULL,NULL,NULL 624,4,32,711540,NULL,NULL,NULL,NULL 625,5,32,Female,NULL,NULL,NULL,NULL 626,6,32,www.penny-arcade.com,NULL,NULL,NULL,NULL 627,7,32,Sonya,NULL,NULL,NULL,NULL 628,8,32,vitae velit egestas lacinia. Sed congue elit sed consequat auctor nunc nulla vulputate dui nec tempus mauris erat eget ipsum. Suspendisse sagittis. Nullam vitae diam. Proin dolor.,NULL,NULL,NULL,NULL 629,9,32,TAS,NULL,NULL,NULL,NULL 630,10,32,2344 Nunc St.,NULL,NULL,NULL,NULL 631,11,32,Badminton,NULL,NULL,NULL,NULL 632,12,32,YT,NULL,NULL,NULL,NULL 633,13,32,(01) 6513 2525,NULL,NULL,NULL,NULL 634,14,32,(02) 6174 463,NULL,NULL,NULL,NULL 635,15,32,tristique@example.com,NULL,NULL,NULL,NULL 636,16,32,47313,NULL,NULL,NULL,NULL 637,17,32,S5264136,NULL,NULL,NULL,NULL 638,18,32,Chrome,NULL,NULL,NULL,NULL 639,19,32,Windows,NULL,NULL,NULL,NULL 640,20,32,Dorothy,NULL,NULL,NULL,NULL 641,1,33,41.172,19.4731,NULL,NULL,NULL 642,2,33,Victoria Bitter,NULL,NULL,NULL,NULL 643,3,33,One month,NULL,NULL,NULL,NULL 644,4,33,300810,NULL,NULL,NULL,NULL 645,5,33,Female,NULL,NULL,NULL,NULL 646,6,33,www.moodle.org,NULL,NULL,NULL,NULL 647,7,33,Murphy,NULL,NULL,NULL,NULL 648,8,33,neque sed dictum eleifend nunc risus varius orci in consequat enim diam vel arcu. Curabitur ut odio vel,NULL,NULL,NULL,NULL 649,9,33,SA,NULL,NULL,NULL,NULL 650,10,33,113-6651 Cum Road,NULL,NULL,NULL,NULL 651,11,33,Water polo,NULL,NULL,NULL,NULL 652,12,33,MB,NULL,NULL,NULL,NULL 653,13,33,(08) 4745 1473,NULL,NULL,NULL,NULL 654,14,33,(01) 6311 6670,NULL,NULL,NULL,NULL 655,15,33,accumsan.laoreet.ipsum@example.com,NULL,NULL,NULL,NULL 656,16,33,66769,NULL,NULL,NULL,NULL 657,17,33,R4124353,NULL,NULL,NULL,NULL 658,18,33,Firefox,NULL,NULL,NULL,NULL 659,19,33,MacOS,NULL,NULL,NULL,NULL 660,20,33,Carter,NULL,NULL,NULL,NULL 661,1,34,75.7557,42.1408,NULL,NULL,NULL 662,2,34,Hahn Premium,NULL,NULL,NULL,NULL 663,3,34,One month,NULL,NULL,NULL,NULL 664,4,34,7004528,NULL,NULL,NULL,NULL 665,5,34,Male,NULL,NULL,NULL,NULL 666,6,34,www.imdb.com,NULL,NULL,NULL,NULL 667,7,34,Nasim,NULL,NULL,NULL,NULL 668,8,34,litora torquent per conubia nostra per inceptos hymenaeos. Mauris ut quam vel sapien imperdiet ornare. In faucibus. Morbi vehicula. Pellentesque tincidunt,NULL,NULL,NULL,NULL 669,9,34,VIC,NULL,NULL,NULL,NULL 670,10,34,969 Faucibus Street,NULL,NULL,NULL,NULL 671,11,34,Squash,NULL,NULL,NULL,NULL 672,12,34,NS,NULL,NULL,NULL,NULL 673,13,34,(09) 1763 7000,NULL,NULL,NULL,NULL 674,14,34,(02) 1253 1261,NULL,NULL,NULL,NULL 675,15,34,tortor@example.com,NULL,NULL,NULL,NULL 676,16,34,433416,NULL,NULL,NULL,NULL 677,17,34,J6632142,NULL,NULL,NULL,NULL 678,18,34,Safari,NULL,NULL,NULL,NULL 679,19,34,Red Hat,NULL,NULL,NULL,NULL 680,20,34,Marvin,NULL,NULL,NULL,NULL 681,1,35,9.6411,13.0734,NULL,NULL,NULL 682,2,35,Asahi,NULL,NULL,NULL,NULL 683,3,35,two months,NULL,NULL,NULL,NULL 684,4,35,3051275,NULL,NULL,NULL,NULL 685,5,35,Male,NULL,NULL,NULL,NULL 686,6,35,www.google.com,NULL,NULL,NULL,NULL 687,7,35,Ruby,NULL,NULL,NULL,NULL 688,8,35,vulputate mauris sagittis placerat. Cras dictum ultricies ligula. Nullam enim. Sed nulla ante iaculis nec eleifend non dapibus rutrum justo. Praesent luctus.,NULL,NULL,NULL,NULL 689,9,35,NSW,NULL,NULL,NULL,NULL 690,10,35,P.O. Box 676 408 Eget Avenue,NULL,NULL,NULL,NULL 691,11,35,Running,NULL,NULL,NULL,NULL 692,12,35,MB,NULL,NULL,NULL,NULL 693,13,35,(02) 410 5300,NULL,NULL,NULL,NULL 694,14,35,(07) 3764 1419,NULL,NULL,NULL,NULL 695,15,35,lacus.pede.sagittis@example.com,NULL,NULL,NULL,NULL 696,16,35,616322,NULL,NULL,NULL,NULL 697,17,35,T5006108,NULL,NULL,NULL,NULL 698,18,35,Other,NULL,NULL,NULL,NULL 699,19,35,Linux,NULL,NULL,NULL,NULL 700,20,35,Christen,NULL,NULL,NULL,NULL 701,1,36,12.6313,19.1669,NULL,NULL,NULL 702,2,36,Victoria Bitter,NULL,NULL,NULL,NULL 703,3,36,One month,NULL,NULL,NULL,NULL 704,4,36,99773510,NULL,NULL,NULL,NULL 705,5,36,Male,NULL,NULL,NULL,NULL 706,6,36,www.imdb.com,NULL,NULL,NULL,NULL 707,7,36,Meghan,NULL,NULL,NULL,NULL 708,8,36,nunc ullamcorper eu euismod ac fermentum vel mauris. Integer sem elit pharetra ut pharetra sed hendrerit a arcu. Sed et,NULL,NULL,NULL,NULL 709,9,36,NT,NULL,NULL,NULL,NULL 710,10,36,P.O. Box 422 643 Ante Street,NULL,NULL,NULL,NULL 711,11,36,Water polo,NULL,NULL,NULL,NULL 712,12,36,PE,NULL,NULL,NULL,NULL 713,13,36,(02) 265 224,NULL,NULL,NULL,NULL 714,14,36,(03) 5614 3966,NULL,NULL,NULL,NULL 715,15,36,convallis@example.com,NULL,NULL,NULL,NULL 716,16,36,110352,NULL,NULL,NULL,NULL 717,17,36,P2810500,NULL,NULL,NULL,NULL 718,18,36,Other,NULL,NULL,NULL,NULL 719,19,36,Ubuntu,NULL,NULL,NULL,NULL 720,20,36,Hammett,NULL,NULL,NULL,NULL 721,1,37,54.3215,23.1647,NULL,NULL,NULL 722,2,37,Victoria Bitter,NULL,NULL,NULL,NULL 723,3,37,One month,NULL,NULL,NULL,NULL 724,4,37,6984542,NULL,NULL,NULL,NULL 725,5,37,Male,NULL,NULL,NULL,NULL 726,6,37,www.moodle.org,NULL,NULL,NULL,NULL 727,7,37,Richard,NULL,NULL,NULL,NULL 728,8,37,In mi pede nonummy ut molestie in tempus eu ligula.,NULL,NULL,NULL,NULL 729,9,37,SA,NULL,NULL,NULL,NULL 730,10,37,272-151 Interdum Street,NULL,NULL,NULL,NULL 731,11,37,Water polo,NULL,NULL,NULL,NULL 732,12,37,MB,NULL,NULL,NULL,NULL 733,13,37,(02) 626 6067,NULL,NULL,NULL,NULL 734,14,37,(04) 2297 296,NULL,NULL,NULL,NULL 735,15,37,Quisque.tincidunt@example.com,NULL,NULL,NULL,NULL 736,16,37,136160,NULL,NULL,NULL,NULL 737,17,37,G763133,NULL,NULL,NULL,NULL 738,18,37,Internet Explorer,NULL,NULL,NULL,NULL 739,19,37,Linux,NULL,NULL,NULL,NULL 740,20,37,Arthur,NULL,NULL,NULL,NULL 741,1,38,4.0754,76.3366,NULL,NULL,NULL 742,2,38,Asahi,NULL,NULL,NULL,NULL 743,3,38,two months,NULL,NULL,NULL,NULL 744,4,38,14054008,NULL,NULL,NULL,NULL 745,5,38,Male,NULL,NULL,NULL,NULL 746,6,38,www.penny-arcade.com,NULL,NULL,NULL,NULL 747,7,38,Galena,NULL,NULL,NULL,NULL 748,8,38,ultrices mauris ipsum porta elit a feugiat tellus lorem eu metus. In lorem. Donec elementum lorem ut aliquam iaculis lacus pede sagittis augue eu tempor,NULL,NULL,NULL,NULL 749,9,38,SA,NULL,NULL,NULL,NULL 750,10,38,P.O. Box 34 5310 Duis St.,NULL,NULL,NULL,NULL 751,11,38,Squash,NULL,NULL,NULL,NULL 752,12,38,NU,NULL,NULL,NULL,NULL 753,13,38,(06) 6960 1169,NULL,NULL,NULL,NULL 754,14,38,(03) 5976 1413,NULL,NULL,NULL,NULL 755,15,38,Vivamus@example.com,NULL,NULL,NULL,NULL 756,16,38,594239,NULL,NULL,NULL,NULL 757,17,38,E213666,NULL,NULL,NULL,NULL 758,18,38,Firefox,NULL,NULL,NULL,NULL 759,19,38,Free-BSD,NULL,NULL,NULL,NULL 760,20,38,Beatrice,NULL,NULL,NULL,NULL 761,1,39,9.2774,43.3595,NULL,NULL,NULL 762,2,39,Hahn Premium,NULL,NULL,NULL,NULL 763,3,39,One month,NULL,NULL,NULL,NULL 764,4,39,1641598,NULL,NULL,NULL,NULL 765,5,39,Male,NULL,NULL,NULL,NULL 766,6,39,www.google.com,NULL,NULL,NULL,NULL 767,7,39,Derek,NULL,NULL,NULL,NULL 768,8,39,fringilla purus mauris a nunc. In at pede. Cras vulputate velit eu sem. Pellentesque ut ipsum ac mi eleifend egestas. Sed pharetra felis eget varius,NULL,NULL,NULL,NULL 769,9,39,NSW,NULL,NULL,NULL,NULL 770,10,39,Ap #306-828 Tempor Rd.,NULL,NULL,NULL,NULL 771,11,39,Water polo,NULL,NULL,NULL,NULL 772,12,39,PE,NULL,NULL,NULL,NULL 773,13,39,(02) 5610 5443,NULL,NULL,NULL,NULL 774,14,39,(03) 5453 744,NULL,NULL,NULL,NULL 775,15,39,dui@example.com,NULL,NULL,NULL,NULL 776,16,39,32301,NULL,NULL,NULL,NULL 777,17,39,V2027709,NULL,NULL,NULL,NULL 778,18,39,Other,NULL,NULL,NULL,NULL 779,19,39,Linux,NULL,NULL,NULL,NULL 780,20,39,Jonah,NULL,NULL,NULL,NULL 781,1,40,68.959,69.232,NULL,NULL,NULL 782,2,40,Asahi,NULL,NULL,NULL,NULL 783,3,40,two months,NULL,NULL,NULL,NULL 784,4,40,3947229,NULL,NULL,NULL,NULL 785,5,40,Female,NULL,NULL,NULL,NULL 786,6,40,www.imdb.com,NULL,NULL,NULL,NULL 787,7,40,Nasim,NULL,NULL,NULL,NULL 788,8,40,ullamcorper eu euismod ac fermentum vel mauris. Integer sem elit pharetra ut pharetra sed hendrerit a arcu. Sed et libero. Proin mi. Aliquam gravida mauris ut,NULL,NULL,NULL,NULL 789,9,40,NSW,NULL,NULL,NULL,NULL 790,10,40,9523 Morbi Road,NULL,NULL,NULL,NULL 791,11,40,Squash,NULL,NULL,NULL,NULL 792,12,40,NT,NULL,NULL,NULL,NULL 793,13,40,(08) 5218 4457,NULL,NULL,NULL,NULL 794,14,40,(04) 976 169,NULL,NULL,NULL,NULL 795,15,40,id@example.com,NULL,NULL,NULL,NULL 796,16,40,59544,NULL,NULL,NULL,NULL 797,17,40,H6915313,NULL,NULL,NULL,NULL 798,18,40,Opera,NULL,NULL,NULL,NULL 799,19,40,Red Hat,NULL,NULL,NULL,NULL 800,20,40,Keefe,NULL,NULL,NULL,NULL 801,1,41,3.3136,99.2675,NULL,NULL,NULL 802,2,41,Asahi,NULL,NULL,NULL,NULL 803,3,41,two months,NULL,NULL,NULL,NULL 804,4,41,41504776,NULL,NULL,NULL,NULL 805,5,41,Male,NULL,NULL,NULL,NULL 806,6,41,www.google.com,NULL,NULL,NULL,NULL 807,7,41,MacKensie,NULL,NULL,NULL,NULL 808,8,41,Vivamus euismod urna. Nullam lobortis quam a felis ullamcorper viverra. Maecenas iaculis aliquet diam. Sed diam lorem,NULL,NULL,NULL,NULL 809,9,41,NT,NULL,NULL,NULL,NULL 810,10,41,7605 Suspendisse Road,NULL,NULL,NULL,NULL 811,11,41,Badminton,NULL,NULL,NULL,NULL 812,12,41,AB,NULL,NULL,NULL,NULL 813,13,41,(00) 1760 4365,NULL,NULL,NULL,NULL 814,14,41,(06) 5015 7144,NULL,NULL,NULL,NULL 815,15,41,Duis.ac@example.com,NULL,NULL,NULL,NULL 816,16,41,810458,NULL,NULL,NULL,NULL 817,17,41,T4251272,NULL,NULL,NULL,NULL 818,18,41,Internet Explorer,NULL,NULL,NULL,NULL 819,19,41,Free-BSD,NULL,NULL,NULL,NULL 820,20,41,Yael,NULL,NULL,NULL,NULL 821,1,42,1.1466,1.15,NULL,NULL,NULL 822,2,42,Carlsberg,NULL,NULL,NULL,NULL 823,3,42,two months,NULL,NULL,NULL,NULL 824,4,42,29639429,NULL,NULL,NULL,NULL 825,5,42,Male,NULL,NULL,NULL,NULL 826,6,42,www.google.com,NULL,NULL,NULL,NULL 827,7,42,Abel,NULL,NULL,NULL,NULL 828,8,42,nec ante blandit viverra. Donec tempus lorem fringilla ornare placerat orci lacus vestibulum lorem sit amet ultricies sem magna nec quam. Curabitur vel lectus. Cum sociis natoque penatibus et magnis dis parturient montes,NULL,NULL,NULL,NULL 829,9,42,NT,NULL,NULL,NULL,NULL 830,10,42,174 Dictum Rd.,NULL,NULL,NULL,NULL 831,11,42,Table tennis,NULL,NULL,NULL,NULL 832,12,42,ON,NULL,NULL,NULL,NULL 833,13,42,(01) 138 7353,NULL,NULL,NULL,NULL 834,14,42,(00) 5250 6052,NULL,NULL,NULL,NULL 835,15,42,tempor.est@example.com,NULL,NULL,NULL,NULL 836,16,42,453048,NULL,NULL,NULL,NULL 837,17,42,I6163745,NULL,NULL,NULL,NULL 838,18,42,Opera,NULL,NULL,NULL,NULL 839,19,42,Windows,NULL,NULL,NULL,NULL 840,20,42,Drake,NULL,NULL,NULL,NULL 841,1,43,44.2968,40.753,NULL,NULL,NULL 842,2,43,Yebisu,NULL,NULL,NULL,NULL 843,3,43,two months,NULL,NULL,NULL,NULL 844,4,43,30675440,NULL,NULL,NULL,NULL 845,5,43,Female,NULL,NULL,NULL,NULL 846,6,43,www.moodle.org,NULL,NULL,NULL,NULL 847,7,43,Dylan,NULL,NULL,NULL,NULL 848,8,43,cursus luctus ipsum leo elementum sem vitae aliquam eros turpis non enim. Mauris quis turpis vitae purus gravida sagittis. Duis gravida. Praesent eu nulla at sem molestie sodales. Mauris blandit enim consequat purus. Maecenas libero est congue a aliquet vel vulputate eu odio. Phasellus at augue id ante,NULL,NULL,NULL,NULL 849,9,43,VIC,NULL,NULL,NULL,NULL 850,10,43,265 Pede. Rd.,NULL,NULL,NULL,NULL 851,11,43,Table tennis,NULL,NULL,NULL,NULL 852,12,43,MB,NULL,NULL,NULL,NULL 853,13,43,(05) 5164 131,NULL,NULL,NULL,NULL 854,14,43,(02) 512 5211,NULL,NULL,NULL,NULL 855,15,43,Quisque.libero.lacus@example.com,NULL,NULL,NULL,NULL 856,16,43,614467,NULL,NULL,NULL,NULL 857,17,43,E1639434,NULL,NULL,NULL,NULL 858,18,43,Safari,NULL,NULL,NULL,NULL 859,19,43,Net-BSD,NULL,NULL,NULL,NULL 860,20,43,Jana,NULL,NULL,NULL,NULL 861,1,44,39.7414,35.5417,NULL,NULL,NULL 862,2,44,Asahi,NULL,NULL,NULL,NULL 863,3,44,One month,NULL,NULL,NULL,NULL 864,4,44,4703494,NULL,NULL,NULL,NULL 865,5,44,Male,NULL,NULL,NULL,NULL 866,6,44,www.google.com,NULL,NULL,NULL,NULL 867,7,44,Fulton,NULL,NULL,NULL,NULL 868,8,44,a neque. Nullam ut nisi a odio semper cursus. Integer mollis. Integer tincidunt aliquam arcu. Aliquam ultrices iaculis odio. Nam interdum enim,NULL,NULL,NULL,NULL 869,9,44,NT,NULL,NULL,NULL,NULL 870,10,44,112-1016 Lorem Rd.,NULL,NULL,NULL,NULL 871,11,44,Football,NULL,NULL,NULL,NULL 872,12,44,NS,NULL,NULL,NULL,NULL 873,13,44,(02) 9718 7417,NULL,NULL,NULL,NULL 874,14,44,(09) 6156 1567,NULL,NULL,NULL,NULL 875,15,44,Suspendisse.sagittis.Nullam@example.com,NULL,NULL,NULL,NULL 876,16,44,503217,NULL,NULL,NULL,NULL 877,17,44,Z183356,NULL,NULL,NULL,NULL 878,18,44,Opera,NULL,NULL,NULL,NULL 879,19,44,MacOS,NULL,NULL,NULL,NULL 880,20,44,Oleg,NULL,NULL,NULL,NULL 881,1,45,59.2162,94.1512,NULL,NULL,NULL 882,2,45,Victoria Bitter,NULL,NULL,NULL,NULL 883,3,45,two months,NULL,NULL,NULL,NULL 884,4,45,15335360,NULL,NULL,NULL,NULL 885,5,45,Female,NULL,NULL,NULL,NULL 886,6,45,www.penny-arcade.com,NULL,NULL,NULL,NULL 887,7,45,Deirdre,NULL,NULL,NULL,NULL 888,8,45,ornare lectus justo eu arcu. Morbi sit amet massa. Quisque porttitor eros nec tellus. Nunc lectus pede ultrices a auctor non,NULL,NULL,NULL,NULL 889,9,45,TAS,NULL,NULL,NULL,NULL 890,10,45,4156 Convallis Road,NULL,NULL,NULL,NULL 891,11,45,Water polo,NULL,NULL,NULL,NULL 892,12,45,NT,NULL,NULL,NULL,NULL 893,13,45,(04) 5296 6098,NULL,NULL,NULL,NULL 894,14,45,(03) 412 6944,NULL,NULL,NULL,NULL 895,15,45,eleifend.Cras@example.com,NULL,NULL,NULL,NULL 896,16,45,720297,NULL,NULL,NULL,NULL 897,17,45,T54319,NULL,NULL,NULL,NULL 898,18,45,Chrome,NULL,NULL,NULL,NULL 899,19,45,Free-BSD,NULL,NULL,NULL,NULL 900,20,45,Rooney,NULL,NULL,NULL,NULL 901,1,46,6.7411,12.216,NULL,NULL,NULL 902,2,46,Yebisu,NULL,NULL,NULL,NULL 903,3,46,One month,NULL,NULL,NULL,NULL 904,4,46,152631,NULL,NULL,NULL,NULL 905,5,46,Female,NULL,NULL,NULL,NULL 906,6,46,www.google.com,NULL,NULL,NULL,NULL 907,7,46,Gray,NULL,NULL,NULL,NULL 908,8,46,ullamcorper. Duis cursus diam at pretium aliquet metus urna convallis erat eget tincidunt dui augue,NULL,NULL,NULL,NULL 909,9,46,WA,NULL,NULL,NULL,NULL 910,10,46,P.O. Box 240 3160 Pede. St.,NULL,NULL,NULL,NULL 911,11,46,Tennis,NULL,NULL,NULL,NULL 912,12,46,AB,NULL,NULL,NULL,NULL 913,13,46,(07) 1208 3498,NULL,NULL,NULL,NULL 914,14,46,(08) 3456 1561,NULL,NULL,NULL,NULL 915,15,46,egestas.Aliquam@example.com,NULL,NULL,NULL,NULL 916,16,46,250721,NULL,NULL,NULL,NULL 917,17,46,V610582,NULL,NULL,NULL,NULL 918,18,46,Opera,NULL,NULL,NULL,NULL 919,19,46,Linux,NULL,NULL,NULL,NULL 920,20,46,Bradley,NULL,NULL,NULL,NULL 921,1,47,63.3348,29.3112,NULL,NULL,NULL 922,2,47,Hahn Premium,NULL,NULL,NULL,NULL 923,3,47,One month,NULL,NULL,NULL,NULL 924,4,47,51469664,NULL,NULL,NULL,NULL 925,5,47,Male,NULL,NULL,NULL,NULL 926,6,47,www.google.com,NULL,NULL,NULL,NULL 927,7,47,Linus,NULL,NULL,NULL,NULL 928,8,47,lacinia. Sed congue elit sed consequat auctor nunc nulla vulputate dui nec tempus mauris erat eget ipsum. Suspendisse sagittis. Nullam vitae diam. Proin dolor. Nulla semper tellus,NULL,NULL,NULL,NULL 929,9,47,WA,NULL,NULL,NULL,NULL 930,10,47,513-634 Cursus. Av.,NULL,NULL,NULL,NULL 931,11,47,Table tennis,NULL,NULL,NULL,NULL 932,12,47,NB,NULL,NULL,NULL,NULL 933,13,47,(05) 841 5052,NULL,NULL,NULL,NULL 934,14,47,(03) 9566 161,NULL,NULL,NULL,NULL 935,15,47,ac.arcu.Nunc@example.com,NULL,NULL,NULL,NULL 936,16,47,663522,NULL,NULL,NULL,NULL 937,17,47,E5102157,NULL,NULL,NULL,NULL 938,18,47,Firefox,NULL,NULL,NULL,NULL 939,19,47,Ubuntu,NULL,NULL,NULL,NULL 940,20,47,Chanda,NULL,NULL,NULL,NULL 941,1,48,24.1237,28.2054,NULL,NULL,NULL 942,2,48,Yebisu,NULL,NULL,NULL,NULL 943,3,48,One month,NULL,NULL,NULL,NULL 944,4,48,73627152,NULL,NULL,NULL,NULL 945,5,48,Male,NULL,NULL,NULL,NULL 946,6,48,www.google.com,NULL,NULL,NULL,NULL 947,7,48,Kibo,NULL,NULL,NULL,NULL 948,8,48,neque et nunc. Quisque ornare tortor at risus. Nunc ac sem ut dolor dapibus gravida. Aliquam tincidunt nunc ac mattis ornare lectus ante dictum mi ac,NULL,NULL,NULL,NULL 949,9,48,WA,NULL,NULL,NULL,NULL 950,10,48,4141 A Road,NULL,NULL,NULL,NULL 951,11,48,Soccer,NULL,NULL,NULL,NULL 952,12,48,NB,NULL,NULL,NULL,NULL 953,13,48,(05) 132 5118,NULL,NULL,NULL,NULL 954,14,48,(07) 1017 177,NULL,NULL,NULL,NULL 955,15,48,dui@example.com,NULL,NULL,NULL,NULL 956,16,48,556220,NULL,NULL,NULL,NULL 957,17,48,X1677572,NULL,NULL,NULL,NULL 958,18,48,Internet Explorer,NULL,NULL,NULL,NULL 959,19,48,Windows,NULL,NULL,NULL,NULL 960,20,48,Bree,NULL,NULL,NULL,NULL 961,1,49,25.9162,65.6405,NULL,NULL,NULL 962,2,49,Carlsberg,NULL,NULL,NULL,NULL 963,3,49,One month,NULL,NULL,NULL,NULL 964,4,49,17210671,NULL,NULL,NULL,NULL 965,5,49,Female,NULL,NULL,NULL,NULL 966,6,49,www.google.com,NULL,NULL,NULL,NULL 967,7,49,Amal,NULL,NULL,NULL,NULL 968,8,49,leo. Morbi neque tellus imperdiet non vestibulum nec euismod in dolor. Fusce feugiat. Lorem ipsum dolor sit amet consectetuer adipiscing elit. Aliquam auctor velit,NULL,NULL,NULL,NULL 969,9,49,SA,NULL,NULL,NULL,NULL 970,10,49,P.O. Box 133 1970 Etiam Avenue,NULL,NULL,NULL,NULL 971,11,49,Badminton,NULL,NULL,NULL,NULL 972,12,49,QC,NULL,NULL,NULL,NULL 973,13,49,(09) 1533 1370,NULL,NULL,NULL,NULL 974,14,49,(08) 1210 1139,NULL,NULL,NULL,NULL 975,15,49,lorem.semper@example.com,NULL,NULL,NULL,NULL 976,16,49,14764,NULL,NULL,NULL,NULL 977,17,49,N1521300,NULL,NULL,NULL,NULL 978,18,49,Firefox,NULL,NULL,NULL,NULL 979,19,49,Linux,NULL,NULL,NULL,NULL 980,20,49,Karly,NULL,NULL,NULL,NULL 981,1,50,94.624,46.3068,NULL,NULL,NULL 982,2,50,Yebisu,NULL,NULL,NULL,NULL 983,3,50,One month,NULL,NULL,NULL,NULL 984,4,50,134335,NULL,NULL,NULL,NULL 985,5,50,Female,NULL,NULL,NULL,NULL 986,6,50,www.google.com,NULL,NULL,NULL,NULL 987,7,50,Hedwig,NULL,NULL,NULL,NULL 988,8,50,orci quis lectus. Nullam suscipit est ac facilisis facilisis magna tellus faucibus leo in lobortis tellus justo sit amet nulla. Donec non justo. Proin non massa non ante bibendum ullamcorper. Duis cursus diam at pretium aliquet metus urna convallis erat eget,NULL,NULL,NULL,NULL 989,9,50,WA,NULL,NULL,NULL,NULL 990,10,50,P.O. Box 59 483 Risus. Road,NULL,NULL,NULL,NULL 991,11,50,Tennis,NULL,NULL,NULL,NULL 992,12,50,ON,NULL,NULL,NULL,NULL 993,13,50,(05) 1031 622,NULL,NULL,NULL,NULL 994,14,50,(04) 2438 3296,NULL,NULL,NULL,NULL 995,15,50,eu@example.com,NULL,NULL,NULL,NULL 996,16,50,94219,NULL,NULL,NULL,NULL 997,17,50,K4525250,NULL,NULL,NULL,NULL 998,18,50,Firefox,NULL,NULL,NULL,NULL 999,19,50,Net-BSD,NULL,NULL,NULL,NULL 1000,20,50,Bernard,NULL,NULL,NULL,NULL 1001,1,51,19.3065,57.3167,NULL,NULL,NULL 1002,2,51,Asahi,NULL,NULL,NULL,NULL 1003,3,51,One month,NULL,NULL,NULL,NULL 1004,4,51,6741400,NULL,NULL,NULL,NULL 1005,5,51,Female,NULL,NULL,NULL,NULL 1006,6,51,www.imdb.com,NULL,NULL,NULL,NULL 1007,7,51,Robin,NULL,NULL,NULL,NULL 1008,8,51,in cursus et eros. Proin ultrices. Duis volutpat nunc sit amet metus. Aliquam erat volutpat. Nulla facilisis. Suspendisse commodo tincidunt nibh. Phasellus nulla. Integer vulputate risus a ultricies adipiscing enim mi tempor lorem eget mollis lectus pede et risus. Quisque libero lacus varius et euismod et commodo at libero. Morbi,NULL,NULL,NULL,NULL 1009,9,51,VIC,NULL,NULL,NULL,NULL 1010,10,51,Ap #568-6233 Amet St.,NULL,NULL,NULL,NULL 1011,11,51,Basketball,NULL,NULL,NULL,NULL 1012,12,51,SK,NULL,NULL,NULL,NULL 1013,13,51,(07) 5035 7156,NULL,NULL,NULL,NULL 1014,14,51,(01) 7246 9959,NULL,NULL,NULL,NULL 1015,15,51,Quisque.nonummy@example.com,NULL,NULL,NULL,NULL 1016,16,51,625754,NULL,NULL,NULL,NULL 1017,17,51,Q3336224,NULL,NULL,NULL,NULL 1018,18,51,Firefox,NULL,NULL,NULL,NULL 1019,19,51,Ubuntu,NULL,NULL,NULL,NULL 1020,20,51,Marah,NULL,NULL,NULL,NULL 1021,1,52,17.577,6.524,NULL,NULL,NULL 1022,2,52,Yebisu,NULL,NULL,NULL,NULL 1023,3,52,One month,NULL,NULL,NULL,NULL 1024,4,52,75320618,NULL,NULL,NULL,NULL 1025,5,52,Male,NULL,NULL,NULL,NULL 1026,6,52,www.imdb.com,NULL,NULL,NULL,NULL 1027,7,52,Karen,NULL,NULL,NULL,NULL 1028,8,52,velit dui semper et lacinia vitae sodales at velit. Pellentesque ultricies dignissim lacus. Aliquam rutrum lorem ac risus. Morbi metus. Vivamus euismod urna. Nullam lobortis quam a felis ullamcorper viverra. Maecenas iaculis aliquet diam. Sed diam lorem auctor quis tristique ac eleifend vitae erat. Vivamus nisi. Mauris nulla. Integer urna.,NULL,NULL,NULL,NULL 1029,9,52,NT,NULL,NULL,NULL,NULL 1030,10,52,Ap #162-7613 Turpis. Ave,NULL,NULL,NULL,NULL 1031,11,52,Running,NULL,NULL,NULL,NULL 1032,12,52,YT,NULL,NULL,NULL,NULL 1033,13,52,(08) 6016 4667,NULL,NULL,NULL,NULL 1034,14,52,(09) 3366 464,NULL,NULL,NULL,NULL 1035,15,52,dui.in@example.com,NULL,NULL,NULL,NULL 1036,16,52,21208,NULL,NULL,NULL,NULL 1037,17,52,P168533,NULL,NULL,NULL,NULL 1038,18,52,Internet Explorer,NULL,NULL,NULL,NULL 1039,19,52,MacOS,NULL,NULL,NULL,NULL 1040,20,52,Plato,NULL,NULL,NULL,NULL 1041,1,53,34.4257,5.5071,NULL,NULL,NULL 1042,2,53,Carlsberg,NULL,NULL,NULL,NULL 1043,3,53,One month,NULL,NULL,NULL,NULL 1044,4,53,50969965,NULL,NULL,NULL,NULL 1045,5,53,Female,NULL,NULL,NULL,NULL 1046,6,53,www.imdb.com,NULL,NULL,NULL,NULL 1047,7,53,Thor,NULL,NULL,NULL,NULL 1048,8,53,a tortor. Nunc commodo auctor velit. Aliquam nisl. Nulla eu neque pellentesque massa lobortis ultrices. Vivamus rhoncus. Donec est. Nunc ullamcorper velit in aliquet lobortis nisi nibh lacinia orci consectetuer euismod est arcu ac orci. Ut semper pretium neque. Morbi quis urna.,NULL,NULL,NULL,NULL 1049,9,53,NSW,NULL,NULL,NULL,NULL 1050,10,53,Ap #531-6646 Et Avenue,NULL,NULL,NULL,NULL 1051,11,53,Soccer,NULL,NULL,NULL,NULL 1052,12,53,NS,NULL,NULL,NULL,NULL 1053,13,53,(09) 158 413,NULL,NULL,NULL,NULL 1054,14,53,(02) 266 472,NULL,NULL,NULL,NULL 1055,15,53,Duis.risus@example.com,NULL,NULL,NULL,NULL 1056,16,53,270199,NULL,NULL,NULL,NULL 1057,17,53,F6627196,NULL,NULL,NULL,NULL 1058,18,53,Chrome,NULL,NULL,NULL,NULL 1059,19,53,Ubuntu,NULL,NULL,NULL,NULL 1060,20,53,Bianca,NULL,NULL,NULL,NULL 1061,1,54,43.9965,8.1401,NULL,NULL,NULL 1062,2,54,Yebisu,NULL,NULL,NULL,NULL 1063,3,54,One month,NULL,NULL,NULL,NULL 1064,4,54,45342156,NULL,NULL,NULL,NULL 1065,5,54,Male,NULL,NULL,NULL,NULL 1066,6,54,www.moodle.org,NULL,NULL,NULL,NULL 1067,7,54,Naomi,NULL,NULL,NULL,NULL 1068,8,54,ipsum nunc id enim. Curabitur massa. Vestibulum accumsan neque et nunc. Quisque ornare tortor at risus. Nunc ac sem ut dolor dapibus gravida.,NULL,NULL,NULL,NULL 1069,9,54,VIC,NULL,NULL,NULL,NULL 1070,10,54,673-273 Auctor Ave,NULL,NULL,NULL,NULL 1071,11,54,Water polo,NULL,NULL,NULL,NULL 1072,12,54,PE,NULL,NULL,NULL,NULL 1073,13,54,(00) 216 3362,NULL,NULL,NULL,NULL 1074,14,54,(07) 2127 6473,NULL,NULL,NULL,NULL 1075,15,54,ligula.Nullam.feugiat@example.com,NULL,NULL,NULL,NULL 1076,16,54,71636,NULL,NULL,NULL,NULL 1077,17,54,P6130160,NULL,NULL,NULL,NULL 1078,18,54,Firefox,NULL,NULL,NULL,NULL 1079,19,54,Free-BSD,NULL,NULL,NULL,NULL 1080,20,54,Ronan,NULL,NULL,NULL,NULL 1081,1,55,1.3742,11.622,NULL,NULL,NULL 1082,2,55,Asahi,NULL,NULL,NULL,NULL 1083,3,55,two months,NULL,NULL,NULL,NULL 1084,4,55,1062575,NULL,NULL,NULL,NULL 1085,5,55,Male,NULL,NULL,NULL,NULL 1086,6,55,www.moodle.org,NULL,NULL,NULL,NULL 1087,7,55,Lysandra,NULL,NULL,NULL,NULL 1088,8,55,Quisque fringilla euismod enim. Etiam gravida molestie arcu. Sed eu nibh vulputate mauris sagittis placerat. Cras dictum ultricies ligula. Nullam enim. Sed nulla ante iaculis nec eleifend non,NULL,NULL,NULL,NULL 1089,9,55,SA,NULL,NULL,NULL,NULL 1090,10,55,2065 Congue Rd.,NULL,NULL,NULL,NULL 1091,11,55,Basketball,NULL,NULL,NULL,NULL 1092,12,55,NL,NULL,NULL,NULL,NULL 1093,13,55,(07) 4136 2726,NULL,NULL,NULL,NULL 1094,14,55,(02) 972 4143,NULL,NULL,NULL,NULL 1095,15,55,mattis.Integer@example.com,NULL,NULL,NULL,NULL 1096,16,55,601011,NULL,NULL,NULL,NULL 1097,17,55,X676496,NULL,NULL,NULL,NULL 1098,18,55,Chrome,NULL,NULL,NULL,NULL 1099,19,55,Net-BSD,NULL,NULL,NULL,NULL 1100,20,55,Rajah,NULL,NULL,NULL,NULL 1101,1,56,30.7212,3.6995,NULL,NULL,NULL 1102,2,56,Hahn Premium,NULL,NULL,NULL,NULL 1103,3,56,One month,NULL,NULL,NULL,NULL 1104,4,56,2224740,NULL,NULL,NULL,NULL 1105,5,56,Female,NULL,NULL,NULL,NULL 1106,6,56,www.moodle.org,NULL,NULL,NULL,NULL 1107,7,56,Patricia,NULL,NULL,NULL,NULL 1108,8,56,lectus sit amet luctus vulputate nisi sem semper erat in consectetuer ipsum nunc id enim. Curabitur massa. Vestibulum accumsan neque et nunc. Quisque ornare tortor at risus. Nunc ac sem ut dolor dapibus gravida. Aliquam tincidunt,NULL,NULL,NULL,NULL 1109,9,56,WA,NULL,NULL,NULL,NULL 1110,10,56,P.O. Box 68 7752 Eu Rd.,NULL,NULL,NULL,NULL 1111,11,56,Soccer,NULL,NULL,NULL,NULL 1112,12,56,BC,NULL,NULL,NULL,NULL 1113,13,56,(06) 3315 5154,NULL,NULL,NULL,NULL 1114,14,56,(01) 772 269,NULL,NULL,NULL,NULL 1115,15,56,Sed.auctor.odio@example.com,NULL,NULL,NULL,NULL 1116,16,56,4272,NULL,NULL,NULL,NULL 1117,17,56,O562969,NULL,NULL,NULL,NULL 1118,18,56,Other,NULL,NULL,NULL,NULL 1119,19,56,Linux,NULL,NULL,NULL,NULL 1120,20,56,Wade,NULL,NULL,NULL,NULL 1121,1,57,24.6015,46.1244,NULL,NULL,NULL 1122,2,57,Victoria Bitter,NULL,NULL,NULL,NULL 1123,3,57,two months,NULL,NULL,NULL,NULL 1124,4,57,4453712,NULL,NULL,NULL,NULL 1125,5,57,Male,NULL,NULL,NULL,NULL 1126,6,57,www.google.com,NULL,NULL,NULL,NULL 1127,7,57,Evan,NULL,NULL,NULL,NULL 1128,8,57,elementum lorem ut aliquam iaculis lacus pede sagittis augue eu tempor erat neque non quam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam fringilla cursus purus. Nullam scelerisque neque sed sem egestas blandit. Nam nulla magna malesuada vel,NULL,NULL,NULL,NULL 1129,9,57,VIC,NULL,NULL,NULL,NULL 1130,10,57,Ap #755-2271 Non Rd.,NULL,NULL,NULL,NULL 1131,11,57,Tennis,NULL,NULL,NULL,NULL 1132,12,57,NB,NULL,NULL,NULL,NULL 1133,13,57,(03) 616 718,NULL,NULL,NULL,NULL 1134,14,57,(04) 6677 6222,NULL,NULL,NULL,NULL 1135,15,57,dolor.sit.amet@example.com,NULL,NULL,NULL,NULL 1136,16,57,612507,NULL,NULL,NULL,NULL 1137,17,57,S1012345,NULL,NULL,NULL,NULL 1138,18,57,Opera,NULL,NULL,NULL,NULL 1139,19,57,Free-BSD,NULL,NULL,NULL,NULL 1140,20,57,Raya,NULL,NULL,NULL,NULL 1141,1,58,19.0417,46.5722,NULL,NULL,NULL 1142,2,58,Carlsberg,NULL,NULL,NULL,NULL 1143,3,58,One month,NULL,NULL,NULL,NULL 1144,4,58,657198,NULL,NULL,NULL,NULL 1145,5,58,Male,NULL,NULL,NULL,NULL 1146,6,58,www.moodle.org,NULL,NULL,NULL,NULL 1147,7,58,Kirestin,NULL,NULL,NULL,NULL 1148,8,58,magnis dis parturient montes nascetur ridiculus mus. Aenean eget magna. Suspendisse tristique neque venenatis lacus. Etiam bibendum fermentum metus. Aenean,NULL,NULL,NULL,NULL 1149,9,58,NSW,NULL,NULL,NULL,NULL 1150,10,58,327-294 Interdum Rd.,NULL,NULL,NULL,NULL 1151,11,58,Football,NULL,NULL,NULL,NULL 1152,12,58,NS,NULL,NULL,NULL,NULL 1153,13,58,(00) 4455 514,NULL,NULL,NULL,NULL 1154,14,58,(05) 139 3408,NULL,NULL,NULL,NULL 1155,15,58,luctus.lobortis.Class@example.com,NULL,NULL,NULL,NULL 1156,16,58,153026,NULL,NULL,NULL,NULL 1157,17,58,K2997467,NULL,NULL,NULL,NULL 1158,18,58,Other,NULL,NULL,NULL,NULL 1159,19,58,MacOS,NULL,NULL,NULL,NULL 1160,20,58,Hiroko,NULL,NULL,NULL,NULL 1161,1,59,12.119,19.4211,NULL,NULL,NULL 1162,2,59,Hahn Premium,NULL,NULL,NULL,NULL 1163,3,59,two months,NULL,NULL,NULL,NULL 1164,4,59,5535812,NULL,NULL,NULL,NULL 1165,5,59,Female,NULL,NULL,NULL,NULL 1166,6,59,www.penny-arcade.com,NULL,NULL,NULL,NULL 1167,7,59,Hyacinth,NULL,NULL,NULL,NULL 1168,8,59,fermentum risus at fringilla purus mauris a nunc. In at pede. Cras vulputate velit eu sem. Pellentesque ut,NULL,NULL,NULL,NULL 1169,9,59,SA,NULL,NULL,NULL,NULL 1170,10,59,454-7216 Dictum St.,NULL,NULL,NULL,NULL 1171,11,59,Tennis,NULL,NULL,NULL,NULL 1172,12,59,QC,NULL,NULL,NULL,NULL 1173,13,59,(01) 3528 9607,NULL,NULL,NULL,NULL 1174,14,59,(02) 1510 538,NULL,NULL,NULL,NULL 1175,15,59,Nunc.ullamcorper@example.com,NULL,NULL,NULL,NULL 1176,16,59,761259,NULL,NULL,NULL,NULL 1177,17,59,Q1613442,NULL,NULL,NULL,NULL 1178,18,59,Firefox,NULL,NULL,NULL,NULL 1179,19,59,Ubuntu,NULL,NULL,NULL,NULL 1180,20,59,Burton,NULL,NULL,NULL,NULL 1181,1,60,52.338,56.7544,NULL,NULL,NULL 1182,2,60,Carlsberg,NULL,NULL,NULL,NULL 1183,3,60,One month,NULL,NULL,NULL,NULL 1184,4,60,565516,NULL,NULL,NULL,NULL 1185,5,60,Female,NULL,NULL,NULL,NULL 1186,6,60,www.imdb.com,NULL,NULL,NULL,NULL 1187,7,60,Zephania,NULL,NULL,NULL,NULL 1188,8,60,Nullam enim. Sed nulla ante iaculis nec eleifend non dapibus rutrum justo. Praesent luctus. Curabitur egestas nunc sed libero. Proin sed turpis nec mauris blandit mattis. Cras eget nisi dictum augue malesuada malesuada. Integer id magna et ipsum cursus vestibulum. Mauris magna. Duis dignissim tempor arcu. Vestibulum ut,NULL,NULL,NULL,NULL 1189,9,60,SA,NULL,NULL,NULL,NULL 1190,10,60,4099 Duis Road,NULL,NULL,NULL,NULL 1191,11,60,Squash,NULL,NULL,NULL,NULL 1192,12,60,ON,NULL,NULL,NULL,NULL 1193,13,60,(04) 3546 5953,NULL,NULL,NULL,NULL 1194,14,60,(04) 3138 2395,NULL,NULL,NULL,NULL 1195,15,60,eros.Proin.ultrices@example.com,NULL,NULL,NULL,NULL 1196,16,60,119760,NULL,NULL,NULL,NULL 1197,17,60,B3551424,NULL,NULL,NULL,NULL 1198,18,60,Chrome,NULL,NULL,NULL,NULL 1199,19,60,Windows,NULL,NULL,NULL,NULL 1200,20,60,Mallory,NULL,NULL,NULL,NULL 1201,1,61,12.0748,64.1249,NULL,NULL,NULL 1202,2,61,Carlsberg,NULL,NULL,NULL,NULL 1203,3,61,two months,NULL,NULL,NULL,NULL 1204,4,61,70520916,NULL,NULL,NULL,NULL 1205,5,61,Male,NULL,NULL,NULL,NULL 1206,6,61,www.google.com,NULL,NULL,NULL,NULL 1207,7,61,Marah,NULL,NULL,NULL,NULL 1208,8,61,augue ut lacus. Nulla tincidunt neque vitae semper egestas urna justo faucibus lectus a sollicitudin orci sem eget massa. Suspendisse eleifend. Cras sed leo. Cras vehicula aliquet libero. Integer in magna. Phasellus dolor elit pellentesque a facilisis non bibendum,NULL,NULL,NULL,NULL 1209,9,61,TAS,NULL,NULL,NULL,NULL 1210,10,61,564 Ultrices Road,NULL,NULL,NULL,NULL 1211,11,61,Squash,NULL,NULL,NULL,NULL 1212,12,61,NL,NULL,NULL,NULL,NULL 1213,13,61,(09) 5071 5372,NULL,NULL,NULL,NULL 1214,14,61,(07) 1632 7738,NULL,NULL,NULL,NULL 1215,15,61,eros@example.com,NULL,NULL,NULL,NULL 1216,16,61,410509,NULL,NULL,NULL,NULL 1217,17,61,F0717157,NULL,NULL,NULL,NULL 1218,18,61,Other,NULL,NULL,NULL,NULL 1219,19,61,Windows,NULL,NULL,NULL,NULL 1220,20,61,Timothy,NULL,NULL,NULL,NULL 1221,1,62,8.6246,37.1311,NULL,NULL,NULL 1222,2,62,Carlsberg,NULL,NULL,NULL,NULL 1223,3,62,One month,NULL,NULL,NULL,NULL 1224,4,62,417251,NULL,NULL,NULL,NULL 1225,5,62,Male,NULL,NULL,NULL,NULL 1226,6,62,www.moodle.org,NULL,NULL,NULL,NULL 1227,7,62,Jamal,NULL,NULL,NULL,NULL 1228,8,62,Morbi accumsan laoreet ipsum. Curabitur consequat lectus sit amet luctus vulputate nisi sem semper erat in consectetuer ipsum nunc id enim. Curabitur massa. Vestibulum,NULL,NULL,NULL,NULL 1229,9,62,TAS,NULL,NULL,NULL,NULL 1230,10,62,5030 Quisque Avenue,NULL,NULL,NULL,NULL 1231,11,62,Water polo,NULL,NULL,NULL,NULL 1232,12,62,NB,NULL,NULL,NULL,NULL 1233,13,62,(00) 372 164,NULL,NULL,NULL,NULL 1234,14,62,(07) 4010 6466,NULL,NULL,NULL,NULL 1235,15,62,pellentesque.a@example.com,NULL,NULL,NULL,NULL 1236,16,62,509735,NULL,NULL,NULL,NULL 1237,17,62,Q131420,NULL,NULL,NULL,NULL 1238,18,62,Chrome,NULL,NULL,NULL,NULL 1239,19,62,Free-BSD,NULL,NULL,NULL,NULL 1240,20,62,Juliet,NULL,NULL,NULL,NULL 1241,1,63,13.1424,21.514,NULL,NULL,NULL 1242,2,63,Victoria Bitter,NULL,NULL,NULL,NULL 1243,3,63,One month,NULL,NULL,NULL,NULL 1244,4,63,5639737,NULL,NULL,NULL,NULL 1245,5,63,Male,NULL,NULL,NULL,NULL 1246,6,63,www.moodle.org,NULL,NULL,NULL,NULL 1247,7,63,Fatima,NULL,NULL,NULL,NULL 1248,8,63,nunc ac mattis ornare lectus ante dictum mi ac mattis velit,NULL,NULL,NULL,NULL 1249,9,63,QLD,NULL,NULL,NULL,NULL 1250,10,63,P.O. Box 747 2212 Arcu. Ave,NULL,NULL,NULL,NULL 1251,11,63,Table tennis,NULL,NULL,NULL,NULL 1252,12,63,NT,NULL,NULL,NULL,NULL 1253,13,63,(06) 314 2141,NULL,NULL,NULL,NULL 1254,14,63,(09) 702 2399,NULL,NULL,NULL,NULL 1255,15,63,orci.Ut@example.com,NULL,NULL,NULL,NULL 1256,16,63,14332,NULL,NULL,NULL,NULL 1257,17,63,C17217,NULL,NULL,NULL,NULL 1258,18,63,Firefox,NULL,NULL,NULL,NULL 1259,19,63,Net-BSD,NULL,NULL,NULL,NULL 1260,20,63,Eugenia,NULL,NULL,NULL,NULL 1261,1,64,99.6914,60.0214,NULL,NULL,NULL 1262,2,64,Yebisu,NULL,NULL,NULL,NULL 1263,3,64,two months,NULL,NULL,NULL,NULL 1264,4,64,34024672,NULL,NULL,NULL,NULL 1265,5,64,Female,NULL,NULL,NULL,NULL 1266,6,64,www.google.com,NULL,NULL,NULL,NULL 1267,7,64,Eugenia,NULL,NULL,NULL,NULL 1268,8,64,varius et euismod et commodo at libero. Morbi accumsan laoreet ipsum. Curabitur consequat lectus sit amet luctus vulputate nisi sem semper erat in consectetuer ipsum nunc id enim. Curabitur massa. Vestibulum accumsan neque et nunc.,NULL,NULL,NULL,NULL 1269,9,64,TAS,NULL,NULL,NULL,NULL 1270,10,64,Ap #775-3212 Ut St.,NULL,NULL,NULL,NULL 1271,11,64,Badminton,NULL,NULL,NULL,NULL 1272,12,64,ON,NULL,NULL,NULL,NULL 1273,13,64,(07) 9642 312,NULL,NULL,NULL,NULL 1274,14,64,(08) 676 759,NULL,NULL,NULL,NULL 1275,15,64,Mauris@example.com,NULL,NULL,NULL,NULL 1276,16,64,704248,NULL,NULL,NULL,NULL 1277,17,64,C150684,NULL,NULL,NULL,NULL 1278,18,64,Firefox,NULL,NULL,NULL,NULL 1279,19,64,Linux,NULL,NULL,NULL,NULL 1280,20,64,Dakota,NULL,NULL,NULL,NULL 1281,1,65,49.298,21.337,NULL,NULL,NULL 1282,2,65,Asahi,NULL,NULL,NULL,NULL 1283,3,65,One month,NULL,NULL,NULL,NULL 1284,4,65,1597316,NULL,NULL,NULL,NULL 1285,5,65,Female,NULL,NULL,NULL,NULL 1286,6,65,www.moodle.org,NULL,NULL,NULL,NULL 1287,7,65,Paula,NULL,NULL,NULL,NULL 1288,8,65,elit erat vitae risus. Duis a mi fringilla mi lacinia mattis. Integer eu lacus. Quisque imperdiet erat nonummy ultricies ornare elit elit fermentum risus at fringilla purus mauris a nunc. In at pede. Cras vulputate velit eu sem. Pellentesque ut ipsum,NULL,NULL,NULL,NULL 1289,9,65,WA,NULL,NULL,NULL,NULL 1290,10,65,Ap #475-157 Sociis Street,NULL,NULL,NULL,NULL 1291,11,65,Table tennis,NULL,NULL,NULL,NULL 1292,12,65,NU,NULL,NULL,NULL,NULL 1293,13,65,(05) 6629 376,NULL,NULL,NULL,NULL 1294,14,65,(06) 2097 6738,NULL,NULL,NULL,NULL 1295,15,65,in.faucibus@example.com,NULL,NULL,NULL,NULL 1296,16,65,612314,NULL,NULL,NULL,NULL 1297,17,65,O7603148,NULL,NULL,NULL,NULL 1298,18,65,Chrome,NULL,NULL,NULL,NULL 1299,19,65,Windows,NULL,NULL,NULL,NULL 1300,20,65,Nichole,NULL,NULL,NULL,NULL 1301,1,66,43.4728,52.112,NULL,NULL,NULL 1302,2,66,Yebisu,NULL,NULL,NULL,NULL 1303,3,66,One month,NULL,NULL,NULL,NULL 1304,4,66,25270168,NULL,NULL,NULL,NULL 1305,5,66,Male,NULL,NULL,NULL,NULL 1306,6,66,www.imdb.com,NULL,NULL,NULL,NULL 1307,7,66,Delilah,NULL,NULL,NULL,NULL 1308,8,66,purus. Maecenas libero est congue a aliquet vel vulputate eu odio. Phasellus at augue id ante dictum cursus. Nunc mauris elit dictum,NULL,NULL,NULL,NULL 1309,9,66,ACT,NULL,NULL,NULL,NULL 1310,10,66,Ap #404-104 Eget St.,NULL,NULL,NULL,NULL 1311,11,66,Squash,NULL,NULL,NULL,NULL 1312,12,66,NT,NULL,NULL,NULL,NULL 1313,13,66,(04) 6698 7118,NULL,NULL,NULL,NULL 1314,14,66,(09) 2739 4204,NULL,NULL,NULL,NULL 1315,15,66,in@example.com,NULL,NULL,NULL,NULL 1316,16,66,366398,NULL,NULL,NULL,NULL 1317,17,66,J4142310,NULL,NULL,NULL,NULL 1318,18,66,Chrome,NULL,NULL,NULL,NULL 1319,19,66,Free-BSD,NULL,NULL,NULL,NULL 1320,20,66,Christine,NULL,NULL,NULL,NULL 1321,1,67,22.4365,70.7721,NULL,NULL,NULL 1322,2,67,Carlsberg,NULL,NULL,NULL,NULL 1323,3,67,One month,NULL,NULL,NULL,NULL 1324,4,67,1975761,NULL,NULL,NULL,NULL 1325,5,67,Female,NULL,NULL,NULL,NULL 1326,6,67,www.google.com,NULL,NULL,NULL,NULL 1327,7,67,Ignatius,NULL,NULL,NULL,NULL 1328,8,67,a nunc. In at pede. Cras vulputate velit eu sem. Pellentesque ut ipsum ac mi eleifend egestas. Sed pharetra felis eget varius ultrices mauris ipsum porta elit a feugiat tellus lorem eu metus. In lorem. Donec elementum lorem ut aliquam iaculis lacus pede sagittis,NULL,NULL,NULL,NULL 1329,9,67,WA,NULL,NULL,NULL,NULL 1330,10,67,P.O. Box 713 858 Dolor Road,NULL,NULL,NULL,NULL 1331,11,67,Basketball,NULL,NULL,NULL,NULL 1332,12,67,YT,NULL,NULL,NULL,NULL 1333,13,67,(08) 1747 255,NULL,NULL,NULL,NULL 1334,14,67,(03) 5202 1144,NULL,NULL,NULL,NULL 1335,15,67,nulla@example.com,NULL,NULL,NULL,NULL 1336,16,67,135941,NULL,NULL,NULL,NULL 1337,17,67,Y5697546,NULL,NULL,NULL,NULL 1338,18,67,Other,NULL,NULL,NULL,NULL 1339,19,67,Red Hat,NULL,NULL,NULL,NULL 1340,20,67,Jemima,NULL,NULL,NULL,NULL 1341,1,68,58.4726,65.7247,NULL,NULL,NULL 1342,2,68,Yebisu,NULL,NULL,NULL,NULL 1343,3,68,One month,NULL,NULL,NULL,NULL 1344,4,68,26451416,NULL,NULL,NULL,NULL 1345,5,68,Female,NULL,NULL,NULL,NULL 1346,6,68,www.penny-arcade.com,NULL,NULL,NULL,NULL 1347,7,68,Marvin,NULL,NULL,NULL,NULL 1348,8,68,Cum sociis natoque penatibus et magnis dis parturient montes nascetur ridiculus mus. Proin vel nisl. Quisque fringilla euismod enim. Etiam gravida molestie arcu. Sed eu nibh,NULL,NULL,NULL,NULL 1349,9,68,WA,NULL,NULL,NULL,NULL 1350,10,68,29-4645 Nascetur Av.,NULL,NULL,NULL,NULL 1351,11,68,Tennis,NULL,NULL,NULL,NULL 1352,12,68,ON,NULL,NULL,NULL,NULL 1353,13,68,(04) 8103 331,NULL,NULL,NULL,NULL 1354,14,68,(09) 623 243,NULL,NULL,NULL,NULL 1355,15,68,Suspendisse.tristique.neque@example.com,NULL,NULL,NULL,NULL 1356,16,68,12576,NULL,NULL,NULL,NULL 1357,17,68,A7705459,NULL,NULL,NULL,NULL 1358,18,68,Internet Explorer,NULL,NULL,NULL,NULL 1359,19,68,Linux,NULL,NULL,NULL,NULL 1360,20,68,Wing,NULL,NULL,NULL,NULL 1361,1,69,35.4251,3.1419,NULL,NULL,NULL 1362,2,69,Hahn Premium,NULL,NULL,NULL,NULL 1363,3,69,two months,NULL,NULL,NULL,NULL 1364,4,69,6014545,NULL,NULL,NULL,NULL 1365,5,69,Female,NULL,NULL,NULL,NULL 1366,6,69,www.penny-arcade.com,NULL,NULL,NULL,NULL 1367,7,69,Wing,NULL,NULL,NULL,NULL 1368,8,69,habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam fringilla cursus purus. Nullam scelerisque neque sed sem egestas blandit. Nam nulla magna malesuada vel convallis in cursus et eros. Proin ultrices. Duis volutpat nunc sit amet,NULL,NULL,NULL,NULL 1369,9,69,WA,NULL,NULL,NULL,NULL 1370,10,69,6957 Nam Street,NULL,NULL,NULL,NULL 1371,11,69,Water polo,NULL,NULL,NULL,NULL 1372,12,69,MB,NULL,NULL,NULL,NULL 1373,13,69,(02) 3165 1456,NULL,NULL,NULL,NULL 1374,14,69,(09) 1214 5776,NULL,NULL,NULL,NULL 1375,15,69,mus@example.com,NULL,NULL,NULL,NULL 1376,16,69,229611,NULL,NULL,NULL,NULL 1377,17,69,Y0732975,NULL,NULL,NULL,NULL 1378,18,69,Internet Explorer,NULL,NULL,NULL,NULL 1379,19,69,MacOS,NULL,NULL,NULL,NULL 1380,20,69,Urielle,NULL,NULL,NULL,NULL 1381,1,70,37.7147,10.3215,NULL,NULL,NULL 1382,2,70,Victoria Bitter,NULL,NULL,NULL,NULL 1383,3,70,One month,NULL,NULL,NULL,NULL 1384,4,70,39511622,NULL,NULL,NULL,NULL 1385,5,70,Female,NULL,NULL,NULL,NULL 1386,6,70,www.moodle.org,NULL,NULL,NULL,NULL 1387,7,70,Orlando,NULL,NULL,NULL,NULL 1388,8,70,sem molestie sodales. Mauris blandit enim consequat purus. Maecenas libero est congue a aliquet vel vulputate eu odio. Phasellus at augue id ante dictum cursus. Nunc mauris elit dictum eu,NULL,NULL,NULL,NULL 1389,9,70,NSW,NULL,NULL,NULL,NULL 1390,10,70,557 Magnis Av.,NULL,NULL,NULL,NULL 1391,11,70,Basketball,NULL,NULL,NULL,NULL 1392,12,70,NT,NULL,NULL,NULL,NULL 1393,13,70,(09) 1119 1376,NULL,NULL,NULL,NULL 1394,14,70,(04) 661 372,NULL,NULL,NULL,NULL 1395,15,70,Duis.cursus@example.com,NULL,NULL,NULL,NULL 1396,16,70,22500,NULL,NULL,NULL,NULL 1397,17,70,C243544,NULL,NULL,NULL,NULL 1398,18,70,Safari,NULL,NULL,NULL,NULL 1399,19,70,Free-BSD,NULL,NULL,NULL,NULL 1400,20,70,Candice,NULL,NULL,NULL,NULL 1401,1,71,56.1655,94.7228,NULL,NULL,NULL 1402,2,71,Asahi,NULL,NULL,NULL,NULL 1403,3,71,One month,NULL,NULL,NULL,NULL 1404,4,71,362447,NULL,NULL,NULL,NULL 1405,5,71,Female,NULL,NULL,NULL,NULL 1406,6,71,www.imdb.com,NULL,NULL,NULL,NULL 1407,7,71,Julian,NULL,NULL,NULL,NULL 1408,8,71,eu odio. Phasellus at augue id ante dictum cursus. Nunc mauris elit dictum eu eleifend nec malesuada ut sem. Nulla interdum. Curabitur dictum. Phasellus in felis. Nulla tempor augue ac ipsum. Phasellus vitae mauris sit amet lorem semper auctor. Mauris vel turpis. Aliquam adipiscing lobortis risus. In mi pede,NULL,NULL,NULL,NULL 1409,9,71,QLD,NULL,NULL,NULL,NULL 1410,10,71,Ap #503-3070 Ac Ave,NULL,NULL,NULL,NULL 1411,11,71,Football,NULL,NULL,NULL,NULL 1412,12,71,AB,NULL,NULL,NULL,NULL 1413,13,71,(05) 4619 16,NULL,NULL,NULL,NULL 1414,14,71,(07) 161 5502,NULL,NULL,NULL,NULL 1415,15,71,mauris@example.com,NULL,NULL,NULL,NULL 1416,16,71,65512,NULL,NULL,NULL,NULL 1417,17,71,F6535551,NULL,NULL,NULL,NULL 1418,18,71,Opera,NULL,NULL,NULL,NULL 1419,19,71,MacOS,NULL,NULL,NULL,NULL 1420,20,71,Basil,NULL,NULL,NULL,NULL 1421,1,72,53.5665,1.055,NULL,NULL,NULL 1422,2,72,Yebisu,NULL,NULL,NULL,NULL 1423,3,72,One month,NULL,NULL,NULL,NULL 1424,4,72,3372406,NULL,NULL,NULL,NULL 1425,5,72,Male,NULL,NULL,NULL,NULL 1426,6,72,www.imdb.com,NULL,NULL,NULL,NULL 1427,7,72,Kathleen,NULL,NULL,NULL,NULL 1428,8,72,magna a neque. Nullam ut nisi a odio semper cursus. Integer mollis. Integer tincidunt aliquam arcu. Aliquam ultrices iaculis odio. Nam interdum enim non nisi. Aenean eget metus. In nec orci. Donec nibh. Quisque,NULL,NULL,NULL,NULL 1429,9,72,NT,NULL,NULL,NULL,NULL 1430,10,72,469 Aliquam Rd.,NULL,NULL,NULL,NULL 1431,11,72,Squash,NULL,NULL,NULL,NULL 1432,12,72,QC,NULL,NULL,NULL,NULL 1433,13,72,(02) 6413 158,NULL,NULL,NULL,NULL 1434,14,72,(03) 1216 7096,NULL,NULL,NULL,NULL 1435,15,72,egestas.lacinia@example.com,NULL,NULL,NULL,NULL 1436,16,72,767604,NULL,NULL,NULL,NULL 1437,17,72,T1226169,NULL,NULL,NULL,NULL 1438,18,72,Safari,NULL,NULL,NULL,NULL 1439,19,72,MacOS,NULL,NULL,NULL,NULL 1440,20,72,Orlando,NULL,NULL,NULL,NULL 1441,1,73,62.755,29.1265,NULL,NULL,NULL 1442,2,73,Yebisu,NULL,NULL,NULL,NULL 1443,3,73,One month,NULL,NULL,NULL,NULL 1444,4,73,2106666,NULL,NULL,NULL,NULL 1445,5,73,Male,NULL,NULL,NULL,NULL 1446,6,73,www.google.com,NULL,NULL,NULL,NULL 1447,7,73,Isaac,NULL,NULL,NULL,NULL 1448,8,73,odio semper cursus. Integer mollis. Integer tincidunt aliquam arcu. Aliquam ultrices iaculis odio. Nam interdum enim non nisi. Aenean eget metus. In nec orci. Donec nibh. Quisque nonummy ipsum non arcu. Vivamus sit,NULL,NULL,NULL,NULL 1449,9,73,QLD,NULL,NULL,NULL,NULL 1450,10,73,354-411 Est St.,NULL,NULL,NULL,NULL 1451,11,73,Badminton,NULL,NULL,NULL,NULL 1452,12,73,NU,NULL,NULL,NULL,NULL 1453,13,73,(06) 7005 819,NULL,NULL,NULL,NULL 1454,14,73,(08) 9601 182,NULL,NULL,NULL,NULL 1455,15,73,eros@example.com,NULL,NULL,NULL,NULL 1456,16,73,106730,NULL,NULL,NULL,NULL 1457,17,73,L9607243,NULL,NULL,NULL,NULL 1458,18,73,Safari,NULL,NULL,NULL,NULL 1459,19,73,Windows,NULL,NULL,NULL,NULL 1460,20,73,Regan,NULL,NULL,NULL,NULL 1461,1,74,2.6319,17.7262,NULL,NULL,NULL 1462,2,74,Carlsberg,NULL,NULL,NULL,NULL 1463,3,74,One month,NULL,NULL,NULL,NULL 1464,4,74,75060725,NULL,NULL,NULL,NULL 1465,5,74,Female,NULL,NULL,NULL,NULL 1466,6,74,www.penny-arcade.com,NULL,NULL,NULL,NULL 1467,7,74,Zoe,NULL,NULL,NULL,NULL 1468,8,74,Mauris magna. Duis dignissim tempor arcu. Vestibulum ut eros non enim commodo hendrerit. Donec porttitor tellus non magna. Nam ligula elit pretium et rutrum non hendrerit id ante. Nunc mauris sapien cursus in hendrerit,NULL,NULL,NULL,NULL 1469,9,74,NSW,NULL,NULL,NULL,NULL 1470,10,74,98-6164 In Av.,NULL,NULL,NULL,NULL 1471,11,74,Badminton,NULL,NULL,NULL,NULL 1472,12,74,YT,NULL,NULL,NULL,NULL 1473,13,74,(01) 6069 7642,NULL,NULL,NULL,NULL 1474,14,74,(02) 4651 748,NULL,NULL,NULL,NULL 1475,15,74,magna.a.tortor@example.com,NULL,NULL,NULL,NULL 1476,16,74,703032,NULL,NULL,NULL,NULL 1477,17,74,X635263,NULL,NULL,NULL,NULL 1478,18,74,Chrome,NULL,NULL,NULL,NULL 1479,19,74,Linux,NULL,NULL,NULL,NULL 1480,20,74,Jack,NULL,NULL,NULL,NULL 1481,1,75,3.9657,37.9648,NULL,NULL,NULL 1482,2,75,Victoria Bitter,NULL,NULL,NULL,NULL 1483,3,75,two months,NULL,NULL,NULL,NULL 1484,4,75,3382453,NULL,NULL,NULL,NULL 1485,5,75,Male,NULL,NULL,NULL,NULL 1486,6,75,www.google.com,NULL,NULL,NULL,NULL 1487,7,75,Rogan,NULL,NULL,NULL,NULL 1488,8,75,felis purus ac tellus. Suspendisse sed dolor. Fusce mi lorem vehicula et rutrum eu ultrices sit amet risus. Donec nibh enim gravida sit amet,NULL,NULL,NULL,NULL 1489,9,75,ACT,NULL,NULL,NULL,NULL 1490,10,75,135-5374 Sodales Av.,NULL,NULL,NULL,NULL 1491,11,75,Soccer,NULL,NULL,NULL,NULL 1492,12,75,SK,NULL,NULL,NULL,NULL 1493,13,75,(08) 204 9644,NULL,NULL,NULL,NULL 1494,14,75,(08) 598 7315,NULL,NULL,NULL,NULL 1495,15,75,Integer.mollis.Integer@example.com,NULL,NULL,NULL,NULL 1496,16,75,6141,NULL,NULL,NULL,NULL 1497,17,75,W12775,NULL,NULL,NULL,NULL 1498,18,75,Firefox,NULL,NULL,NULL,NULL 1499,19,75,MacOS,NULL,NULL,NULL,NULL 1500,20,75,Megan,NULL,NULL,NULL,NULL 1501,1,76,99.0157,26.2151,NULL,NULL,NULL 1502,2,76,Carlsberg,NULL,NULL,NULL,NULL 1503,3,76,One month,NULL,NULL,NULL,NULL 1504,4,76,48266,NULL,NULL,NULL,NULL 1505,5,76,Female,NULL,NULL,NULL,NULL 1506,6,76,www.penny-arcade.com,NULL,NULL,NULL,NULL 1507,7,76,Ulysses,NULL,NULL,NULL,NULL 1508,8,76,mus. Aenean eget magna. Suspendisse tristique neque venenatis lacus. Etiam bibendum fermentum metus.,NULL,NULL,NULL,NULL 1509,9,76,NT,NULL,NULL,NULL,NULL 1510,10,76,Ap #916-1150 Tempus Street,NULL,NULL,NULL,NULL 1511,11,76,Badminton,NULL,NULL,NULL,NULL 1512,12,76,BC,NULL,NULL,NULL,NULL 1513,13,76,(07) 5168 6516,NULL,NULL,NULL,NULL 1514,14,76,(00) 3968 3225,NULL,NULL,NULL,NULL 1515,15,76,sapien.molestie.orci@example.com,NULL,NULL,NULL,NULL 1516,16,76,10197,NULL,NULL,NULL,NULL 1517,17,76,Q7427535,NULL,NULL,NULL,NULL 1518,18,76,Chrome,NULL,NULL,NULL,NULL 1519,19,76,Linux,NULL,NULL,NULL,NULL 1520,20,76,Ferris,NULL,NULL,NULL,NULL 1521,1,77,95.5214,58.2411,NULL,NULL,NULL 1522,2,77,Carlsberg,NULL,NULL,NULL,NULL 1523,3,77,One month,NULL,NULL,NULL,NULL 1524,4,77,50623943,NULL,NULL,NULL,NULL 1525,5,77,Female,NULL,NULL,NULL,NULL 1526,6,77,www.google.com,NULL,NULL,NULL,NULL 1527,7,77,Ryder,NULL,NULL,NULL,NULL 1528,8,77,gravida sagittis. Duis gravida. Praesent eu nulla at sem molestie sodales. Mauris,NULL,NULL,NULL,NULL 1529,9,77,NSW,NULL,NULL,NULL,NULL 1530,10,77,6322 Ut Ave,NULL,NULL,NULL,NULL 1531,11,77,Squash,NULL,NULL,NULL,NULL 1532,12,77,NS,NULL,NULL,NULL,NULL 1533,13,77,(09) 3732 107,NULL,NULL,NULL,NULL 1534,14,77,(00) 727 2016,NULL,NULL,NULL,NULL 1535,15,77,eu.erat.semper@example.com,NULL,NULL,NULL,NULL 1536,16,77,614308,NULL,NULL,NULL,NULL 1537,17,77,D2163462,NULL,NULL,NULL,NULL 1538,18,77,Other,NULL,NULL,NULL,NULL 1539,19,77,Linux,NULL,NULL,NULL,NULL 1540,20,77,Iona,NULL,NULL,NULL,NULL 1541,1,78,11.4271,39.9761,NULL,NULL,NULL 1542,2,78,Asahi,NULL,NULL,NULL,NULL 1543,3,78,One month,NULL,NULL,NULL,NULL 1544,4,78,12344526,NULL,NULL,NULL,NULL 1545,5,78,Male,NULL,NULL,NULL,NULL 1546,6,78,www.imdb.com,NULL,NULL,NULL,NULL 1547,7,78,Lois,NULL,NULL,NULL,NULL 1548,8,78,Donec at arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec tincidunt. Donec vitae erat vel pede blandit congue. In,NULL,NULL,NULL,NULL 1549,9,78,NT,NULL,NULL,NULL,NULL 1550,10,78,Ap #122-3703 Nisl St.,NULL,NULL,NULL,NULL 1551,11,78,Squash,NULL,NULL,NULL,NULL 1552,12,78,SK,NULL,NULL,NULL,NULL 1553,13,78,(00) 9672 1666,NULL,NULL,NULL,NULL 1554,14,78,(09) 1229 7628,NULL,NULL,NULL,NULL 1555,15,78,eros.nec@example.com,NULL,NULL,NULL,NULL 1556,16,78,61485,NULL,NULL,NULL,NULL 1557,17,78,B133121,NULL,NULL,NULL,NULL 1558,18,78,Other,NULL,NULL,NULL,NULL 1559,19,78,Red Hat,NULL,NULL,NULL,NULL 1560,20,78,Kamal,NULL,NULL,NULL,NULL 1561,1,79,57.195,18.748,NULL,NULL,NULL 1562,2,79,Asahi,NULL,NULL,NULL,NULL 1563,3,79,two months,NULL,NULL,NULL,NULL 1564,4,79,65026321,NULL,NULL,NULL,NULL 1565,5,79,Male,NULL,NULL,NULL,NULL 1566,6,79,www.moodle.org,NULL,NULL,NULL,NULL 1567,7,79,Cassady,NULL,NULL,NULL,NULL 1568,8,79,fermentum convallis ligula. Donec luctus aliquet odio. Etiam ligula tortor dictum eu placerat eget venenatis a magna. Lorem ipsum dolor sit amet consectetuer adipiscing elit. Etiam laoreet libero et tristique pellentesque tellus sem mollis dui in sodales elit,NULL,NULL,NULL,NULL 1569,9,79,ACT,NULL,NULL,NULL,NULL 1570,10,79,3156 A Ave,NULL,NULL,NULL,NULL 1571,11,79,Squash,NULL,NULL,NULL,NULL 1572,12,79,AB,NULL,NULL,NULL,NULL 1573,13,79,(09) 4254 5062,NULL,NULL,NULL,NULL 1574,14,79,(08) 5012 9496,NULL,NULL,NULL,NULL 1575,15,79,Cum@example.com,NULL,NULL,NULL,NULL 1576,16,79,227125,NULL,NULL,NULL,NULL 1577,17,79,N1043713,NULL,NULL,NULL,NULL 1578,18,79,Other,NULL,NULL,NULL,NULL 1579,19,79,Ubuntu,NULL,NULL,NULL,NULL 1580,20,79,Linus,NULL,NULL,NULL,NULL 1581,1,80,5.012,48.6914,NULL,NULL,NULL 1582,2,80,Yebisu,NULL,NULL,NULL,NULL 1583,3,80,One month,NULL,NULL,NULL,NULL 1584,4,80,2444637,NULL,NULL,NULL,NULL 1585,5,80,Male,NULL,NULL,NULL,NULL 1586,6,80,www.moodle.org,NULL,NULL,NULL,NULL 1587,7,80,Erica,NULL,NULL,NULL,NULL 1588,8,80,Sed diam lorem auctor quis tristique ac eleifend vitae erat. Vivamus nisi. Mauris nulla. Integer urna. Vivamus molestie,NULL,NULL,NULL,NULL 1589,9,80,SA,NULL,NULL,NULL,NULL 1590,10,80,P.O. Box 721 9449 Pede. Ave,NULL,NULL,NULL,NULL 1591,11,80,Basketball,NULL,NULL,NULL,NULL 1592,12,80,NB,NULL,NULL,NULL,NULL 1593,13,80,(07) 1942 4757,NULL,NULL,NULL,NULL 1594,14,80,(03) 810 6133,NULL,NULL,NULL,NULL 1595,15,80,libero@example.com,NULL,NULL,NULL,NULL 1596,16,80,76568,NULL,NULL,NULL,NULL 1597,17,80,D05337,NULL,NULL,NULL,NULL 1598,18,80,Chrome,NULL,NULL,NULL,NULL 1599,19,80,Windows,NULL,NULL,NULL,NULL 1600,20,80,Hilel,NULL,NULL,NULL,NULL 1601,1,81,3.5377,39.411,NULL,NULL,NULL 1602,2,81,Asahi,NULL,NULL,NULL,NULL 1603,3,81,two months,NULL,NULL,NULL,NULL 1604,4,81,1371611,NULL,NULL,NULL,NULL 1605,5,81,Female,NULL,NULL,NULL,NULL 1606,6,81,www.google.com,NULL,NULL,NULL,NULL 1607,7,81,Cain,NULL,NULL,NULL,NULL 1608,8,81,Nunc mauris elit dictum eu eleifend nec malesuada ut sem. Nulla interdum. Curabitur dictum. Phasellus in felis. Nulla tempor augue ac ipsum. Phasellus vitae mauris sit amet lorem semper auctor. Mauris vel turpis. Aliquam adipiscing lobortis risus. In,NULL,NULL,NULL,NULL 1609,9,81,QLD,NULL,NULL,NULL,NULL 1610,10,81,5614 Non Ave,NULL,NULL,NULL,NULL 1611,11,81,Tennis,NULL,NULL,NULL,NULL 1612,12,81,ON,NULL,NULL,NULL,NULL 1613,13,81,(02) 367 6616,NULL,NULL,NULL,NULL 1614,14,81,(07) 250 4742,NULL,NULL,NULL,NULL 1615,15,81,dictum.placerat@example.com,NULL,NULL,NULL,NULL 1616,16,81,405070,NULL,NULL,NULL,NULL 1617,17,81,R3603335,NULL,NULL,NULL,NULL 1618,18,81,Opera,NULL,NULL,NULL,NULL 1619,19,81,Net-BSD,NULL,NULL,NULL,NULL 1620,20,81,Colton,NULL,NULL,NULL,NULL 1621,1,82,67.1494,47.2719,NULL,NULL,NULL 1622,2,82,Hahn Premium,NULL,NULL,NULL,NULL 1623,3,82,two months,NULL,NULL,NULL,NULL 1624,4,82,4635210,NULL,NULL,NULL,NULL 1625,5,82,Female,NULL,NULL,NULL,NULL 1626,6,82,www.imdb.com,NULL,NULL,NULL,NULL 1627,7,82,Courtney,NULL,NULL,NULL,NULL 1628,8,82,sem vitae aliquam eros turpis non enim. Mauris quis turpis vitae purus gravida sagittis. Duis gravida. Praesent eu nulla at sem molestie sodales. Mauris blandit enim consequat purus. Maecenas libero est congue a aliquet vel vulputate eu odio. Phasellus at augue id ante dictum cursus. Nunc mauris elit,NULL,NULL,NULL,NULL 1629,9,82,SA,NULL,NULL,NULL,NULL 1630,10,82,427-397 Quisque Avenue,NULL,NULL,NULL,NULL 1631,11,82,Squash,NULL,NULL,NULL,NULL 1632,12,82,ON,NULL,NULL,NULL,NULL 1633,13,82,(02) 1359 617,NULL,NULL,NULL,NULL 1634,14,82,(08) 2548 6725,NULL,NULL,NULL,NULL 1635,15,82,facilisis@example.com,NULL,NULL,NULL,NULL 1636,16,82,667117,NULL,NULL,NULL,NULL 1637,17,82,W0260916,NULL,NULL,NULL,NULL 1638,18,82,Internet Explorer,NULL,NULL,NULL,NULL 1639,19,82,Net-BSD,NULL,NULL,NULL,NULL 1640,20,82,Simone,NULL,NULL,NULL,NULL 1641,1,83,10.6029,62.0412,NULL,NULL,NULL 1642,2,83,Asahi,NULL,NULL,NULL,NULL 1643,3,83,two months,NULL,NULL,NULL,NULL 1644,4,83,51644,NULL,NULL,NULL,NULL 1645,5,83,Male,NULL,NULL,NULL,NULL 1646,6,83,www.imdb.com,NULL,NULL,NULL,NULL 1647,7,83,Yolanda,NULL,NULL,NULL,NULL 1648,8,83,Fusce mollis. Duis sit amet diam eu dolor egestas rhoncus. Proin nisl sem consequat nec mollis vitae posuere at velit. Cras lorem lorem luctus ut pellentesque eget dictum placerat augue. Sed molestie. Sed id risus,NULL,NULL,NULL,NULL 1649,9,83,WA,NULL,NULL,NULL,NULL 1650,10,83,Ap #409-645 Mollis. St.,NULL,NULL,NULL,NULL 1651,11,83,Tennis,NULL,NULL,NULL,NULL 1652,12,83,ON,NULL,NULL,NULL,NULL 1653,13,83,(07) 3670 4765,NULL,NULL,NULL,NULL 1654,14,83,(05) 531 218,NULL,NULL,NULL,NULL 1655,15,83,eu.nibh.vulputate@example.com,NULL,NULL,NULL,NULL 1656,16,83,140305,NULL,NULL,NULL,NULL 1657,17,83,Z612249,NULL,NULL,NULL,NULL 1658,18,83,Chrome,NULL,NULL,NULL,NULL 1659,19,83,Windows,NULL,NULL,NULL,NULL 1660,20,83,Dora,NULL,NULL,NULL,NULL 1661,1,84,66.2749,33.0449,NULL,NULL,NULL 1662,2,84,Hahn Premium,NULL,NULL,NULL,NULL 1663,3,84,two months,NULL,NULL,NULL,NULL 1664,4,84,47746502,NULL,NULL,NULL,NULL 1665,5,84,Female,NULL,NULL,NULL,NULL 1666,6,84,www.penny-arcade.com,NULL,NULL,NULL,NULL 1667,7,84,Iola,NULL,NULL,NULL,NULL 1668,8,84,varius orci in consequat enim diam vel arcu. Curabitur ut odio vel est tempor bibendum. Donec felis orci adipiscing non luctus sit amet faucibus ut,NULL,NULL,NULL,NULL 1669,9,84,SA,NULL,NULL,NULL,NULL 1670,10,84,1625 Fringilla Avenue,NULL,NULL,NULL,NULL 1671,11,84,Badminton,NULL,NULL,NULL,NULL 1672,12,84,NU,NULL,NULL,NULL,NULL 1673,13,84,(01) 9543 7471,NULL,NULL,NULL,NULL 1674,14,84,(08) 524 3959,NULL,NULL,NULL,NULL 1675,15,84,et.magnis.dis@example.com,NULL,NULL,NULL,NULL 1676,16,84,645120,NULL,NULL,NULL,NULL 1677,17,84,M5523134,NULL,NULL,NULL,NULL 1678,18,84,Other,NULL,NULL,NULL,NULL 1679,19,84,MacOS,NULL,NULL,NULL,NULL 1680,20,84,Janna,NULL,NULL,NULL,NULL 1681,1,85,31.4132,41.1573,NULL,NULL,NULL 1682,2,85,Carlsberg,NULL,NULL,NULL,NULL 1683,3,85,two months,NULL,NULL,NULL,NULL 1684,4,85,12069159,NULL,NULL,NULL,NULL 1685,5,85,Male,NULL,NULL,NULL,NULL 1686,6,85,www.google.com,NULL,NULL,NULL,NULL 1687,7,85,Vaughan,NULL,NULL,NULL,NULL 1688,8,85,nulla. Integer vulputate risus a ultricies adipiscing enim mi tempor lorem eget mollis lectus pede et risus. Quisque libero lacus varius et euismod et commodo at libero. Morbi accumsan laoreet ipsum.,NULL,NULL,NULL,NULL 1689,9,85,NSW,NULL,NULL,NULL,NULL 1690,10,85,577-2305 Risus Avenue,NULL,NULL,NULL,NULL 1691,11,85,Water polo,NULL,NULL,NULL,NULL 1692,12,85,BC,NULL,NULL,NULL,NULL 1693,13,85,(02) 263 2760,NULL,NULL,NULL,NULL 1694,14,85,(08) 144 7256,NULL,NULL,NULL,NULL 1695,15,85,enim.Nunc@example.com,NULL,NULL,NULL,NULL 1696,16,85,14313,NULL,NULL,NULL,NULL 1697,17,85,L2520723,NULL,NULL,NULL,NULL 1698,18,85,Chrome,NULL,NULL,NULL,NULL 1699,19,85,Free-BSD,NULL,NULL,NULL,NULL 1700,20,85,Mohammad,NULL,NULL,NULL,NULL 1701,1,86,60.1475,26.1373,NULL,NULL,NULL 1702,2,86,Carlsberg,NULL,NULL,NULL,NULL 1703,3,86,One month,NULL,NULL,NULL,NULL 1704,4,86,14400496,NULL,NULL,NULL,NULL 1705,5,86,Male,NULL,NULL,NULL,NULL 1706,6,86,www.google.com,NULL,NULL,NULL,NULL 1707,7,86,Ruth,NULL,NULL,NULL,NULL 1708,8,86,porta elit a feugiat tellus lorem eu metus. In lorem. Donec elementum lorem ut aliquam iaculis lacus pede sagittis augue eu tempor erat neque non quam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aliquam fringilla cursus purus. Nullam scelerisque,NULL,NULL,NULL,NULL 1709,9,86,ACT,NULL,NULL,NULL,NULL 1710,10,86,Ap #261-528 Bibendum Street,NULL,NULL,NULL,NULL 1711,11,86,Running,NULL,NULL,NULL,NULL 1712,12,86,PE,NULL,NULL,NULL,NULL 1713,13,86,(08) 6464 4531,NULL,NULL,NULL,NULL 1714,14,86,(09) 4810 2314,NULL,NULL,NULL,NULL 1715,15,86,eget@example.com,NULL,NULL,NULL,NULL 1716,16,86,146695,NULL,NULL,NULL,NULL 1717,17,86,D715664,NULL,NULL,NULL,NULL 1718,18,86,Chrome,NULL,NULL,NULL,NULL 1719,19,86,Net-BSD,NULL,NULL,NULL,NULL 1720,20,86,Geoffrey,NULL,NULL,NULL,NULL 1721,1,87,33.649,4.218,NULL,NULL,NULL 1722,2,87,Carlsberg,NULL,NULL,NULL,NULL 1723,3,87,two months,NULL,NULL,NULL,NULL 1724,4,87,74106329,NULL,NULL,NULL,NULL 1725,5,87,Male,NULL,NULL,NULL,NULL 1726,6,87,www.google.com,NULL,NULL,NULL,NULL 1727,7,87,Porter,NULL,NULL,NULL,NULL 1728,8,87,gravida sit amet dapibus id blandit at nisi. Cum sociis natoque penatibus et magnis dis parturient montes nascetur ridiculus mus. Proin vel nisl. Quisque fringilla euismod enim. Etiam gravida molestie arcu. Sed eu nibh,NULL,NULL,NULL,NULL 1729,9,87,NSW,NULL,NULL,NULL,NULL 1730,10,87,Ap #299-7748 Nullam St.,NULL,NULL,NULL,NULL 1731,11,87,Soccer,NULL,NULL,NULL,NULL 1732,12,87,PE,NULL,NULL,NULL,NULL 1733,13,87,(01) 5199 1755,NULL,NULL,NULL,NULL 1734,14,87,(07) 4644 154,NULL,NULL,NULL,NULL 1735,15,87,Phasellus@example.com,NULL,NULL,NULL,NULL 1736,16,87,44947,NULL,NULL,NULL,NULL 1737,17,87,T149520,NULL,NULL,NULL,NULL 1738,18,87,Opera,NULL,NULL,NULL,NULL 1739,19,87,MacOS,NULL,NULL,NULL,NULL 1740,20,87,Whitney,NULL,NULL,NULL,NULL 1741,1,88,48.4295,28.9737,NULL,NULL,NULL 1742,2,88,Yebisu,NULL,NULL,NULL,NULL 1743,3,88,One month,NULL,NULL,NULL,NULL 1744,4,88,6662035,NULL,NULL,NULL,NULL 1745,5,88,Male,NULL,NULL,NULL,NULL 1746,6,88,www.imdb.com,NULL,NULL,NULL,NULL 1747,7,88,McKenzie,NULL,NULL,NULL,NULL 1748,8,88,Aenean egestas hendrerit neque. In ornare sagittis felis. Donec tempor est ac mattis semper dui lectus rutrum urna nec luctus felis purus ac tellus. Suspendisse sed dolor. Fusce mi lorem vehicula et rutrum eu ultrices sit amet risus. Donec nibh,NULL,NULL,NULL,NULL 1749,9,88,VIC,NULL,NULL,NULL,NULL 1750,10,88,4097 Neque Road,NULL,NULL,NULL,NULL 1751,11,88,Tennis,NULL,NULL,NULL,NULL 1752,12,88,PE,NULL,NULL,NULL,NULL 1753,13,88,(01) 403 3670,NULL,NULL,NULL,NULL 1754,14,88,(01) 940 5058,NULL,NULL,NULL,NULL 1755,15,88,pede.Cum@example.com,NULL,NULL,NULL,NULL 1756,16,88,640356,NULL,NULL,NULL,NULL 1757,17,88,I121518,NULL,NULL,NULL,NULL 1758,18,88,Firefox,NULL,NULL,NULL,NULL 1759,19,88,Linux,NULL,NULL,NULL,NULL 1760,20,88,Myles,NULL,NULL,NULL,NULL 1761,1,89,7.7562,99.735,NULL,NULL,NULL 1762,2,89,Hahn Premium,NULL,NULL,NULL,NULL 1763,3,89,One month,NULL,NULL,NULL,NULL 1764,4,89,26613531,NULL,NULL,NULL,NULL 1765,5,89,Female,NULL,NULL,NULL,NULL 1766,6,89,www.imdb.com,NULL,NULL,NULL,NULL 1767,7,89,Jaquelyn,NULL,NULL,NULL,NULL 1768,8,89,mollis. Duis sit amet diam eu dolor egestas rhoncus. Proin nisl sem consequat nec mollis vitae posuere at velit. Cras lorem lorem luctus ut pellentesque eget dictum placerat augue. Sed molestie. Sed id risus quis diam luctus lobortis. Class aptent taciti sociosqu,NULL,NULL,NULL,NULL 1769,9,89,WA,NULL,NULL,NULL,NULL 1770,10,89,P.O. Box 973 5222 Quisque Street,NULL,NULL,NULL,NULL 1771,11,89,Running,NULL,NULL,NULL,NULL 1772,12,89,NL,NULL,NULL,NULL,NULL 1773,13,89,(00) 5218 1562,NULL,NULL,NULL,NULL 1774,14,89,(01) 373 6368,NULL,NULL,NULL,NULL 1775,15,89,lorem.fringilla@example.com,NULL,NULL,NULL,NULL 1776,16,89,603941,NULL,NULL,NULL,NULL 1777,17,89,D134124,NULL,NULL,NULL,NULL 1778,18,89,Internet Explorer,NULL,NULL,NULL,NULL 1779,19,89,Free-BSD,NULL,NULL,NULL,NULL 1780,20,89,Alexandra,NULL,NULL,NULL,NULL 1781,1,90,44.3029,75.6318,NULL,NULL,NULL 1782,2,90,Victoria Bitter,NULL,NULL,NULL,NULL 1783,3,90,two months,NULL,NULL,NULL,NULL 1784,4,90,1916505,NULL,NULL,NULL,NULL 1785,5,90,Female,NULL,NULL,NULL,NULL 1786,6,90,www.google.com,NULL,NULL,NULL,NULL 1787,7,90,Freya,NULL,NULL,NULL,NULL 1788,8,90,pulvinar arcu et pede. Nunc sed orci lobortis augue scelerisque mollis. Phasellus,NULL,NULL,NULL,NULL 1789,9,90,WA,NULL,NULL,NULL,NULL 1790,10,90,4561 In Ave,NULL,NULL,NULL,NULL 1791,11,90,Tennis,NULL,NULL,NULL,NULL 1792,12,90,MB,NULL,NULL,NULL,NULL 1793,13,90,(04) 414 7757,NULL,NULL,NULL,NULL 1794,14,90,(07) 9614 4435,NULL,NULL,NULL,NULL 1795,15,90,dignissim@example.com,NULL,NULL,NULL,NULL 1796,16,90,155718,NULL,NULL,NULL,NULL 1797,17,90,H224752,NULL,NULL,NULL,NULL 1798,18,90,Chrome,NULL,NULL,NULL,NULL 1799,19,90,Windows,NULL,NULL,NULL,NULL 1800,20,90,Ori,NULL,NULL,NULL,NULL 1801,1,91,15.227,68.326,NULL,NULL,NULL 1802,2,91,Carlsberg,NULL,NULL,NULL,NULL 1803,3,91,One month,NULL,NULL,NULL,NULL 1804,4,91,70997304,NULL,NULL,NULL,NULL 1805,5,91,Female,NULL,NULL,NULL,NULL 1806,6,91,www.penny-arcade.com,NULL,NULL,NULL,NULL 1807,7,91,Guy,NULL,NULL,NULL,NULL 1808,8,91,pede. Suspendisse dui. Fusce diam nunc ullamcorper eu euismod ac fermentum vel mauris. Integer sem elit pharetra ut pharetra sed hendrerit a arcu. Sed et libero. Proin mi. Aliquam gravida mauris ut mi. Duis risus odio auctor vitae aliquet nec imperdiet nec leo. Morbi,NULL,NULL,NULL,NULL 1809,9,91,VIC,NULL,NULL,NULL,NULL 1810,10,91,116-9642 Amet Rd.,NULL,NULL,NULL,NULL 1811,11,91,Badminton,NULL,NULL,NULL,NULL 1812,12,91,BC,NULL,NULL,NULL,NULL 1813,13,91,(05) 121 7306,NULL,NULL,NULL,NULL 1814,14,91,(01) 1240 6621,NULL,NULL,NULL,NULL 1815,15,91,molestie@example.com,NULL,NULL,NULL,NULL 1816,16,91,465758,NULL,NULL,NULL,NULL 1817,17,91,B61133,NULL,NULL,NULL,NULL 1818,18,91,Internet Explorer,NULL,NULL,NULL,NULL 1819,19,91,Windows,NULL,NULL,NULL,NULL 1820,20,91,Brittany,NULL,NULL,NULL,NULL 1821,1,92,60.128,12.0261,NULL,NULL,NULL 1822,2,92,Asahi,NULL,NULL,NULL,NULL 1823,3,92,One month,NULL,NULL,NULL,NULL 1824,4,92,4047262,NULL,NULL,NULL,NULL 1825,5,92,Female,NULL,NULL,NULL,NULL 1826,6,92,www.penny-arcade.com,NULL,NULL,NULL,NULL 1827,7,92,Hashim,NULL,NULL,NULL,NULL 1828,8,92,Donec feugiat metus sit amet ante. Vivamus non lorem vitae odio sagittis semper. Nam tempor diam dictum sapien. Aenean massa. Integer vitae nibh. Donec est mauris rhoncus id mollis nec cursus a enim. Suspendisse aliquet sem ut cursus luctus ipsum leo elementum sem vitae,NULL,NULL,NULL,NULL 1829,9,92,SA,NULL,NULL,NULL,NULL 1830,10,92,P.O. Box 125 531 Natoque Ave,NULL,NULL,NULL,NULL 1831,11,92,Badminton,NULL,NULL,NULL,NULL 1832,12,92,QC,NULL,NULL,NULL,NULL 1833,13,92,(05) 273 455,NULL,NULL,NULL,NULL 1834,14,92,(07) 3428 4450,NULL,NULL,NULL,NULL 1835,15,92,tempor.bibendum@example.com,NULL,NULL,NULL,NULL 1836,16,92,37943,NULL,NULL,NULL,NULL 1837,17,92,G41770,NULL,NULL,NULL,NULL 1838,18,92,Internet Explorer,NULL,NULL,NULL,NULL 1839,19,92,Ubuntu,NULL,NULL,NULL,NULL 1840,20,92,Duncan,NULL,NULL,NULL,NULL 1841,1,93,71.7322,2.1622,NULL,NULL,NULL 1842,2,93,Asahi,NULL,NULL,NULL,NULL 1843,3,93,two months,NULL,NULL,NULL,NULL 1844,4,93,6511942,NULL,NULL,NULL,NULL 1845,5,93,Female,NULL,NULL,NULL,NULL 1846,6,93,www.imdb.com,NULL,NULL,NULL,NULL 1847,7,93,Leilani,NULL,NULL,NULL,NULL 1848,8,93,tincidunt pede ac urna. Ut tincidunt vehicula risus. Nulla eget metus eu erat semper rutrum. Fusce dolor quam elementum at egestas a scelerisque sed sapien. Nunc pulvinar arcu et pede. Nunc sed orci lobortis augue scelerisque mollis. Phasellus libero mauris aliquam eu accumsan,NULL,NULL,NULL,NULL 1849,9,93,NSW,NULL,NULL,NULL,NULL 1850,10,93,P.O. Box 467 2996 Gravida St.,NULL,NULL,NULL,NULL 1851,11,93,Running,NULL,NULL,NULL,NULL 1852,12,93,ON,NULL,NULL,NULL,NULL 1853,13,93,(01) 3632 4134,NULL,NULL,NULL,NULL 1854,14,93,(00) 3402 108,NULL,NULL,NULL,NULL 1855,15,93,Aliquam.fringilla.cursus@example.com,NULL,NULL,NULL,NULL 1856,16,93,41053,NULL,NULL,NULL,NULL 1857,17,93,Y296637,NULL,NULL,NULL,NULL 1858,18,93,Chrome,NULL,NULL,NULL,NULL 1859,19,93,Free-BSD,NULL,NULL,NULL,NULL 1860,20,93,Cailin,NULL,NULL,NULL,NULL 1861,1,94,4.0399,10.7356,NULL,NULL,NULL 1862,2,94,Yebisu,NULL,NULL,NULL,NULL 1863,3,94,two months,NULL,NULL,NULL,NULL 1864,4,94,9810598,NULL,NULL,NULL,NULL 1865,5,94,Female,NULL,NULL,NULL,NULL 1866,6,94,www.moodle.org,NULL,NULL,NULL,NULL 1867,7,94,Conan,NULL,NULL,NULL,NULL 1868,8,94,vehicula aliquet libero. Integer in magna. Phasellus dolor elit pellentesque a facilisis non bibendum sed est. Nunc laoreet lectus quis massa. Mauris vestibulum neque,NULL,NULL,NULL,NULL 1869,9,94,NT,NULL,NULL,NULL,NULL 1870,10,94,514-5766 Magna. Rd.,NULL,NULL,NULL,NULL 1871,11,94,Tennis,NULL,NULL,NULL,NULL 1872,12,94,NU,NULL,NULL,NULL,NULL 1873,13,94,(07) 6975 4097,NULL,NULL,NULL,NULL 1874,14,94,(05) 1255 814,NULL,NULL,NULL,NULL 1875,15,94,luctus.et.ultrices@example.com,NULL,NULL,NULL,NULL 1876,16,94,41527,NULL,NULL,NULL,NULL 1877,17,94,A52469,NULL,NULL,NULL,NULL 1878,18,94,Other,NULL,NULL,NULL,NULL 1879,19,94,MacOS,NULL,NULL,NULL,NULL 1880,20,94,Sybil,NULL,NULL,NULL,NULL 1881,1,95,66.442,51.4955,NULL,NULL,NULL 1882,2,95,Victoria Bitter,NULL,NULL,NULL,NULL 1883,3,95,One month,NULL,NULL,NULL,NULL 1884,4,95,3146041,NULL,NULL,NULL,NULL 1885,5,95,Male,NULL,NULL,NULL,NULL 1886,6,95,www.moodle.org,NULL,NULL,NULL,NULL 1887,7,95,Colby,NULL,NULL,NULL,NULL 1888,8,95,erat. Sed nunc est mollis non cursus non egestas a dui. Cras pellentesque. Sed dictum. Proin eget odio. Aliquam vulputate ullamcorper magna. Sed eu eros. Nam consequat dolor vitae dolor. Donec fringilla. Donec feugiat metus sit amet ante. Vivamus non lorem vitae odio,NULL,NULL,NULL,NULL 1889,9,95,TAS,NULL,NULL,NULL,NULL 1890,10,95,Ap #69-1616 Donec Ave,NULL,NULL,NULL,NULL 1891,11,95,Soccer,NULL,NULL,NULL,NULL 1892,12,95,NL,NULL,NULL,NULL,NULL 1893,13,95,(01) 2732 1501,NULL,NULL,NULL,NULL 1894,14,95,(06) 457 145,NULL,NULL,NULL,NULL 1895,15,95,fringilla@example.com,NULL,NULL,NULL,NULL 1896,16,95,13315,NULL,NULL,NULL,NULL 1897,17,95,L512400,NULL,NULL,NULL,NULL 1898,18,95,Internet Explorer,NULL,NULL,NULL,NULL 1899,19,95,MacOS,NULL,NULL,NULL,NULL 1900,20,95,Giselle,NULL,NULL,NULL,NULL 1901,1,96,34.0415,44.6331,NULL,NULL,NULL 1902,2,96,Carlsberg,NULL,NULL,NULL,NULL 1903,3,96,two months,NULL,NULL,NULL,NULL 1904,4,96,26066722,NULL,NULL,NULL,NULL 1905,5,96,Male,NULL,NULL,NULL,NULL 1906,6,96,www.moodle.org,NULL,NULL,NULL,NULL 1907,7,96,Colt,NULL,NULL,NULL,NULL 1908,8,96,luctus vulputate nisi sem semper erat in consectetuer ipsum nunc id enim. Curabitur massa. Vestibulum accumsan neque et nunc. Quisque ornare tortor at risus. Nunc ac sem ut dolor dapibus gravida. Aliquam tincidunt nunc ac mattis ornare lectus ante dictum mi ac mattis velit justo nec ante.,NULL,NULL,NULL,NULL 1909,9,96,SA,NULL,NULL,NULL,NULL 1910,10,96,Ap #158-656 Integer Av.,NULL,NULL,NULL,NULL 1911,11,96,Basketball,NULL,NULL,NULL,NULL 1912,12,96,SK,NULL,NULL,NULL,NULL 1913,13,96,(04) 1700 597,NULL,NULL,NULL,NULL 1914,14,96,(01) 148 9967,NULL,NULL,NULL,NULL 1915,15,96,Donec.tincidunt@example.com,NULL,NULL,NULL,NULL 1916,16,96,606004,NULL,NULL,NULL,NULL 1917,17,96,Y1311315,NULL,NULL,NULL,NULL 1918,18,96,Internet Explorer,NULL,NULL,NULL,NULL 1919,19,96,Net-BSD,NULL,NULL,NULL,NULL 1920,20,96,Signe,NULL,NULL,NULL,NULL 1921,1,97,6.1044,42.149,NULL,NULL,NULL 1922,2,97,Carlsberg,NULL,NULL,NULL,NULL 1923,3,97,One month,NULL,NULL,NULL,NULL 1924,4,97,3319714,NULL,NULL,NULL,NULL 1925,5,97,Female,NULL,NULL,NULL,NULL 1926,6,97,www.moodle.org,NULL,NULL,NULL,NULL 1927,7,97,Ignatius,NULL,NULL,NULL,NULL 1928,8,97,ut mi. Duis risus odio auctor vitae aliquet nec imperdiet nec leo. Morbi neque tellus imperdiet non vestibulum nec euismod in dolor. Fusce feugiat. Lorem ipsum dolor sit amet consectetuer adipiscing elit. Aliquam auctor velit eget laoreet posuere enim nisl elementum purus accumsan interdum libero,NULL,NULL,NULL,NULL 1929,9,97,VIC,NULL,NULL,NULL,NULL 1930,10,97,P.O. Box 141 1096 Bibendum Ave,NULL,NULL,NULL,NULL 1931,11,97,Water polo,NULL,NULL,NULL,NULL 1932,12,97,BC,NULL,NULL,NULL,NULL 1933,13,97,(09) 458 6346,NULL,NULL,NULL,NULL 1934,14,97,(02) 7657 5031,NULL,NULL,NULL,NULL 1935,15,97,nulla@example.com,NULL,NULL,NULL,NULL 1936,16,97,440698,NULL,NULL,NULL,NULL 1937,17,97,C712522,NULL,NULL,NULL,NULL 1938,18,97,Firefox,NULL,NULL,NULL,NULL 1939,19,97,MacOS,NULL,NULL,NULL,NULL 1940,20,97,Dana,NULL,NULL,NULL,NULL 1941,1,98,32.2734,6.1702,NULL,NULL,NULL 1942,2,98,Victoria Bitter,NULL,NULL,NULL,NULL 1943,3,98,two months,NULL,NULL,NULL,NULL 1944,4,98,7360343,NULL,NULL,NULL,NULL 1945,5,98,Female,NULL,NULL,NULL,NULL 1946,6,98,www.penny-arcade.com,NULL,NULL,NULL,NULL 1947,7,98,Jillian,NULL,NULL,NULL,NULL 1948,8,98,vel quam dignissim pharetra. Nam ac nulla. In tincidunt congue turpis. In condimentum. Donec at arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec tincidunt. Donec vitae erat vel pede blandit congue. In scelerisque scelerisque dui. Suspendisse ac metus vitae velit egestas,NULL,NULL,NULL,NULL 1949,9,98,NT,NULL,NULL,NULL,NULL 1950,10,98,P.O. Box 370 2946 Accumsan Av.,NULL,NULL,NULL,NULL 1951,11,98,Basketball,NULL,NULL,NULL,NULL 1952,12,98,MB,NULL,NULL,NULL,NULL 1953,13,98,(05) 974 2169,NULL,NULL,NULL,NULL 1954,14,98,(03) 4340 9998,NULL,NULL,NULL,NULL 1955,15,98,risus@example.com,NULL,NULL,NULL,NULL 1956,16,98,610312,NULL,NULL,NULL,NULL 1957,17,98,O2337502,NULL,NULL,NULL,NULL 1958,18,98,Chrome,NULL,NULL,NULL,NULL 1959,19,98,Free-BSD,NULL,NULL,NULL,NULL 1960,20,98,Lenore,NULL,NULL,NULL,NULL 1961,1,99,52.5407,71.2977,NULL,NULL,NULL 1962,2,99,Yebisu,NULL,NULL,NULL,NULL 1963,3,99,One month,NULL,NULL,NULL,NULL 1964,4,99,1574011,NULL,NULL,NULL,NULL 1965,5,99,Female,NULL,NULL,NULL,NULL 1966,6,99,www.moodle.org,NULL,NULL,NULL,NULL 1967,7,99,Philip,NULL,NULL,NULL,NULL 1968,8,99,ligula. Donec luctus aliquet odio. Etiam ligula tortor dictum eu placerat eget venenatis,NULL,NULL,NULL,NULL 1969,9,99,ACT,NULL,NULL,NULL,NULL 1970,10,99,71-7660 Scelerisque St.,NULL,NULL,NULL,NULL 1971,11,99,Soccer,NULL,NULL,NULL,NULL 1972,12,99,QC,NULL,NULL,NULL,NULL 1973,13,99,(03) 9528 6094,NULL,NULL,NULL,NULL 1974,14,99,(09) 1225 7137,NULL,NULL,NULL,NULL 1975,15,99,sagittis.semper@example.com,NULL,NULL,NULL,NULL 1976,16,99,575810,NULL,NULL,NULL,NULL 1977,17,99,Q596020,NULL,NULL,NULL,NULL 1978,18,99,Safari,NULL,NULL,NULL,NULL 1979,19,99,Net-BSD,NULL,NULL,NULL,NULL 1980,20,99,Leah,NULL,NULL,NULL,NULL 1981,1,100,58.162,15.375,NULL,NULL,NULL 1982,2,100,Asahi,NULL,NULL,NULL,NULL 1983,3,100,two months,NULL,NULL,NULL,NULL 1984,4,100,69716576,NULL,NULL,NULL,NULL 1985,5,100,Male,NULL,NULL,NULL,NULL 1986,6,100,www.imdb.com,NULL,NULL,NULL,NULL 1987,7,100,Vanna,NULL,NULL,NULL,NULL 1988,8,100,amet ornare lectus justo eu arcu. Morbi sit amet massa. Quisque porttitor eros nec tellus. Nunc lectus pede ultrices a auctor non feugiat nec diam. Duis mi enim condimentum eget volutpat ornare facilisis eget ipsum. Donec sollicitudin adipiscing ligula. Aenean gravida nunc sed pede. Cum,NULL,NULL,NULL,NULL 1989,9,100,SA,NULL,NULL,NULL,NULL 1990,10,100,P.O. Box 430 2114 Nisl. Avenue,NULL,NULL,NULL,NULL 1991,11,100,Tennis,NULL,NULL,NULL,NULL 1992,12,100,AB,NULL,NULL,NULL,NULL 1993,13,100,(04) 7451 4406,NULL,NULL,NULL,NULL 1994,14,100,(02) 7205 4223,NULL,NULL,NULL,NULL 1995,15,100,in.lobortis@example.com,NULL,NULL,NULL,NULL 1996,16,100,6704,NULL,NULL,NULL,NULL 1997,17,100,O5261644,NULL,NULL,NULL,NULL 1998,18,100,Chrome,NULL,NULL,NULL,NULL 1999,19,100,Ubuntu,NULL,NULL,NULL,NULL 2000,20,100,Sandra,NULL,NULL,NULL,NULL tests/fixtures/test_data_import_with_field_username.csv000064400000000344152265517740017752 0ustar00ID,Username,Param2,Username,"Email address" 1,otherusername1,"My first entry",student,student@moodle.org 2,otherusername2,"My second entry",student2,student@moodle.org 3,otherusername3,"My third entry",student,student@moodle.orgtests/fixtures/test_data_import_with_files_missing_file.zip000064400000001662152265517740020635 0ustar00PK 6dVfiles/UT wddux PKldV(]files/samplepicture.pngUT <dDdux  sb``p  $<{ H1;1,test_data_import_with_files_missing_file.csvUT dDdux tIIMLI)L.)-p us @r9zyP.TH PK 6dVAfiles/UTwdux PKldV(]@files/samplepicture.pngUT<dux PKMdV1W ->,test_data_import_with_files_missing_file.csvUTdux PKtests/fixtures/test_data_records.csv000064400000006563152265517740014015 0ustar00id,userid,groupid,dataid,timecreated,timemodified,approved 1,1,0,1,1234567891,1234567892,1 2,2,1,1,1234567891,1234567892,1 3,3,2,1,1234567891,1234567892,1 4,4,1,1,1234567891,1234567892,1 5,5,1,1,1234567891,1234567892,1 6,6,2,1,1234567891,1234567892,1 7,7,0,1,1234567891,1234567892,1 8,8,2,1,1234567891,1234567892,1 9,9,1,1,1234567891,1234567892,1 10,10,1,1,1234567891,1234567892,1 11,11,1,1,1234567891,1234567892,1 12,12,2,1,1234567891,1234567892,1 13,13,2,1,1234567891,1234567892,1 14,14,0,1,1234567891,1234567892,1 15,15,2,1,1234567891,1234567892,1 16,16,0,1,1234567891,1234567892,1 17,17,1,1,1234567891,1234567892,1 18,18,2,1,1234567891,1234567892,1 19,19,0,1,1234567891,1234567892,1 20,20,1,1,1234567891,1234567892,1 21,21,0,1,1234567891,1234567892,1 22,22,2,1,1234567891,1234567892,1 23,23,0,1,1234567891,1234567892,1 24,24,2,1,1234567891,1234567892,1 25,25,2,1,1234567891,1234567892,1 26,26,0,1,1234567891,1234567892,1 27,27,1,1,1234567891,1234567892,1 28,28,2,1,1234567891,1234567892,1 29,29,0,1,1234567891,1234567892,1 30,30,2,1,1234567891,1234567892,1 31,31,0,1,1234567891,1234567892,1 32,32,1,1,1234567891,1234567892,1 33,33,1,1,1234567891,1234567892,1 34,34,2,1,1234567891,1234567892,1 35,35,1,1,1234567891,1234567892,1 36,36,0,1,1234567891,1234567892,1 37,37,0,1,1234567891,1234567892,1 38,38,2,1,1234567891,1234567892,1 39,39,1,1,1234567891,1234567892,1 40,40,1,1,1234567891,1234567892,1 41,41,0,1,1234567891,1234567892,1 42,42,1,1,1234567891,1234567892,1 43,43,0,1,1234567891,1234567892,1 44,44,1,1,1234567891,1234567892,1 45,45,0,1,1234567891,1234567892,1 46,46,0,1,1234567891,1234567892,1 47,47,1,1,1234567891,1234567892,1 48,48,0,1,1234567891,1234567892,1 49,49,0,1,1234567891,1234567892,1 50,50,1,1,1234567891,1234567892,1 51,51,0,1,1234567891,1234567892,1 52,52,0,1,1234567891,1234567892,1 53,53,1,1,1234567891,1234567892,1 54,54,1,1,1234567891,1234567892,1 55,55,0,1,1234567891,1234567892,1 56,56,2,1,1234567891,1234567892,1 57,57,2,1,1234567891,1234567892,1 58,58,2,1,1234567891,1234567892,1 59,59,1,1,1234567891,1234567892,1 60,60,1,1,1234567891,1234567892,1 61,61,0,1,1234567891,1234567892,1 62,62,2,1,1234567891,1234567892,1 63,63,0,1,1234567891,1234567892,1 64,64,0,1,1234567891,1234567892,1 65,65,1,1,1234567891,1234567892,1 66,66,1,1,1234567891,1234567892,1 67,67,0,1,1234567891,1234567892,1 68,68,0,1,1234567891,1234567892,1 69,69,2,1,1234567891,1234567892,1 70,70,2,1,1234567891,1234567892,1 71,71,0,1,1234567891,1234567892,1 72,72,1,1,1234567891,1234567892,1 73,73,1,1,1234567891,1234567892,1 74,74,0,1,1234567891,1234567892,1 75,75,0,1,1234567891,1234567892,1 76,76,2,1,1234567891,1234567892,1 77,77,2,1,1234567891,1234567892,1 78,78,0,1,1234567891,1234567892,1 79,79,1,1,1234567891,1234567892,1 80,80,1,1,1234567891,1234567892,1 81,81,0,1,1234567891,1234567892,1 82,82,1,1,1234567891,1234567892,1 83,83,1,1,1234567891,1234567892,1 84,84,1,1,1234567891,1234567892,1 85,85,1,1,1234567891,1234567892,1 86,86,0,1,1234567891,1234567892,1 87,87,0,1,1234567891,1234567892,1 88,88,0,1,1234567891,1234567892,1 89,89,1,1,1234567891,1234567892,1 90,90,1,1,1234567891,1234567892,0 91,91,2,1,1234567891,1234567892,0 92,92,0,1,1234567891,1234567892,0 93,93,2,1,1234567891,1234567892,0 94,94,1,1,1234567891,1234567892,0 95,95,1,1,1234567891,1234567892,0 96,96,1,1,1234567891,1234567892,0 97,97,0,1,1234567891,1234567892,0 98,98,1,1,1234567891,1234567892,0 99,99,2,1,1234567891,1234567892,0 100,100,0,1,1234567891,1234567892,0 tests/fixtures/test_data_fields.csv000064400000001732152265517740013613 0ustar00id,dataid,type,name,description,param1,param2,param3,param4,param5 1,1,latlong,location,Your current location,,-1,,NULL,NULL 2,1,multimenu,beer,Which beer is good?,,,,NULL,NULL 3,1,checkbox,month,One month or Two,,,,NULL,NULL 4,1,number,tfn,Tax file number,,,,NULL,NULL 5,1,radiobutton,gender,Male or Female,,,,NULL,NULL 6,1,url,webpage,Your webpage,,,,NULL,NULL 7,1,text,firstname,Your first name,,,,NULL,NULL 8,1,textarea,about,About you,,60,35,1,NULL 9,1,menu,state,State,,,,NULL,NULL 10,1,text,address,address of the participant,,,,NULL,NULL 11,1,menu,Sport,Favourite sport,,,,NULL,NULL 12,1,text,suburb,suburb,,,,NULL,NULL 13,1,text,mobile,mobile number,,,,NULL,NULL 14,1,text,phone,phone number,,,,NULL,NULL 15,1,text,email,email address,,,,NULL,NULL 16,1,text,license,Driver's license,,,,NULL,NULL 17,1,text,passport,Passport number,,,,NULL,NULL 18,1,menu,browser,prefered browser,,,,NULL,NULL 19,1,menu,OS,Operating System,,,,NULL,NULL 20,1,text,nickname,Nick name,,,,NULL,NULL tests/fixtures/test_data_import_with_userdata.csv000064400000000200152265517740016567 0ustar00ID,Param2,Username,"Email address" 1,"My first entry",student,student@moodle.org 2,"My second entry",student2,student@moodle.orgtests/fixtures/test_data_import.csv000064400000000060152265517740013650 0ustar00ID,Param2 1,"My first entry" 2,"My second entry"tests/fixtures/behat_preset.zip000064400000007736152265517740013003 0ustar00PKk UwAQz listtemplate.htmln @Ȭe U$F`14_IP}hA;E tMT&Sւ3BW@Q?m>h@x+ t)Vnw[58ZxxLirT}^9$E Rytj9&R\0SYG!899 t&7E!W H//ׅ8"qҊ>d+cZQߚp pi(xb:k rYx=2m`y"(sEnc;3O,^j"Y 9))Q(+^18 ~(j{<(KRTt!2Ms{ŋg8!'T*\w\6PKk Uh singletemplate.html͎ Ȝ=yݞV{$Fq}gwCLmޢ Q_#U1>V;j=F}jܙтpDLLQhO3t?hiѮAR:`搣ehjPRR Ce#)cIʳSHaL:C8ZU"b~4-D!@ L.R+P({#fHh2 u}|CNJJ)?)ʅW6OJ5yRCRT@[&S4wRwCJjڰ-Rük4 /aPwTjVJ+׀PKk UR asearchtemplate.htmln nשgnUKT1yY6mV!;B; x<Ӹ`q 0ΖI5YR6.FCs4Ƒgs݂[D)_=CM&Ff`zY|9_mўELrfRZEz}!̙2f޻BHPfTtR@0PhuT(JF (t:DRo hiq:T '~1)rʩІ*rvi0HzREݗv I0XaiF V2z'HEM)m|WNE1^q(nRE7xO^Rr$HG )g>;G_gaa([D)3(ql17@SGbg閝g6<=?PKk UG addtemplate.htmlj0 @ϡ۵d2SAԉtq&`Tc!BPBx4DX5M1Ksqu0>Bcr:@|)i; Ԇt@lsWPtI~\ EkzDbK'$R~-_XGkY+ yc+bXWcuT;sy.yt\W+źRXu[,4^Oڰ kx:ym_<(^ýl=sb:6FQ4?L}W a} R~Z]&ǘ~PKk Us1] rsstemplate.html͖;o JVڎQ{1EpiP0 8qͤ1!y%q &i ONFc18kdR2sp@@]HW5Vehm\}P eMA`[dDJ^lPl{'SF砙bDӕ*/Eӵ |A8㽑 l>c)_Ox,u4RQ|קl/`STO>7.aM~Yp`]"Yld-&R/s<8dҨo(kRJ┠ΟPKk U-++csstemplate.css.field-custom-hide { display: none; }PKk Uۊ jstemplate.js5A 0DUh.P*\@I~011&Xg5List header

PKk Ut566listtemplatefooter.html

List footer

PKk Ursstitletemplate.htmlPKk UA> preset.xmlUMs0W0{RCԗtdXMp ;c&9/1ЇS##h#T.{`v XFZt)lE[$ZiZk؆OP%YuBh mi&8;9-c!H(:\"3(*;GpུFi[ Ȼ<3B$w myl 0z<\P8-TZ%g:  %tJ13&:y 'llA_2g(_)m_x57<JQ5UVvK̒z'g߲c4`nB7{QԖ53^{nUwFF{Cr ̊:rc3ۧw VlV=~?B[6^/2ϪIeGXLPq=AKGpEbjD(m\t{|PK?k UwAQz listtemplate.htmlPK?k Uh singletemplate.htmlPK?k UR asearchtemplate.htmlPK?k UG addtemplate.htmlPK?k Us1] -rsstemplate.htmlPK?k U-++csstemplate.cssPK?k Uۊ N jstemplate.jsPK?k U%l66 listtemplateheader.htmlPK?k Ut566n listtemplatefooter.htmlPK?k U rsstitletemplate.htmlPK?k UA> preset.xmlPK  tests/fixtures/test_data_export_without_files.csv000064400000000171152265517740016627 0ustar00numberfield,textfield,filefield1,filefield2,picturefield 3,"a simple text",samplefile.png,samplefile.png,picturefile.png tests/fixtures/test_data_export_with_files.csv000064400000000173152265517740016101 0ustar00numberfield,textfield,filefield1,filefield2,picturefield 3,"a simple text",samplefile.png,samplefile_1.png,picturefile.png tests/backup/restore_date_test.php000064400000007277152265517740013436 0ustar00. namespace mod_data\backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); require_once($CFG->dirroot . '/rating/lib.php'); /** * Restore date tests. * * @package mod_data * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_date_test extends \restore_date_testcase { /** * Test restore dates. */ public function test_restore_dates(): void { global $DB, $USER; $gg = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = ['assesstimefinish' => 100, 'assesstimestart' => 100, 'ratingtime' => 1, 'assessed' => 2, 'scale' => 1, 'timeavailablefrom' => 100, 'timeavailableto' => 100, 'timeviewfrom' => 100, 'timeviewto' => 100]; list($course, $data) = $this->create_course_and_module('data', $record); // Data field/record. $timestamp = 996699; $diff = $this->get_diff(); $record = new \stdClass(); $record->name = 'field-1'; $record->type = 'text'; $field = $gg->create_field($record, $data); $datarecordid = $gg->create_entry($data, [$field->field->id => 'NERDS NERDS EVERYWHERE, NO BRAIN TO THINK']); $datarecord = $DB->get_record('data_records', ['id' => $datarecordid]); // Ratings. $ratingoptions = new \stdClass; $ratingoptions->context = \context_module::instance($data->cmid); $ratingoptions->ratingarea = 'entry'; $ratingoptions->component = 'mod_data'; $ratingoptions->itemid = $datarecord->id; $ratingoptions->scaleid = 2; $ratingoptions->userid = $USER->id; $rating = new \rating($ratingoptions); $rating->update_rating(2); $rating = $DB->get_record('rating', ['itemid' => $datarecord->id]); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); $newdata = $DB->get_record('data', ['course' => $newcourseid]); $this->assertFieldsNotRolledForward($data, $newdata, ['timemodified']); $props = ['assesstimefinish', 'assesstimestart', 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto']; $this->assertFieldsRolledForward($data, $newdata, $props); $newdatarecord = $DB->get_record('data_records', ['dataid' => $newdata->id]); $newcm = $DB->get_record('course_modules', ['course' => $newcourseid, 'instance' => $newdata->id]); // Data record time checks. $this->assertEquals($datarecord->timecreated, $newdatarecord->timecreated); $this->assertEquals($datarecord->timemodified, $newdatarecord->timemodified); // Rating test. $newrating = $DB->get_record('rating', ['contextid' => \context_module::instance($newcm->id)->id]); $this->assertEquals($rating->timecreated, $newrating->timecreated); $this->assertEquals($rating->timemodified, $newrating->timemodified); } } tests/backup/restore_type_test.php000064400000005472152265517740013475 0ustar00. namespace mod_data\backup; /** * Restore type tests. * * @package mod_data * @copyright 2024 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_type_test extends \advanced_testcase { /** * Data provider for test_duplicating_data_remove_unwanted_types. * * @return array[] */ public static function restore_format_test_provider(): array { return [ 'text' => [ 'type' => 'text', 'expected' => 'text', ], 'picture' => [ 'type' => 'picture', 'expected' => 'picture', ], 'wrong type' => [ 'type' => '../wrongtype123', 'expected' => 'wrongtype', ], ]; } /** * Test that duplicating a database removes unwanted / invalid format. * * @param string $type The type of the field. * @param string $expected The expected type of the field after duplication. * * @covers \restore_data_activity_structure_step * @dataProvider restore_format_test_provider */ public function test_duplicating_data_remove_unwanted_types(string $type, string $expected): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Make a test course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field( (object) ['name' => 'field', 'type' => $type], $data ); // Duplicate the data module with the type. $newdata = duplicate_module($course, get_fast_modinfo($course)->get_cm($data->cmid)); // Verify the settings of the duplicated activity. $fields = $DB->get_records('data_fields', ['dataid' => $newdata->instance], 'id'); $newfield = reset($fields); $this->assertEquals($expected, $newfield->type); } } tests/backup/encode_links_test.php000064400000010012152265517740013370 0ustar00. namespace mod_data\backup; /** * Tests for Database * * @package mod_data * @category test * @copyright 2025 ISB Bayern * @author Stefan Hanauska * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class encode_links_test extends \advanced_testcase { /** * Test that links are encoded correctly. * * @return void * * @covers \backup_data_activity_task::encode_content_links * @covers \restore_data_activity_task::define_decode_rules */ public function test_encode_links(): void { global $CFG, $DB; $this->resetAfterTest(); $this->setAdminUser(); // Make a test course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $newcourse = $generator->create_course(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $field = $datagenerator->create_field( (object) ['name' => 'field', 'type' => 'text'], $data ); $entry = [$field->field->id => 'test']; $datagenerator->create_entry($data, $entry); $data->intro = $CFG->wwwroot . '/mod/data/view.php?id=' . $data->cmid . '|'; $data->intro .= urlencode($CFG->wwwroot . '/mod/data/view.php?id='. $data->cmid) . '|'; $data->intro .= $CFG->wwwroot . '/mod/data/view.php?d=' . $data->id . '|'; $data->intro .= urlencode($CFG->wwwroot . '/mod/data/view.php?d='. $data->id) . '|'; $data->intro .= $CFG->wwwroot . '/mod/data/index.php?id=' . $data->course . '|'; $data->intro .= urlencode($CFG->wwwroot . '/mod/data/index.php?id=' . $data->course) . '|'; $data->intro .= $CFG->wwwroot . '/mod/data/edit.php?id=' . $data->cmid . '|'; $data->intro .= urlencode($CFG->wwwroot . '/mod/data/edit.php?id='. $data->cmid) . '|'; $data->intro .= $CFG->wwwroot . '/mod/data/edit.php?d=' . $data->id . '|'; $data->intro .= urlencode($CFG->wwwroot . '/mod/data/edit.php?d=' . $data->id) . '|'; $DB->update_record('data', $data); // Duplicate the data module with the type. $newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($data->cmid)); $newdata = $DB->get_record('data', ['id' => $newcm->instance]); $expected = $CFG->wwwroot . '/mod/data/view.php?id=' . $newcm->id . '|'; $expected .= urlencode($CFG->wwwroot . '/mod/data/view.php?id=' . $newcm->id) . '|'; $expected .= $CFG->wwwroot . '/mod/data/view.php?d=' . $newdata->id . '|'; $expected .= urlencode($CFG->wwwroot . '/mod/data/view.php?d=' . $newdata->id) . '|'; $expected .= $CFG->wwwroot . '/mod/data/index.php?id=' . $newcm->course . '|'; $expected .= urlencode($CFG->wwwroot . '/mod/data/index.php?id=' . $newcm->course) . '|'; $expected .= $CFG->wwwroot . '/mod/data/edit.php?id=' . $newcm->id . '|'; $expected .= urlencode($CFG->wwwroot . '/mod/data/edit.php?id='. $newcm->id) . '|'; $expected .= $CFG->wwwroot . '/mod/data/edit.php?d=' . $newdata->id . '|'; $expected .= urlencode($CFG->wwwroot . '/mod/data/edit.php?d=' . $newdata->id) . '|'; $this->assertEquals($expected, $newdata->intro); } } tests/generator/behat_mod_data_generator.php000064400000012015152265517740015403 0ustar00. /** * Behat data generator for mod_data. * * @package mod_data * @category test * @copyright 2022 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_data_generator extends behat_generator_base { /** * Get a list of the entities that Behat can create using the generator step. * * @return array */ protected function get_creatable_entities(): array { return [ 'entries' => [ 'singular' => 'entry', 'datagenerator' => 'entry', 'required' => ['database'], 'switchids' => ['database' => 'databaseid', 'user' => 'userid', 'group' => 'groupid'], ], 'fields' => [ 'singular' => 'field', 'datagenerator' => 'field', 'required' => ['database', 'type', 'name'], 'switchids' => ['database' => 'databaseid'], ], 'templates' => [ 'singular' => 'template', 'datagenerator' => 'template', 'required' => ['database', 'name'], 'switchids' => ['database' => 'databaseid'], ], 'presets' => [ 'singular' => 'preset', 'datagenerator' => 'preset', 'required' => ['database', 'name'], 'switchids' => ['database' => 'databaseid', 'user' => 'userid'], ], ]; } /** * Get the database id using an activity idnumber. * * @param string $idnumber * @return int The database id */ protected function get_database_id(string $idnumber): int { $cm = $this->get_cm_by_activity_name('data', $idnumber); return $cm->instance; } /** * Add an entry. * * @param array $data Entry data. */ public function process_entry(array $data): void { global $DB; $database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST); unset($data['databaseid']); $userid = 0; if (array_key_exists('userid', $data)) { $userid = $data['userid']; unset($data['userid']); } if (array_key_exists('groupid', $data)) { $groupid = $data['groupid']; unset($data['groupid']); } else { $groupid = 0; } $data = array_reduce(array_keys($data), function ($fields, $fieldname) use ($data, $database) { global $DB; $field = $DB->get_record('data_fields', ['name' => $fieldname, 'dataid' => $database->id], 'id', MUST_EXIST); $fields[$field->id] = $data[$fieldname]; return $fields; }, []); $this->get_data_generator()->create_entry($database, $data, $groupid, [], null, $userid); } /** * Add a field. * * @param array $data Field data. */ public function process_field(array $data): void { global $DB; $database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST); unset($data['databaseid']); $this->get_data_generator()->create_field((object) $data, $database); } /** * Add a template. * * @param array $data Template data. */ public function process_template(array $data): void { global $DB; $database = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST); if (empty($data['content'])) { data_generate_default_template($database, $data['name']); } else { $newdata = new stdClass(); $newdata->id = $database->id; $newdata->{$data['name']} = $data['content']; $DB->update_record('data', $newdata); } } /** * Saves a preset. * * @param array $data Preset data. */ protected function process_preset(array $data): void { global $DB; $instance = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST); $this->get_data_generator()->create_preset($instance, (object) $data); } /** * Get the module data generator. * * @return mod_data_generator Database data generator. */ protected function get_data_generator(): mod_data_generator { return $this->componentdatagenerator; } } tests/generator/lib.php000064400000035136152265517740011201 0ustar00. /** * Data generator class for mod_data. * * @package mod_data * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use mod_data\manager; use mod_data\preset; defined('MOODLE_INTERNAL') || die(); /** * Data generator class for mod_data. * * @package mod_data * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_data_generator extends testing_module_generator { /** * @var int keep track of how many database fields have been created. */ protected $databasefieldcount = 0; /** * @var int keep track of how many database records have been created. */ protected $databaserecordcount = 0; /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->databasefieldcount = 0; $this->databaserecordcount = 0; parent::reset(); } /** * Creates a mod_data instance * * @param array $record * @param array $options * @return StdClass */ public function create_instance($record = null, ?array $options = null) { // Note, the parent class does not type $record to cast to array and then to object. $record = (object) (array) $record; if (!isset($record->assessed)) { $record->assessed = 0; } if (!isset($record->scale)) { $record->scale = 0; } return parent::create_instance((array) $record, $options); } /** * Creates a field for a mod_data instance. * * @param stdClass $record * @param stdClass|null $data * @return data_field_base */ public function create_field(?stdClass $record = null, $data = null) { $record = (array) $record; $this->databasefieldcount++; if (!isset($data->course)) { throw new coding_exception('course must be present in phpunit_util::create_field() $data'); } if (!isset($data->id)) { throw new coding_exception('dataid must be present in phpunit_util::create_field() $data'); } else { $record['dataid'] = $data->id; } if (!isset($record['type'])) { throw new coding_exception('type must be present in phpunit_util::create_field() $record'); } if (!isset($record['required'])) { $record['required'] = 0; } if (!isset($record['name'])) { $record['name'] = "testField - " . $this->databasefieldcount; } if (!isset($record['description'])) { $record['description'] = " This is testField - " . $this->databasefieldcount; } if (isset($record['param1']) && !empty($record['param1'])) { // Some fields have multiline entries. $record['param1'] = str_replace('\n', "\n", $record['param1']); } if (!isset($record['param1'])) { if ($record['type'] == 'checkbox') { $record['param1'] = implode("\n", array('opt1', 'opt2', 'opt3', 'opt4')); } else if ($record['type'] == 'radiobutton') { $record['param1'] = implode("\n", array('radioopt1', 'radioopt2', 'radioopt3', 'radioopt4')); } else if ($record['type'] == 'menu') { $record['param1'] = implode("\n", array('menu1', 'menu2', 'menu3', 'menu4')); } else if ($record['type'] == 'multimenu') { $record['param1'] = implode("\n", array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4')); } else if (($record['type'] === 'text') || ($record['type'] === 'url')) { $record['param1'] = 1; } else if ($record['type'] == 'latlong') { $record['param1'] = 'Google Maps'; } else { $record['param1'] = ''; } } if (!isset($record['param2'])) { if ($record['type'] === 'textarea') { $record['param2'] = 60; } else if ($record['type'] == 'latlong') { $record['param2'] = -1; } else { $record['param2'] = ''; } } if (!isset($record['param3'])) { if (($record['type'] === 'textarea')) { $record['param3'] = 35; } else if ($record['type'] == 'picture' || $record['type'] == 'file') { $record['param3'] = 0; } else { $record['param3'] = ''; } } if (!isset($record['param4'])) { if (($record['type'] === 'textarea')) { $record['param4'] = 1; } } if (!isset($record['param5'])) { if (($record['type'] === 'textarea')) { $record['param5'] = 0; } } $record = (object) $record; $field = data_get_field($record, $data); $field->insert_field(); return $field; } /** * Creates a field for a mod_data instance. * Keep in mind the default data field params created in create_field() function! * ...if you haven't provided your own custom data field parameters there. * The developers using the generator must adhere to the following format : * * Syntax : $contents[ fieldid ] = fieldvalue * $contents['checkbox'] = array('val1', 'val2', 'val3' .....) * $contents['data'] = 'dd-mm-yyyy' * $contents['menu'] = 'value'; * $contents['multimenu'] = array('val1', 'val2', 'val3' .....) * $contents['number'] = 'numeric value' * $contents['radiobuton'] = 'value' * $contents['text'] = 'text' * $contents['textarea'] = 'text' * $contents['url'] = 'example.url' or array('example.url', 'urlname') * $contents['latlong'] = array('value for lattitude', 'value for longitude') * $contents['file'] = 'filename or draftitemid' * $contents['picture'] = array('filename or draftitemid', 'alternative text') * * @param stdClass $data record from table {data} * @param array $contents * @param int $groupid * @param array $tags * @param array $options * @param int $userid if defined, it will be the author of the entry * @return int id of the generated record in table {data_records} */ public function create_entry($data, array $contents, $groupid = 0, $tags = [], ?array $options = null, int $userid = 0) { global $DB, $USER, $CFG; // Set current user if defined. if (!empty($userid)) { $currentuser = $USER; $user = \core_user::get_user($userid); $this->set_user($user); } $this->databaserecordcount++; $recordid = data_add_record($data, $groupid); if (isset($options['approved'])) { data_approve_entry($recordid, !empty($options['approved'])); } else { $approved = null; } $fields = $DB->get_records('data_fields', array('dataid' => $data->id)); // Validating whether required field are filled. foreach ($fields as $field) { $fieldhascontent = true; $field = data_get_field($field, $data); $fieldid = $field->field->id; if ($field->type === 'date') { $values = array(); $temp = explode('-', $contents[$fieldid], 3); $values['field_' . $fieldid . '_day'] = (int)trim($temp[0]); $values['field_' . $fieldid . '_month'] = (int)trim($temp[1]); $values['field_' . $fieldid . '_year'] = (int)trim($temp[2]); // Year should be less than 2038, so it can be handled by 32 bit windows. if ($values['field_' . $fieldid . '_year'] > 2038) { throw new coding_exception('DateTime::getTimestamp resturns false on 32 bit win for year beyond ' . '2038. Please use year less than 2038.'); } $contents[$fieldid] = $values; foreach ($values as $fieldname => $value) { if (!$field->notemptyfield($value, $fieldname)) { $fieldhascontent = false; } } } else if ($field->type === 'textarea') { $values = array(); $values['field_' . $fieldid] = $contents[$fieldid]; $values['field_' . $fieldid . '_content1'] = 1; $contents[$fieldid] = $values; $fieldname = 'field_' . $fieldid; if (!$field->notemptyfield($values[$fieldname], $fieldname)) { $fieldhascontent = false; } } else if ($field->type === 'url') { $values = array(); if (is_array($contents[$fieldid])) { foreach ($contents[$fieldid] as $key => $value) { $values['field_' . $fieldid . '_' . $key] = $value; } } else { $values['field_' . $fieldid . '_0'] = $contents[$fieldid]; } $contents[$fieldid] = $values; $fieldname = 'field_' . $fieldid . '_0'; if (!$field->notemptyfield($values[$fieldname], $fieldname)) { $fieldhascontent = false; } } else if ($field->type === 'latlong') { $values = array(); foreach ($contents[$fieldid] as $key => $value) { $values['field_' . $fieldid . '_' . $key] = $value; } $contents[$fieldid] = $values; $fieldname = 'field_' . $fieldid . '_0'; if (!$field->notemptyfield($values[$fieldname], $fieldname)) { $fieldhascontent = false; } } else if ($field->type === 'file' || $field->type === 'picture') { if (is_array($contents[$fieldid])) { list($itemid, $alttext) = $contents[$fieldid]; } else { $itemid = $contents[$fieldid]; $alttext = ''; } if (strlen($itemid) && !is_numeric($itemid)) { // We expect draftarea item id here but it can also be a filename, in this case provider will generate file. $filename = $itemid; $usercontext = context_user::instance($USER->id); $itemid = file_get_unused_draft_itemid(); get_file_storage()->create_file_from_string(['component' => 'user', 'filearea' => 'draft', 'contextid' => $usercontext->id, 'itemid' => $itemid, 'filepath' => '/', 'filename' => $filename], file_get_contents($CFG->dirroot.'/mod/data/field/picture/pix/sample.png')); } $fieldname = 'field_' . $fieldid . '_file'; if ($field->type === 'file') { $contents[$fieldid] = $itemid; } else { $contents[$fieldid] = [ $fieldname => $itemid, 'field_' . $fieldid . '_alttext' => $alttext ]; } if (!$field->notemptyfield($itemid, $fieldname)) { $fieldhascontent = false; } } else { if ($field->notemptyfield($contents[$fieldid], 'field_' . $fieldid . '_0')) { continue; } } if ($field->field->required && !$fieldhascontent) { return false; } } foreach ($contents as $fieldid => $content) { $field = data_get_field_from_id($fieldid, $data); if (is_array($content) and in_array($field->type, array('date', 'textarea', 'url', 'picture', 'latlong'))) { foreach ($content as $fieldname => $value) { $field->update_content($recordid, $value, $fieldname); } } else { $field->update_content($recordid, $content); } } if (!empty($tags)) { $cm = get_coursemodule_from_instance('data', $data->id); core_tag_tag::set_item_tags('mod_data', 'data_records', $recordid, context_module::instance($cm->id), $tags); } if (isset($currentuser)) { $this->set_user($currentuser); } return $recordid; } /** * Creates a preset from a mod_data instance. * * @param stdClass $instance The mod_data instance. * @param stdClass|null $record The preset information, like 'name'. * @return preset The preset that has been created. */ public function create_preset(stdClass $instance, ?stdClass $record = null): preset { global $USER; if (is_null($record)) { $record = new stdClass(); } // Set current user if defined. if (isset($record->userid) && $record->userid != $USER->id) { $currentuser = $USER; $user = \core_user::get_user($record->userid); $this->set_user($user); } // Fill in optional values if not specified. $presetname = 'New preset ' . microtime(); if (isset($record->name)) { $presetname = $record->name; } $presetdescription = null; if (isset($record->description)) { $presetdescription = $record->description; } $manager = manager::create_from_instance($instance); $preset = preset::create_from_instance($manager, $presetname, $presetdescription); $preset->save(); if (isset($currentuser)) { $this->set_user($currentuser); } return $preset; } } tests/external/delete_saved_preset_test.php000064400000013521152265517740015326 0ustar00. namespace mod_data\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use externallib_advanced_testcase; use core_external\external_api; use mod_data\manager; /** * External function test for delete_saved_preset. * * @package mod_data * @category external * @since Moodle 4.1 * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data\external\delete_saved_preset */ final class delete_saved_preset_test extends externallib_advanced_testcase { /** * Test the behaviour of delete_saved_preset(). * * @covers ::execute */ public function test_delete_saved_preset(): void { $this->resetAfterTest(); // Create course, database activity and users. $course = $this->getDataGenerator()->create_course(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $manager = manager::create_from_instance($data); $initialpresets = $manager->get_available_presets(); $preset1name = 'Admin preset'; $preset2name = 'Teacher preset'; // Trying to delete a preset when there is no saved preset created. $result = delete_saved_preset::execute($data->id, [$preset1name, $preset2name]); $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); $this->assertFalse($result['result']); $this->assertCount(2, $result['warnings']); // Check no preset has been deleted. $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets), count($currentpresets)); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object)[ 'name' => $preset1name, 'description' => 'Testing preset description', ]; $adminpreset = $plugingenerator->create_preset($data, $record); // Update initial preset list. $initialpresets = $manager->get_available_presets(); // There is a warning for non-existing preset. $result = delete_saved_preset::execute($data->id, ['Another preset']); $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); $this->assertFalse($result['result']); $this->assertCount(1, $result['warnings']); // Check no preset has been deleted. $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets), count($currentpresets)); // Create a saved preset by teacher. $this->setUser($teacher); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object)[ 'name' => $preset2name, 'description' => 'Testing preset description', ]; $teacherpreset = $plugingenerator->create_preset($data, $record); // Update initial preset list. $this->setAdminUser(); $initialpresets = $manager->get_available_presets(); // Student can't delete presets. $this->setUser($student); $result = delete_saved_preset::execute($data->id, [$preset1name, $preset2name]); $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); $this->assertFalse($result['result']); $this->assertCount(2, $result['warnings']); // Check no preset has been deleted. $this->setAdminUser(); $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets), count($currentpresets)); // Teacher can delete their preset. $this->setUser($teacher); $result = delete_saved_preset::execute($data->id, [$preset2name]); $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); $this->assertTrue($result['result']); $this->assertCount(0, $result['warnings']); // Check the preset has been deleted. $this->setAdminUser(); $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); foreach ($currentpresets as $currentpreset) { $this->assertNotEquals($currentpreset->name, $preset2name); } // Teacher can't delete other users' preset. $this->setUser($teacher); $result = delete_saved_preset::execute($data->id, [$preset1name]); $result = external_api::clean_returnvalue(delete_saved_preset::execute_returns(), $result); $this->assertFalse($result['result']); $this->assertCount(1, $result['warnings']); // Check no preset has been deleted. $this->setAdminUser(); $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); } } tests/external/get_mapping_information_test.php000064400000022415152265517740016221 0ustar00. namespace mod_data\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; use mod_data\manager; /** * External function tests class for get_mapping_information. * * @package mod_data * @category external * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data\external\get_mapping_information */ final class get_mapping_information_test extends \advanced_testcase { /** * Data provider for test_get_mapping_information(). * * @return array[] */ public static function get_mapping_information_provider(): array { // Image gallery preset is: ['title' => 'text', 'description' => 'textarea', 'image' => 'picture']; $titlefield = new \stdClass(); $titlefield->name = 'title'; $titlefield->type = 'text'; $descfield = new \stdClass(); $descfield->name = 'description'; $descfield->type = 'textarea'; $imagefield = new \stdClass(); $imagefield->name = 'image'; $imagefield->type = 'picture'; $difffield = new \stdClass(); $difffield->name = 'title'; $difffield->type = 'textarea'; $newfield = new \stdClass(); $newfield->name = 'number'; $newfield->type = 'number'; return [ 'Empty database / Empty importer' => [ 'currentfields' => [], 'newfields' => [], 'pluginname' => '', 'fieldstocreate' => '', 'fieldstoremove' => '', ], 'Empty database / Importer with fields' => [ 'currentfields' => [], 'newfields' => [$imagefield, $titlefield, $descfield], 'pluginname' => 'imagegallery', 'fieldstocreate' => 'image, title, description', 'fieldstoremove' => '', ], 'Database with fields / Empty importer' => [ 'currentfields' => [$imagefield, $titlefield, $descfield], 'newfields' => [], 'pluginname' => '', 'fieldstocreate' => '', 'fieldstoremove' => 'image, title, description', ], 'Same fields' => [ 'currentfields' => [$imagefield, $titlefield, $descfield], 'newfields' => [$imagefield, $titlefield, $descfield], 'pluginname' => 'imagegallery', 'fieldstocreate' => '', 'fieldstoremove' => '', ], 'Fields to create' => [ 'currentfields' => [$titlefield, $descfield], 'newfields' => [$imagefield, $titlefield, $descfield], 'pluginname' => 'imagegallery', 'fieldstocreate' => 'image', 'fieldstoremove' => '', ], 'Fields to remove' => [ 'currentfields' => [$imagefield, $titlefield, $descfield, $difffield], 'newfields' => [$imagefield, $titlefield, $descfield], 'pluginname' => 'imagegallery', 'fieldstocreate' => '', 'fieldstoremove' => 'title', ], 'Fields to update' => [ 'currentfields' => [$imagefield, $difffield, $descfield], 'newfields' => [$imagefield, $titlefield, $descfield], 'pluginname' => 'imagegallery', 'fieldstocreate' => 'title', 'fieldstoremove' => 'title', ], 'Fields to create, remove and update' => [ 'currentfields' => [$titlefield, $descfield, $imagefield, $difffield], 'newfields' => [$titlefield, $descfield, $newfield], 'pluginname' => '', 'fieldstocreate' => 'number', 'fieldstoremove' => 'image, title', ], ]; } /** * Test for get_mapping_information method. * * @dataProvider get_mapping_information_provider * @covers ::execute * * @param array $currentfields Fields of the current activity. * @param array $newfields Fields to be imported. * @param string $pluginname The plugin preset to be imported. * @param string $fieldstocreate Expected fields on $fieldstocreate. * @param string $fieldstoremove Expected fields on $fieldstoremove. */ public function test_get_mapping_information( array $currentfields, array $newfields, string $pluginname, string $fieldstocreate, string $fieldstoremove ): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($currentfields as $field) { $plugingenerator->create_field($field, $activity); } $manager = manager::create_from_instance($activity); $module = $manager->get_coursemodule(); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($newfields as $field) { $plugingenerator->create_field($field, $presetactivity); } $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); $result = get_mapping_information::execute($module->id, $USER->id . '/' . $saved->name); $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); $this->assertEquals($result['data']['fieldstocreate'], $fieldstocreate); $this->assertEquals($result['data']['fieldstoremove'], $fieldstoremove); // Create presets and importers. if ($pluginname) { $result = get_mapping_information::execute($module->id, '/' . $pluginname);; $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); $this->assertEquals($result['data']['fieldstoremove'], $fieldstoremove); $this->assertEquals($result['data']['fieldstocreate'], $fieldstocreate); } } /** * Test for get_mapping_information method for wrong presets. * * @covers ::execute * */ public function test_get_mapping_information_for_wrong_preset(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); $module = $manager->get_coursemodule(); // We get warnings with empty preset name. $result = get_mapping_information::execute($module->id, ''); $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); $this->assertFalse(array_key_exists('data', $result)); $this->assertTrue(array_key_exists('warnings', $result)); // We get warnings with non-existing preset name. $result = get_mapping_information::execute($module->id, $USER->id . '/Non-existing'); $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); $this->assertFalse(array_key_exists('data', $result)); $this->assertTrue(array_key_exists('warnings', $result)); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($activity, $record); // We get no warning with the right preset. $result = get_mapping_information::execute($module->id, $USER->id . '/' . $saved->name); $result = external_api::clean_returnvalue(get_mapping_information::execute_returns(), $result); $this->assertTrue(array_key_exists('data', $result)); $this->assertFalse(array_key_exists('warnings', $result)); } } tests/privacy/provider_test.php000064400000027621152265517740013013 0ustar00. /** * Privacy provider tests. * * @package mod_data * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_userlist; use mod_data\privacy\provider; defined('MOODLE_INTERNAL') || die(); /** * Privacy provider tests class. * * @package mod_data * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** @var stdClass The student object. */ protected $student; /** @var stdClass The student object. */ protected $student2; /** @var stdClass The student object. */ protected $student3; /** @var stdClass The data object. */ protected $datamodule; /** @var stdClass The course object. */ protected $course; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); global $DB; $generator = $this->getDataGenerator(); $course = $generator->create_course(); $params = [ 'course' => $course->id, 'name' => 'Database module', 'comments' => 1, 'assessed' => 1, ]; // The database activity. $datamodule = $this->get_generator()->create_instance($params); $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url', 'latlong', 'file', 'picture'); // Creating test Fields with default parameter values. foreach ($fieldtypes as $count => $fieldtype) { // Creating variables dynamically. $fieldname = 'field' . $count; $record = new \stdClass(); $record->name = $fieldname; $record->description = $fieldname . ' descr'; $record->type = $fieldtype; ${$fieldname} = $this->get_generator()->create_field($record, $datamodule); } $cm = get_coursemodule_from_instance('data', $datamodule->id); // Create a student. $student1 = $generator->create_user(); $student2 = $generator->create_user(); $student3 = $generator->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $generator->enrol_user($student1->id, $course->id, $studentrole->id); $generator->enrol_user($student2->id, $course->id, $studentrole->id); $generator->enrol_user($student3->id, $course->id, $studentrole->id); // Add records. $this->setUser($student1); $record1id = $this->generate_data_record($datamodule); $this->generate_data_record($datamodule); $this->setUser($student2); $this->generate_data_record($datamodule); $this->generate_data_record($datamodule); $this->generate_data_record($datamodule); $this->setUser($student3); $this->generate_data_record($datamodule); $this->student = $student1; $this->student2 = $student2; $this->student3 = $student3; $this->datamodule = $datamodule; $this->course = $course; } /** * Get mod_data generator * * @return mod_data_generator */ protected function get_generator() { return $this->getDataGenerator()->get_plugin_generator('mod_data'); } /** * Generates one record in the database module as the current student * * @param stdClass $datamodule * @return mixed */ protected function generate_data_record($datamodule) { global $DB; static $counter = 0; $counter++; $contents = array(); $contents[] = array('opt1', 'opt2', 'opt3', 'opt4'); $contents[] = sprintf("%02f", $counter) . '-01-2000'; $contents[] = 'menu1'; $contents[] = array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4'); $contents[] = 5 * $counter; $contents[] = 'radioopt1'; $contents[] = 'text for testing' . $counter; $contents[] = "

text area testing $counter

"; $contents[] = array('example.url', 'sampleurl' . $counter); $contents[] = [-31.9489873, 115.8382036]; // Latlong. $contents[] = "Filename{$counter}.pdf"; // File - filename. $contents[] = array("Cat{$counter}.jpg", 'Cat' . $counter); // Picture - filename with alt text. $count = 0; $fieldcontents = array(); $fields = $DB->get_records('data_fields', array('dataid' => $datamodule->id), 'id'); foreach ($fields as $fieldrecord) { $fieldcontents[$fieldrecord->id] = $contents[$count++]; } $tags = ['Cats', 'mice' . $counter]; return $this->get_generator()->create_entry($datamodule, $fieldcontents, 0, $tags); } /** * Test for provider::get_metadata(). */ public function test_get_metadata(): void { $collection = new collection('mod_data'); $newcollection = provider::get_metadata($collection); $itemcollection = $newcollection->get_collection(); $this->assertCount(7, $itemcollection); $table = reset($itemcollection); $this->assertEquals('data_records', $table->get_name()); $table = next($itemcollection); $this->assertEquals('data_content', $table->get_name()); } /** * Test for provider::get_contexts_for_userid(). */ public function test_get_contexts_for_userid(): void { $cm = get_coursemodule_from_instance('data', $this->datamodule->id); $contextlist = provider::get_contexts_for_userid($this->student->id); $this->assertCount(1, $contextlist); $contextforuser = $contextlist->current(); $cmcontext = \context_module::instance($cm->id); $this->assertEquals($cmcontext->id, $contextforuser->id); } /** * Test for provider::get_users_in_context(). */ public function test_get_users_in_context(): void { $component = 'mod_data'; $cm = get_coursemodule_from_instance('data', $this->datamodule->id); $cmcontext = \context_module::instance($cm->id); $userlist = new \core_privacy\local\request\userlist($cmcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(3, $userlist); $expected = [$this->student->id, $this->student2->id, $this->student3->id]; $actual = $userlist->get_userids(); sort($expected); sort($actual); $this->assertEquals($expected, $actual); } /** * Get test privacy writer * * @param context $context * @return \core_privacy\tests\request\content_writer */ protected function get_writer($context) { return \core_privacy\local\request\writer::with_context($context); } /** * Test for provider::export_user_data(). */ public function test_export_for_context(): void { global $DB; $cm = get_coursemodule_from_instance('data', $this->datamodule->id); $cmcontext = \context_module::instance($cm->id); $records = $DB->get_records_select('data_records', 'userid = :userid ORDER BY id', ['userid' => $this->student->id]); $record = reset($records); $contents = $DB->get_records('data_content', ['recordid' => $record->id]); // Export all of the data for the context. $this->export_context_data_for_user($this->student->id, $cmcontext, 'mod_data'); $writer = $this->get_writer($cmcontext); $data = $writer->get_data([$record->id]); $this->assertNotEmpty($data); foreach ($contents as $content) { $data = $writer->get_data([$record->id, $content->id]); $this->assertNotEmpty($data); $hasfile = in_array($data->field['type'], ['file', 'picture']); $this->assertEquals($hasfile, !empty($writer->get_files([$record->id, $content->id]))); } $tags = $writer->get_related_data([$record->id], 'tags'); $this->assertNotEmpty($tags); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { $cm = get_coursemodule_from_instance('data', $this->datamodule->id); $cmcontext = \context_module::instance($cm->id); provider::delete_data_for_all_users_in_context($cmcontext); $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]); provider::export_user_data($appctxt); $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { $cm = get_coursemodule_from_instance('data', $this->datamodule->id); $cmcontext = \context_module::instance($cm->id); $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]); provider::delete_data_for_user($appctxt); provider::export_user_data($appctxt); $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users(): void { $cm = get_coursemodule_from_instance('data', $this->datamodule->id); $cmcontext = \context_module::instance($cm->id); $userstodelete = [$this->student->id, $this->student2->id]; // Ensure student, student 2 and student 3 have data before being deleted. $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]); provider::export_user_data($appctxt); $this->assertTrue($this->get_writer($cmcontext)->has_any_data()); $appctxt = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_data', [$cmcontext->id]); provider::export_user_data($appctxt); $this->assertTrue($this->get_writer($cmcontext)->has_any_data()); // Delete data for student 1 and 2. $approvedlist = new approved_userlist($cmcontext, 'mod_data', $userstodelete); provider::delete_data_for_users($approvedlist); // Reset the writer so it doesn't contain the data from before deletion. \core_privacy\local\request\writer::reset(); // Ensure data is now deleted for student and student 2. $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]); provider::export_user_data($appctxt); $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); $appctxt = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_data', [$cmcontext->id]); provider::export_user_data($appctxt); $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); // Ensure data still intact for student 3. $appctxt = new \core_privacy\local\request\approved_contextlist($this->student3, 'mod_data', [$cmcontext->id]); provider::export_user_data($appctxt); $this->assertTrue($this->get_writer($cmcontext)->has_any_data()); } } tests/lib_test.php000064400000256646152265517740010265 0ustar00. /** * Unit tests for lib.php * * @package mod_data * @category phpunit * @copyright 2013 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data; use stdClass; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/data/lib.php'); /** * Unit tests for lib.php * * @package mod_data * @copyright 2013 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends \advanced_testcase { /** * @var moodle_database */ protected $DB = null; /** * Tear Down to reset DB. */ public function tearDown(): void { global $DB; if (isset($this->DB)) { $DB = $this->DB; $this->DB = null; } parent::tearDown(); } /** * Confirms that completionentries is working * Sets it to 1, confirms that * it is not complete. Inserts a record and * confirms that it is complete. */ public function test_data_completion(): void { global $DB, $CFG; $this->resetAfterTest(); $this->setAdminUser(); $CFG->enablecompletion = 1; $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $record = new \stdClass(); $record->course = $course->id; $record->name = "Mod data completion test"; $record->intro = "Some intro of some sort"; $record->completionentries = "1"; /* completion=2 means Show activity commplete when condition is met and completionentries means 1 record is * required for the activity to be considered complete */ $module = $this->getDataGenerator()->create_module('data', $record, array('completion' => 2, 'completionentries' => 1)); $cm = get_coursemodule_from_instance('data', $module->id, $course->id); $completion = new \completion_info($course); $completiondata = $completion->get_data($cm, true, 0); /* Confirm it is not complete as there are no entries */ $this->assertNotEquals(1, $completiondata->completionstate); $field = data_get_field_new('text', $module); $fielddetail = new \stdClass(); $fielddetail->d = $module->id; $fielddetail->mode = 'add'; $fielddetail->type = 'text'; $fielddetail->sesskey = sesskey(); $fielddetail->name = 'Name'; $fielddetail->description = 'Some name'; $field->define_field($fielddetail); $field->insert_field(); $recordid = data_add_record($module); $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid; $datacontent['content'] = 'Asterix'; $contentid = $DB->insert_record('data_content', $datacontent); $cm = get_coursemodule_from_instance('data', $module->id, $course->id); $completion = new \completion_info($course); $completiondata = $completion->get_data($cm); /* Confirm it is complete because it has 1 entry */ $this->assertEquals(1, $completiondata->completionstate); } public function test_data_delete_record(): void { global $DB; $this->resetAfterTest(); // Create a record for deleting. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->course = $course->id; $record->name = "Mod data delete test"; $record->intro = "Some intro of some sort"; $module = $this->getDataGenerator()->create_module('data', $record); $field = data_get_field_new('text', $module); $fielddetail = new \stdClass(); $fielddetail->d = $module->id; $fielddetail->mode = 'add'; $fielddetail->type = 'text'; $fielddetail->sesskey = sesskey(); $fielddetail->name = 'Name'; $fielddetail->description = 'Some name'; $field->define_field($fielddetail); $field->insert_field(); $recordid = data_add_record($module); $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid; $datacontent['content'] = 'Asterix'; $contentid = $DB->insert_record('data_content', $datacontent); $cm = get_coursemodule_from_instance('data', $module->id, $course->id); // Check to make sure that we have a database record. $data = $DB->get_records('data', array('id' => $module->id)); $this->assertEquals(1, count($data)); $datacontent = $DB->get_records('data_content', array('id' => $contentid)); $this->assertEquals(1, count($datacontent)); $datafields = $DB->get_records('data_fields', array('id' => $field->field->id)); $this->assertEquals(1, count($datafields)); $datarecords = $DB->get_records('data_records', array('id' => $recordid)); $this->assertEquals(1, count($datarecords)); // Test to see if a failed delete returns false. $result = data_delete_record(8798, $module, $course->id, $cm->id); $this->assertFalse($result); // Delete the record. $result = data_delete_record($recordid, $module, $course->id, $cm->id); // Check that all of the record is gone. $datacontent = $DB->get_records('data_content', array('id' => $contentid)); $this->assertEquals(0, count($datacontent)); $datarecords = $DB->get_records('data_records', array('id' => $recordid)); $this->assertEquals(0, count($datarecords)); // Make sure the function returns true on a successful deletion. $this->assertTrue($result); } /** * Test comment_created event. */ public function test_data_comment_created_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/comment/lib.php'); $this->resetAfterTest(); // Create a record for deleting. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->course = $course->id; $record->name = "Mod data delete test"; $record->intro = "Some intro of some sort"; $record->comments = 1; $module = $this->getDataGenerator()->create_module('data', $record); $field = data_get_field_new('text', $module); $fielddetail = new \stdClass(); $fielddetail->name = 'Name'; $fielddetail->description = 'Some name'; $field->define_field($fielddetail); $field->insert_field(); $recordid = data_add_record($module); $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid; $datacontent['content'] = 'Asterix'; $contentid = $DB->insert_record('data_content', $datacontent); $cm = get_coursemodule_from_instance('data', $module->id, $course->id); $context = \context_module::instance($module->cmid); $cmt = new \stdClass(); $cmt->context = $context; $cmt->course = $course; $cmt->cm = $cm; $cmt->area = 'database_entry'; $cmt->itemid = $recordid; $cmt->showcount = true; $cmt->component = 'mod_data'; $comment = new \comment($cmt); // Triggering and capturing the event. $sink = $this->redirectEvents(); $comment->add('New comment'); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_data\event\comment_created', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/data/view.php', array('id' => $cm->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } /** * Test comment_deleted event. */ public function test_data_comment_deleted_event(): void { global $CFG, $DB; require_once($CFG->dirroot . '/comment/lib.php'); $this->resetAfterTest(); // Create a record for deleting. $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->course = $course->id; $record->name = "Mod data delete test"; $record->intro = "Some intro of some sort"; $record->comments = 1; $module = $this->getDataGenerator()->create_module('data', $record); $field = data_get_field_new('text', $module); $fielddetail = new \stdClass(); $fielddetail->name = 'Name'; $fielddetail->description = 'Some name'; $field->define_field($fielddetail); $field->insert_field(); $recordid = data_add_record($module); $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid; $datacontent['content'] = 'Asterix'; $contentid = $DB->insert_record('data_content', $datacontent); $cm = get_coursemodule_from_instance('data', $module->id, $course->id); $context = \context_module::instance($module->cmid); $cmt = new \stdClass(); $cmt->context = $context; $cmt->course = $course; $cmt->cm = $cm; $cmt->area = 'database_entry'; $cmt->itemid = $recordid; $cmt->showcount = true; $cmt->component = 'mod_data'; $comment = new \comment($cmt); $newcomment = $comment->add('New comment 1'); // Triggering and capturing the event. $sink = $this->redirectEvents(); $comment->delete($newcomment->id); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_data\event\comment_deleted', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/mod/data/view.php', array('id' => $module->cmid)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } /** * Checks that data_user_can_manage_entry will return true if the user * has the mod/data:manageentries capability. */ public function test_data_user_can_manage_entry_return_true_with_capability(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $data = new \stdClass(); $this->setUser($user); assign_capability('mod/data:manageentries', CAP_ALLOW, $roleid, $context); $this->assertTrue(data_user_can_manage_entry($record, $data, $context), 'data_user_can_manage_entry() returns true if the user has mod/data:manageentries capability'); } /** * Checks that data_user_can_manage_entry will return false if the data * is set to readonly. */ public function test_data_user_can_manage_entry_return_false_readonly(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $this->setUser($user); // Need to make sure they don't have this capability in order to fall back to // the other checks. assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context); // Causes readonly mode to be enabled. $data = new \stdClass(); $now = time(); // Add a small margin around the periods to prevent errors with slow tests. $data->timeviewfrom = $now - 1; $data->timeviewto = $now + 5; $this->assertFalse(data_user_can_manage_entry($record, $data, $context), 'data_user_can_manage_entry() returns false if the data is read only'); } /** * Checks that data_user_can_manage_entry will return false if the record * can't be found in the database. */ public function test_data_user_can_manage_entry_return_false_no_record(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $data = new \stdClass(); // Causes readonly mode to be disabled. $now = time(); $data->timeviewfrom = $now + 100; $data->timeviewto = $now - 100; $this->setUser($user); // Need to make sure they don't have this capability in order to fall back to // the other checks. assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context); // Pass record id instead of object to force DB lookup. $this->assertFalse(data_user_can_manage_entry(1, $data, $context), 'data_user_can_manage_entry() returns false if the record cannot be found'); } /** * Checks that data_user_can_manage_entry will return false if the record * isn't owned by the user. */ public function test_data_user_can_manage_entry_return_false_not_owned_record(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $data = new \stdClass(); // Causes readonly mode to be disabled. $now = time(); $data->timeviewfrom = $now + 100; $data->timeviewto = $now - 100; // Make sure the record isn't owned by this user. $record->userid = $user->id + 1; $this->setUser($user); // Need to make sure they don't have this capability in order to fall back to // the other checks. assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context); $this->assertFalse(data_user_can_manage_entry($record, $data, $context), 'data_user_can_manage_entry() returns false if the record isnt owned by the user'); } /** * Checks that data_user_can_manage_entry will return true if the data * doesn't require approval. */ public function test_data_user_can_manage_entry_return_true_data_no_approval(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $data = new \stdClass(); // Causes readonly mode to be disabled. $now = time(); $data->timeviewfrom = $now + 100; $data->timeviewto = $now - 100; // The record doesn't need approval. $data->approval = false; // Make sure the record is owned by this user. $record->userid = $user->id; $this->setUser($user); // Need to make sure they don't have this capability in order to fall back to // the other checks. assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context); $this->assertTrue(data_user_can_manage_entry($record, $data, $context), 'data_user_can_manage_entry() returns true if the record doesnt require approval'); } /** * Checks that data_user_can_manage_entry will return true if the record * isn't yet approved. */ public function test_data_user_can_manage_entry_return_true_record_unapproved(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $data = new \stdClass(); // Causes readonly mode to be disabled. $now = time(); $data->timeviewfrom = $now + 100; $data->timeviewto = $now - 100; // The record needs approval. $data->approval = true; // Make sure the record is owned by this user. $record->userid = $user->id; // The record hasn't yet been approved. $record->approved = false; $this->setUser($user); // Need to make sure they don't have this capability in order to fall back to // the other checks. assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context); $this->assertTrue(data_user_can_manage_entry($record, $data, $context), 'data_user_can_manage_entry() returns true if the record is not yet approved'); } /** * Checks that data_user_can_manage_entry will return the 'manageapproved' * value if the record has already been approved. */ public function test_data_user_can_manage_entry_return_manageapproved(): void { $this->resetAfterTest(); $testdata = $this->create_user_test_data(); $user = $testdata['user']; $course = $testdata['course']; $roleid = $testdata['roleid']; $context = $testdata['context']; $record = $testdata['record']; $data = new \stdClass(); // Causes readonly mode to be disabled. $now = time(); $data->timeviewfrom = $now + 100; $data->timeviewto = $now - 100; // The record needs approval. $data->approval = true; // Can the user managed approved records? $data->manageapproved = false; // Make sure the record is owned by this user. $record->userid = $user->id; // The record has been approved. $record->approved = true; $this->setUser($user); // Need to make sure they don't have this capability in order to fall back to // the other checks. assign_capability('mod/data:manageentries', CAP_PROHIBIT, $roleid, $context); $canmanageentry = data_user_can_manage_entry($record, $data, $context); // Make sure the result of the check is what ever the manageapproved setting // is set to. $this->assertEquals($data->manageapproved, $canmanageentry, 'data_user_can_manage_entry() returns the manageapproved setting on approved records'); } /** * Helper method to create a set of test data for data_user_can_manage tests * * @return array contains user, course, roleid, module, context and record */ private function create_user_test_data() { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $roleid = $this->getDataGenerator()->create_role(); $record = new \stdClass(); $record->name = "test name"; $record->intro = "test intro"; $record->comments = 1; $record->course = $course->id; $record->userid = $user->id; $module = $this->getDataGenerator()->create_module('data', $record); $cm = get_coursemodule_from_instance('data', $module->id, $course->id); $context = \context_module::instance($module->cmid); $this->getDataGenerator()->role_assign($roleid, $user->id, $context->id); return array( 'user' => $user, 'course' => $course, 'roleid' => $roleid, 'module' => $module, 'context' => $context, 'record' => $record ); } /** * Tests for mod_data_rating_can_see_item_ratings(). * * @throws coding_exception * @throws rating_exception */ public function test_mod_data_rating_can_see_item_ratings(): void { global $DB; $this->resetAfterTest(); // Setup test data. $course = new \stdClass(); $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; $course = $this->getDataGenerator()->create_course($course); $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id)); $cm = get_coursemodule_from_instance('data', $data->id); $context = \context_module::instance($cm->id); // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); // Groups and stuff. $role = $DB->get_record('role', array('shortname' => 'teacher'), '*', MUST_EXIST); $this->getDataGenerator()->enrol_user($user1->id, $course->id, $role->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $role->id); $this->getDataGenerator()->enrol_user($user3->id, $course->id, $role->id); $this->getDataGenerator()->enrol_user($user4->id, $course->id, $role->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); groups_add_member($group1, $user1); groups_add_member($group1, $user2); groups_add_member($group2, $user3); groups_add_member($group2, $user4); // Add data. $field = data_get_field_new('text', $data); $fielddetail = new \stdClass(); $fielddetail->name = 'Name'; $fielddetail->description = 'Some name'; $field->define_field($fielddetail); $field->insert_field(); // Add a record with a group id of zero (all participants). $recordid1 = data_add_record($data, 0); $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid1; $datacontent['content'] = 'Obelix'; $DB->insert_record('data_content', $datacontent); $recordid = data_add_record($data, $group1->id); $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid; $datacontent['content'] = 'Asterix'; $DB->insert_record('data_content', $datacontent); // Now try to access it as various users. unassign_capability('moodle/site:accessallgroups', $role->id); // Eveyone should have access to the record with the group id of zero. $params1 = array('contextid' => 2, 'component' => 'mod_data', 'ratingarea' => 'entry', 'itemid' => $recordid1, 'scaleid' => 2); $params = array('contextid' => 2, 'component' => 'mod_data', 'ratingarea' => 'entry', 'itemid' => $recordid, 'scaleid' => 2); $this->setUser($user1); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user2); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user3); $this->assertFalse(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user4); $this->assertFalse(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); // Now try with accessallgroups cap and make sure everything is visible. assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $role->id, $context->id); $this->setUser($user1); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user2); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user3); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user4); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); // Change group mode and verify visibility. $course->groupmode = VISIBLEGROUPS; $DB->update_record('course', $course); unassign_capability('moodle/site:accessallgroups', $role->id); $this->setUser($user1); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user2); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user3); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); $this->setUser($user4); $this->assertTrue(mod_data_rating_can_see_item_ratings($params)); $this->assertTrue(mod_data_rating_can_see_item_ratings($params1)); } /** * Tests for mod_data_refresh_events. */ public function test_data_refresh_events(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $timeopen = time(); $timeclose = time() + 86400; $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $params['course'] = $course->id; $params['timeavailablefrom'] = $timeopen; $params['timeavailableto'] = $timeclose; $data = $generator->create_instance($params); // Normal case, with existing course. $this->assertTrue(data_refresh_events($course->id)); $eventparams = array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => 'open'); $openevent = $DB->get_record('event', $eventparams, '*', MUST_EXIST); $this->assertEquals($openevent->timestart, $timeopen); $eventparams = array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => 'close'); $closeevent = $DB->get_record('event', $eventparams, '*', MUST_EXIST); $this->assertEquals($closeevent->timestart, $timeclose); // In case the course ID is passed as a numeric string. $this->assertTrue(data_refresh_events('' . $course->id)); // Course ID not provided. $this->assertTrue(data_refresh_events()); $eventparams = array('modulename' => 'data'); $events = $DB->get_records('event', $eventparams); foreach ($events as $event) { if ($event->modulename === 'data' && $event->instance === $data->id && $event->eventtype === 'open') { $this->assertEquals($event->timestart, $timeopen); } if ($event->modulename === 'data' && $event->instance === $data->id && $event->eventtype === 'close') { $this->assertEquals($event->timestart, $timeclose); } } } /** * Data provider for tests of data_get_config. * * @return array */ public static function data_get_config_provider(): array { $initialdata = (object) [ 'template_foo' => true, 'template_bar' => false, 'template_baz' => null, ]; $database = (object) [ 'config' => json_encode($initialdata), ]; return [ 'Return full dataset (no key/default)' => [ [$database], $initialdata, ], 'Return full dataset (no default)' => [ [$database, null], $initialdata, ], 'Return full dataset' => [ [$database, null, null], $initialdata, ], 'Return requested key only, value true, no default' => [ [$database, 'template_foo'], true, ], 'Return requested key only, value false, no default' => [ [$database, 'template_bar'], false, ], 'Return requested key only, value null, no default' => [ [$database, 'template_baz'], null, ], 'Return unknown key, value null, no default' => [ [$database, 'template_bum'], null, ], 'Return requested key only, value true, default null' => [ [$database, 'template_foo', null], true, ], 'Return requested key only, value false, default null' => [ [$database, 'template_bar', null], false, ], 'Return requested key only, value null, default null' => [ [$database, 'template_baz', null], null, ], 'Return unknown key, value null, default null' => [ [$database, 'template_bum', null], null, ], 'Return requested key only, value true, default 42' => [ [$database, 'template_foo', 42], true, ], 'Return requested key only, value false, default 42' => [ [$database, 'template_bar', 42], false, ], 'Return requested key only, value null, default 42' => [ [$database, 'template_baz', 42], null, ], 'Return unknown key, value null, default 42' => [ [$database, 'template_bum', 42], 42, ], ]; } /** * Tests for data_get_config. * * @dataProvider data_get_config_provider * @param array $funcargs The args to pass to data_get_config * @param mixed $expectation The expected value */ public function test_data_get_config($funcargs, $expectation): void { $this->assertEquals($expectation, call_user_func_array('data_get_config', $funcargs)); } /** * Data provider for tests of data_set_config. * * @return array */ public static function data_set_config_provider(): array { $basevalue = (object) ['id' => rand(1, 1000)]; $config = [ 'template_foo' => true, 'template_bar' => false, ]; $withvalues = clone $basevalue; $withvalues->config = json_encode((object) $config); return [ 'Empty config, New value' => [ $basevalue, 'etc', 'newvalue', true, json_encode((object) ['etc' => 'newvalue']) ], 'Has config, New value' => [ clone $withvalues, 'etc', 'newvalue', true, json_encode((object) array_merge($config, ['etc' => 'newvalue'])) ], 'Has config, Update value, string' => [ clone $withvalues, 'template_foo', 'newvalue', true, json_encode((object) array_merge($config, ['template_foo' => 'newvalue'])) ], 'Has config, Update value, true' => [ clone $withvalues, 'template_bar', true, true, json_encode((object) array_merge($config, ['template_bar' => true])) ], 'Has config, Update value, false' => [ clone $withvalues, 'template_foo', false, true, json_encode((object) array_merge($config, ['template_foo' => false])) ], 'Has config, Update value, null' => [ clone $withvalues, 'template_foo', null, true, json_encode((object) array_merge($config, ['template_foo' => null])) ], 'Has config, No update, value true' => [ clone $withvalues, 'template_foo', true, false, $withvalues->config, ], ]; } /** * Tests for data_set_config. * * @dataProvider data_set_config_provider * @param object $database The example row for the entry * @param string $key The config key to set * @param mixed $value The value of the key * @param bool $expectupdate Whether we expected an update * @param mixed $newconfigvalue The expected value */ public function test_data_set_config($database, $key, $value, $expectupdate, $newconfigvalue): void { global $DB; // Mock the database. // Note: Use the actual test class here rather than the abstract because are testing concrete methods. $this->DB = $DB; $DB = $this->getMockBuilder(get_class($DB)) ->onlyMethods(['set_field']) ->getMock(); $DB->expects($this->exactly((int) $expectupdate)) ->method('set_field') ->with( 'data', 'config', $newconfigvalue, ['id' => $database->id] ); // Perform the update. data_set_config($database, $key, $value); // Ensure that the value was updated by reference in $database. $config = json_decode($database->config); $this->assertEquals($value, $config->$key); } public function test_mod_data_get_tagged_records(): void { $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course1 = $this->getDataGenerator()->create_course(); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course1->id, 'approval' => true)); $field1 = $datagenerator->create_field($fieldrecord, $data1); $datagenerator->create_entry($data1, [$field1->field->id => 'value11'], 0, ['Cats', 'Dogs']); $datagenerator->create_entry($data1, [$field1->field->id => 'value12'], 0, ['Cats', 'mice']); $datagenerator->create_entry($data1, [$field1->field->id => 'value13'], 0, ['Cats']); $datagenerator->create_entry($data1, [$field1->field->id => 'value14'], 0); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value12', $res->content); $this->assertStringContainsString('value13', $res->content); $this->assertStringNotContainsString('value14', $res->content); } public function test_mod_data_get_tagged_records_approval(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course2 = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course(); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course1->id)); $field1 = $datagenerator->create_field($fieldrecord, $data1); $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course2->id, 'approval' => true)); $field2 = $datagenerator->create_field($fieldrecord, $data2); $record11 = $datagenerator->create_entry($data1, [$field1->field->id => 'value11'], 0, ['Cats', 'Dogs']); $record21 = $datagenerator->create_entry($data2, [$field2->field->id => 'value21'], 0, ['Cats'], ['approved' => false]); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); $this->assertEmpty($res->prevpageurl); $this->assertEmpty($res->nextpageurl); // Create and enrol a user. $student = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual'); $this->setUser($student); // User can search data records inside a course. \core_tag_index_builder::reset_caches(); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringNotContainsString('value21', $res->content); $recordtoupdate = new \stdClass(); $recordtoupdate->id = $record21; $recordtoupdate->approved = true; $DB->update_record('data_records', $recordtoupdate); \core_tag_index_builder::reset_caches(); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); } public function test_mod_data_get_tagged_records_time(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course2 = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course(); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $timefrom = time() - YEARSECS; $timeto = time() - WEEKSECS; $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course1->id, 'approval' => true)); $field1 = $datagenerator->create_field($fieldrecord, $data1); $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course2->id, 'timeviewfrom' => $timefrom, 'timeviewto' => $timeto)); $field2 = $datagenerator->create_field($fieldrecord, $data2); $record11 = $datagenerator->create_entry($data1, [$field1->field->id => 'value11'], 0, ['Cats', 'Dogs']); $record21 = $datagenerator->create_entry($data2, [$field2->field->id => 'value21'], 0, ['Cats']); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); $this->assertEmpty($res->prevpageurl); $this->assertEmpty($res->nextpageurl); // Create and enrol a user. $student = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual'); $this->setUser($student); // User can search data records inside a course. \core_tag_index_builder::reset_caches(); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringNotContainsString('value21', $res->content); $data2->timeviewto = time() + YEARSECS; $DB->update_record('data', $data2); \core_tag_index_builder::reset_caches(); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); } public function test_mod_data_get_tagged_records_course_enrolment(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course2 = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course(); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course1->id, 'approval' => true)); $field1 = $datagenerator->create_field($fieldrecord, $data1); $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course2->id)); $field2 = $datagenerator->create_field($fieldrecord, $data2); $record11 = $datagenerator->create_entry($data1, [$field1->field->id => 'value11'], 0, ['Cats', 'Dogs']); $record21 = $datagenerator->create_entry($data2, [$field2->field->id => 'value21'], 0, ['Cats']); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); $this->assertEmpty($res->prevpageurl); $this->assertEmpty($res->nextpageurl); // Create and enrol a user. $student = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->setUser($student); \core_tag_index_builder::reset_caches(); // User can search data records inside a course. $coursecontext = \context_course::instance($course1->id); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringNotContainsString('value21', $res->content); $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual'); \core_tag_index_builder::reset_caches(); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); } public function test_mod_data_get_tagged_records_course_groups(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course2 = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course(); $groupa = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'name' => 'groupA')); $groupb = $this->getDataGenerator()->create_group(array('courseid' => $course2->id, 'name' => 'groupB')); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course1->id, 'approval' => true)); $field1 = $datagenerator->create_field($fieldrecord, $data1); $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course2->id)); $field2 = $datagenerator->create_field($fieldrecord, $data2); set_coursemodule_groupmode($data2->cmid, SEPARATEGROUPS); $record11 = $datagenerator->create_entry($data1, [$field1->field->id => 'value11'], 0, ['Cats', 'Dogs']); $record21 = $datagenerator->create_entry($data2, [$field2->field->id => 'value21'], $groupa->id, ['Cats']); $record22 = $datagenerator->create_entry($data2, [$field2->field->id => 'value22'], $groupb->id, ['Cats']); $tag = \core_tag_tag::get_by_name(0, 'Cats'); // Admin can see everything. $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); $this->assertStringContainsString('value22', $res->content); $this->assertEmpty($res->prevpageurl); $this->assertEmpty($res->nextpageurl); // Create and enrol a user. $student = self::getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual'); groups_add_member($groupa, $student); $this->setUser($student); \core_tag_index_builder::reset_caches(); // User can search data records inside a course. $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); $this->assertStringNotContainsString('value22', $res->content); groups_add_member($groupb, $student); \core_tag_index_builder::reset_caches(); $res = mod_data_get_tagged_records($tag, false, 0, 0, 1, 0); $this->assertStringContainsString('value11', $res->content); $this->assertStringContainsString('value21', $res->content); $this->assertStringContainsString('value22', $res->content); } /** * Test check_updates_since callback. */ public function test_check_updates_since(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Create user. $student = self::getDataGenerator()->create_user(); // User enrolment. $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual'); $this->setCurrentTimeStart(); $record = array( 'course' => $course->id, ); $data = $this->getDataGenerator()->create_module('data', $record); $cm = get_coursemodule_from_instance('data', $data->id, $course->id); $cm = \cm_info::create($cm); $this->setUser($student); // Check that upon creation, the updates are only about the new configuration created. $onehourago = time() - HOURSECS; $updates = data_check_updates_since($cm, $onehourago); foreach ($updates as $el => $val) { if ($el == 'configuration') { $this->assertTrue($val->updated); $this->assertTimeCurrent($val->timeupdated); } else { $this->assertFalse($val->updated); } } // Add a couple of entries. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $fieldtypes = array('checkbox', 'date'); $count = 1; // Creating test Fields with default parameter values. foreach ($fieldtypes as $fieldtype) { // Creating variables dynamically. $fieldname = 'field-' . $count; $record = new \stdClass(); $record->name = $fieldname; $record->type = $fieldtype; $record->required = 1; ${$fieldname} = $datagenerator->create_field($record, $data); $count++; } $fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id'); $contents = array(); $contents[] = array('opt1', 'opt2', 'opt3', 'opt4'); $contents[] = '01-01-2037'; // It should be lower than 2038, to avoid failing on 32-bit windows. $count = 0; $fieldcontents = array(); foreach ($fields as $fieldrecord) { $fieldcontents[$fieldrecord->id] = $contents[$count++]; } $datarecor1did = $datagenerator->create_entry($data, $fieldcontents); $datarecor2did = $datagenerator->create_entry($data, $fieldcontents); $records = $DB->get_records('data_records', array('dataid' => $data->id)); $this->assertCount(2, $records); // Check we received the entries updated. $updates = data_check_updates_since($cm, $onehourago); $this->assertTrue($updates->entries->updated); $this->assertEqualsCanonicalizing([$datarecor1did, $datarecor2did], $updates->entries->itemids); } public function test_data_core_calendar_provide_event_action_in_hidden_section(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailablefrom' => time() - DAYSECS, 'timeavailableto' => time() + DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Set sections 0 as hidden. set_section_visible($course->id, 0, 0); // Now, log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event is not shown at all. $this->assertNull($actionevent); } public function test_data_core_calendar_provide_event_action_for_non_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailablefrom' => time() - DAYSECS, 'timeavailableto' => time() + DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Now, log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory); // Confirm the event is not shown at all. $this->assertNull($actionevent); } public function test_data_core_calendar_provide_event_action_open(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailablefrom' => time() - DAYSECS, 'timeavailableto' => time() + DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('add', 'data'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_data_core_calendar_provide_event_action_open_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailablefrom' => time() - DAYSECS, 'timeavailableto' => time() + DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Now log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('add', 'data'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_data_core_calendar_provide_event_action_closed(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailableto' => time() - DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory); // No event on the dashboard if module is closed. $this->assertNull($actionevent); } public function test_data_core_calendar_provide_event_action_closed_for_user(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailableto' => time() - DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Now log out. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory, $student->id); // No event on the dashboard if module is closed. $this->assertNull($actionevent); } public function test_data_core_calendar_provide_event_action_open_in_future(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailablefrom' => time() + DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('add', 'data'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertFalse($actionevent->is_actionable()); } public function test_data_core_calendar_provide_event_action_open_in_future_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id, 'timeavailablefrom' => time() + DAYSECS)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Now log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('add', 'data'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertFalse($actionevent->is_actionable()); } public function test_data_core_calendar_provide_event_action_no_time_specified(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('add', 'data'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } public function test_data_core_calendar_provide_event_action_no_time_specified_for_user(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Create a student. $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create a database activity. $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id)); // Create a calendar event. $event = $this->create_action_event($course->id, $data->id, DATA_EVENT_TYPE_OPEN); // Now log out. $CFG->forcelogin = true; // We don't want to be logged in as guest, as guest users might still have some capabilities. $this->setUser(); // Create an action factory. $factory = new \core_calendar\action_factory(); // Decorate action event for the student. $actionevent = mod_data_core_calendar_provide_event_action($event, $factory, $student->id); // Confirm the event was decorated. $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent); $this->assertEquals(get_string('add', 'data'), $actionevent->get_name()); $this->assertInstanceOf('moodle_url', $actionevent->get_url()); $this->assertEquals(1, $actionevent->get_item_count()); $this->assertTrue($actionevent->is_actionable()); } /** * Creates an action event. * * @param int $courseid * @param int $instanceid The data id. * @param string $eventtype The event type. eg. DATA_EVENT_TYPE_OPEN. * @param int|null $timestart The start timestamp for the event * @return bool|calendar_event */ private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) { $event = new \stdClass(); $event->name = 'Calendar event'; $event->modulename = 'data'; $event->courseid = $courseid; $event->instance = $instanceid; $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = $eventtype; if ($timestart) { $event->timestart = $timestart; } else { $event->timestart = time(); } return \calendar_event::create($event); } /** * Test the callback responsible for returning the completion rule descriptions. * This function should work given either an instance of the module (cm_info), such as when checking the active rules, * or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type. */ public function test_mod_data_completion_get_active_rule_descriptions(): void { $this->resetAfterTest(); $this->setAdminUser(); // Two activities, both with automatic completion. One has the 'completionentries' rule, one doesn't. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]); $data1 = $this->getDataGenerator()->create_module('data', [ 'course' => $course->id, 'completion' => 2, 'completionentries' => 3 ]); $data2 = $this->getDataGenerator()->create_module('data', [ 'course' => $course->id, 'completion' => 2, 'completionentries' => 0 ]); $cm1 = \cm_info::create(get_coursemodule_from_instance('data', $data1->id)); $cm2 = \cm_info::create(get_coursemodule_from_instance('data', $data2->id)); // Data for the stdClass input type. // This type of input would occur when checking the default completion rules for an activity type, where we don't have // any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info. $moddefaults = new \stdClass(); $moddefaults->customdata = ['customcompletionrules' => ['completionentries' => 3]]; $moddefaults->completion = 2; $activeruledescriptions = [get_string('completionentriesdesc', 'data', 3)]; $this->assertEquals(mod_data_get_completion_active_rule_descriptions($cm1), $activeruledescriptions); $this->assertEquals(mod_data_get_completion_active_rule_descriptions($cm2), []); $this->assertEquals(mod_data_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions); $this->assertEquals(mod_data_get_completion_active_rule_descriptions(new \stdClass()), []); } /** * An unknown event type should not change the data instance. */ public function test_mod_data_core_calendar_event_timestart_updated_unknown_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $datagenerator = $generator->get_plugin_generator('mod_data'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $data = $datagenerator->create_instance(['course' => $course->id]); $data->timeavailablefrom = $timeopen; $data->timeavailableto = $timeclose; $DB->update_record('data', $data); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'data', 'instance' => $data->id, 'eventtype' => DATA_EVENT_TYPE_OPEN . "SOMETHING ELSE", 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); mod_data_core_calendar_event_timestart_updated($event, $data); $data = $DB->get_record('data', ['id' => $data->id]); $this->assertEquals($timeopen, $data->timeavailablefrom); $this->assertEquals($timeclose, $data->timeavailableto); } /** * A DATA_EVENT_TYPE_OPEN event should update the timeavailablefrom property of the data activity. */ public function test_mod_data_core_calendar_event_timestart_updated_open_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $datagenerator = $generator->get_plugin_generator('mod_data'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeopen = $timeopen - DAYSECS; $data = $datagenerator->create_instance(['course' => $course->id]); $data->timeavailablefrom = $timeopen; $data->timeavailableto = $timeclose; $data->timemodified = $timemodified; $DB->update_record('data', $data); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'data', 'instance' => $data->id, 'eventtype' => DATA_EVENT_TYPE_OPEN, 'timestart' => $newtimeopen, 'timeduration' => 86400, 'visible' => 1 ]); // Trigger and capture the event when adding a contact. $sink = $this->redirectEvents(); mod_data_core_calendar_event_timestart_updated($event, $data); $triggeredevents = $sink->get_events(); $moduleupdatedevents = array_filter($triggeredevents, function($e) { return is_a($e, 'core\event\course_module_updated'); }); $data = $DB->get_record('data', ['id' => $data->id]); // Ensure the timeavailablefrom property matches the event timestart. $this->assertEquals($newtimeopen, $data->timeavailablefrom); // Ensure the timeavailableto isn't changed. $this->assertEquals($timeclose, $data->timeavailableto); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $data->timemodified); // Confirm that a module updated event is fired when the module is changed. $this->assertNotEmpty($moduleupdatedevents); } /** * A DATA_EVENT_TYPE_CLOSE event should update the timeavailableto property of the data activity. */ public function test_mod_data_core_calendar_event_timestart_updated_close_event(): void { global $CFG, $DB; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $datagenerator = $generator->get_plugin_generator('mod_data'); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $timemodified = 1; $newtimeclose = $timeclose + DAYSECS; $data = $datagenerator->create_instance(['course' => $course->id]); $data->timeavailablefrom = $timeopen; $data->timeavailableto = $timeclose; $data->timemodified = $timemodified; $DB->update_record('data', $data); // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'data', 'instance' => $data->id, 'eventtype' => DATA_EVENT_TYPE_CLOSE, 'timestart' => $newtimeclose, 'timeduration' => 86400, 'visible' => 1 ]); // Trigger and capture the event when adding a contact. $sink = $this->redirectEvents(); mod_data_core_calendar_event_timestart_updated($event, $data); $triggeredevents = $sink->get_events(); $moduleupdatedevents = array_filter($triggeredevents, function($e) { return is_a($e, 'core\event\course_module_updated'); }); $data = $DB->get_record('data', ['id' => $data->id]); // Ensure the timeavailableto property matches the event timestart. $this->assertEquals($newtimeclose, $data->timeavailableto); // Ensure the timeavailablefrom isn't changed. $this->assertEquals($timeopen, $data->timeavailablefrom); // Ensure the timemodified property has been changed. $this->assertNotEquals($timemodified, $data->timemodified); // Confirm that a module updated event is fired when the module is changed. $this->assertNotEmpty($moduleupdatedevents); } /** * An unknown event type should not have any limits. */ public function test_mod_data_core_calendar_get_valid_event_timestart_range_unknown_event(): void { global $CFG; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $data = new \stdClass(); $data->timeavailablefrom = $timeopen; $data->timeavailableto = $timeclose; // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'data', 'instance' => 1, 'eventtype' => DATA_EVENT_TYPE_OPEN . "SOMETHING ELSE", 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data); $this->assertNull($min); $this->assertNull($max); } /** * The open event should be limited by the data's timeclose property, if it's set. */ public function test_mod_data_core_calendar_get_valid_event_timestart_range_open_event(): void { global $CFG; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $data = new \stdClass(); $data->timeavailablefrom = $timeopen; $data->timeavailableto = $timeclose; // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'data', 'instance' => 1, 'eventtype' => DATA_EVENT_TYPE_OPEN, 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); // The max limit should be bounded by the timeclose value. list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data); $this->assertNull($min); $this->assertEquals($timeclose, $max[0]); // No timeclose value should result in no upper limit. $data->timeavailableto = 0; list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data); $this->assertNull($min); $this->assertNull($max); } /** * The close event should be limited by the data's timeavailablefrom property, if it's set. */ public function test_mod_data_core_calendar_get_valid_event_timestart_range_close_event(): void { global $CFG; require_once($CFG->dirroot . "/calendar/lib.php"); $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $timeopen = time(); $timeclose = $timeopen + DAYSECS; $data = new \stdClass(); $data->timeavailablefrom = $timeopen; $data->timeavailableto = $timeclose; // Create a valid event. $event = new \calendar_event([ 'name' => 'Test event', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => 2, 'modulename' => 'data', 'instance' => 1, 'eventtype' => DATA_EVENT_TYPE_CLOSE, 'timestart' => 1, 'timeduration' => 86400, 'visible' => 1 ]); // The max limit should be bounded by the timeclose value. list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data); $this->assertEquals($timeopen, $min[0]); $this->assertNull($max); // No timeavailableto value should result in no upper limit. $data->timeavailablefrom = 0; list ($min, $max) = mod_data_core_calendar_get_valid_event_timestart_range($event, $data); $this->assertNull($min); $this->assertNull($max); } /** * A user who does not have capabilities to add events to the calendar should be able to create an database. */ public function test_creation_with_no_calendar_capabilities(): void { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(); $context = \context_course::instance($course->id); $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher'); $roleid = self::getDataGenerator()->create_role(); self::getDataGenerator()->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); $generator = self::getDataGenerator()->get_plugin_generator('mod_data'); // Create an instance as a user without the calendar capabilities. $this->setUser($user); $time = time(); $params = array( 'course' => $course->id, 'timeavailablefrom' => $time + 200, 'timeavailableto' => $time + 2000, 'timeviewfrom' => $time + 400, 'timeviewto' => $time + 2000, ); $generator->create_instance($params); } /** * Test for data_generate_default_template(). This method covers different scenarios for checking when the returned value * is empty or not, but doesn't check if the content has the expected value when it's not empty. * * @covers ::data_generate_default_template */ public function test_data_generate_default_template(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Check the result is empty when $data and/or $template are null. $nullactivity = null; $result = data_generate_default_template($nullactivity, 'listtemplate', 0, false, false); $this->assertEmpty($result); $result = data_generate_default_template($activity, null, 0, false, false); $this->assertEmpty($result); $result = data_generate_default_template($nullactivity, null, 0, false, false); $this->assertEmpty($result); // Check the result is empty when any of the templates that are empty are given. $emptytemplates = [ 'csstemplate', 'jstemplate', 'listtemplateheader', 'listtemplatefooter', 'rsstitletemplate', ]; foreach ($emptytemplates as $emptytemplate) { $result = data_generate_default_template($activity, $emptytemplate, 0, false, false); $this->assertEmpty($result); } $templates = [ 'listtemplate', 'singletemplate', 'asearchtemplate', ]; // Check the result is empty when the database has no fields. foreach ($templates as $template) { $result = data_generate_default_template($activity, $template, 0, false, false); $this->assertEmpty($result); $this->assertEmpty($activity->{$template}); } // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); // Check the result is not empty when the database has no entries. foreach ($templates as $template) { $result = data_generate_default_template($activity, $template, 0, false, false); $this->assertNotEmpty($result); $this->assertEmpty($activity->{$template}); } // Check the result is not empty when the database has no entries and the result is saved when $update = true. foreach ($templates as $template) { $result = data_generate_default_template($activity, $template, 0, false, true); $this->assertNotEmpty($result); $this->assertNotEmpty($activity->{$template}); } } /** * Test for data_replace_field_in_templates(). * * @covers ::data_replace_field_in_templates */ public function test_data_replace_field_in_templates(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $templatecontent = "Field [[myfield]], [[myfield#id]], [[myfield#name]], [[myfield#description]], "; $params = ['course' => $course]; foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { $params[$templatename] = $templatecontent; } $activity = $this->getDataGenerator()->create_module(manager::MODULE, $params); $generator = $this->getDataGenerator()->get_plugin_generator(manager::PLUGINNAME); $fieldrecord = (object)['name' => 'myfield', 'type' => 'text', 'description' => 'This is a field']; $generator->create_field($fieldrecord, $activity); data_replace_field_in_templates($activity, 'myfield', 'newfieldname'); $dbactivity = $DB->get_record(manager::MODULE, ['id' => $activity->id]); $newcontent = "Field [[newfieldname]], [[newfieldname#id]], [[newfieldname#name]], [[newfieldname#description]], "; // Field compatible templates. $this->assertEquals($newcontent, $dbactivity->listtemplate); $this->assertEquals($newcontent, $dbactivity->singletemplate); $this->assertEquals($newcontent, $dbactivity->asearchtemplate); $this->assertEquals($newcontent, $dbactivity->addtemplate); $this->assertEquals($newcontent, $dbactivity->rsstemplate); // Other templates. $this->assertEquals($templatecontent, $dbactivity->listtemplateheader); $this->assertEquals($templatecontent, $dbactivity->listtemplatefooter); $this->assertEquals($templatecontent, $dbactivity->csstemplate); $this->assertEquals($templatecontent, $dbactivity->jstemplate); $this->assertEquals($templatecontent, $dbactivity->rsstitletemplate); } /** * Test for data_append_new_field_to_templates(). * * @covers ::data_append_new_field_to_templates * @dataProvider data_append_new_field_to_templates_provider * @param bool $hasfield if the field is present in the templates * @param bool $hasotherfields if the field is not present in the templates * @param bool $expected the expected return */ public function test_data_append_new_field_to_templates(bool $hasfield, bool $hasotherfields, bool $expected): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $templatecontent = "Template content"; if ($hasfield) { $templatecontent .= "Has [[myfield]]."; } if ($hasotherfields) { $templatecontent .= "And also ##otherfields##."; } $course = $this->getDataGenerator()->create_course(); $params = ['course' => $course]; foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { $params[$templatename] = $templatecontent; } $activity = $this->getDataGenerator()->create_module(manager::MODULE, $params); $result = data_append_new_field_to_templates($activity, 'myfield'); $this->assertEquals($expected, $result); // Check fields with auto add fields. $dbactivity = $DB->get_record(manager::MODULE, ['id' => $activity->id]); if ($hasfield || $hasotherfields) { $this->assertEquals($dbactivity->singletemplate, $templatecontent); $this->assertEquals($dbactivity->addtemplate, $templatecontent); $this->assertEquals($dbactivity->rsstemplate, $templatecontent); } else { $regexp = '|Template content.*\[\[myfield\]\]|'; // We don't want line breaks for the validations. $this->assertMatchesRegularExpression($regexp, str_replace("\n", '', $dbactivity->singletemplate)); $this->assertMatchesRegularExpression($regexp, str_replace("\n", '', $dbactivity->addtemplate)); $this->assertMatchesRegularExpression($regexp, str_replace("\n", '', $dbactivity->rsstemplate)); } // No auto add field templates. $this->assertEquals($dbactivity->asearchtemplate, $templatecontent); $this->assertEquals($dbactivity->listtemplate, $templatecontent); $this->assertEquals($dbactivity->listtemplateheader, $templatecontent); $this->assertEquals($dbactivity->listtemplatefooter, $templatecontent); $this->assertEquals($dbactivity->csstemplate, $templatecontent); $this->assertEquals($dbactivity->jstemplate, $templatecontent); $this->assertEquals($dbactivity->rsstitletemplate, $templatecontent); } /** * Data provider for test_data_append_new_field_to_templates(). * * @return array of scenarios */ public static function data_append_new_field_to_templates_provider(): array { return [ 'Plain template' => [ 'hasfield' => false, 'hasotherfields' => false, 'expected' => true, ], 'Field already present' => [ 'hasfield' => true, 'hasotherfields' => false, 'expected' => false, ], '##otherfields## tag present' => [ 'hasfield' => false, 'hasotherfields' => true, 'expected' => false, ], 'Field already present and ##otherfields## tag present' => [ 'hasfield' => true, 'hasotherfields' => true, 'expected' => false, ], ]; } /** * Test that format that are not supported are raising an exception * * @param string $type * @param string $expected * @covers \data_get_field_new * @dataProvider format_parser_provider */ public function test_create_field(string $type, string $expected): void { $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id] ); if ($expected === 'exception') { $this->expectException(\moodle_exception::class); } $field = data_get_field_new($type, $data); $this->assertStringContainsString($expected, get_class($field)); } /** * Data provider for test_format_parser * * @return array[] */ public static function format_parser_provider(): array { return [ 'text' => [ 'type' => 'text', 'expected' => 'data_field_text', ], 'picture' => [ 'type' => 'picture', 'expected' => 'data_field_picture', ], 'wrong type' => [ 'type' => '../wrongformat123', 'expected' => 'exception', ], ]; } } tests/preset_importer_test.php000064400000045706152265517740012733 0ustar00. namespace mod_data; use mod_data\local\importer\preset_existing_importer; use mod_data\local\importer\preset_importer; /** * Preset importer tests class for mod_data. * * @package mod_data * @category test * @copyright 2022 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data\local\importer\preset_importer */ final class preset_importer_test extends \advanced_testcase { /** * Data provider for build providers for test_needs_mapping and test_set_affected_fields. * * @return array[] */ public static function preset_importer_provider(): array { // Image gallery preset is: ['title' => 'text', 'description' => 'textarea', 'image' => 'picture']; $titlefield = new \stdClass(); $titlefield->name = 'title'; $titlefield->type = 'text'; $descfield = new \stdClass(); $descfield->name = 'description'; $descfield->type = 'textarea'; $imagefield = new \stdClass(); $imagefield->name = 'image'; $imagefield->type = 'picture'; $difffield = new \stdClass(); $difffield->name = 'title'; $difffield->type = 'textarea'; $newfield = new \stdClass(); $newfield->name = 'number'; $newfield->type = 'number'; return [ 'Empty database / Empty importer' => [ 'currentfields' => [], 'newfields' => [], 'pluginname' => '', ], 'Empty database / Importer with fields' => [ 'currentfields' => [], 'newfields' => [$titlefield, $descfield, $imagefield], 'pluginname' => 'imagegallery', ], 'Database with fields / Empty importer' => [ 'currentfields' => [$titlefield, $descfield, $imagefield], 'newfields' => [], 'pluginname' => '', ], 'Same fields' => [ 'currentfields' => [$titlefield, $descfield, $imagefield], 'newfields' => [$titlefield, $descfield, $imagefield], 'pluginname' => 'imagegallery', ], 'Fields to create' => [ 'currentfields' => [$titlefield, $descfield], 'newfields' => [$titlefield, $descfield, $imagefield], 'pluginname' => 'imagegallery', ], 'Fields to remove' => [ 'currentfields' => [$titlefield, $descfield, $imagefield, $difffield], 'newfields' => [$titlefield, $descfield, $imagefield], 'pluginname' => 'imagegallery', ], 'Fields to update' => [ 'currentfields' => [$difffield, $descfield, $imagefield], 'newfields' => [$titlefield, $descfield, $imagefield], 'pluginname' => 'imagegallery', ], 'Fields to create, remove and update' => [ 'currentfields' => [$titlefield, $descfield, $imagefield, $difffield], 'newfields' => [$titlefield, $descfield, $newfield], 'pluginname' => '', ], ]; } /** * Data provider for needs_mapping(). * * @return array[] */ public static function needs_mapping_provider(): array { $basedprovider = static::preset_importer_provider(); $basedprovider['Empty database / Empty importer']['expectedresult'] = false; $basedprovider['Empty database / Importer with fields']['expectedresult'] = false; $basedprovider['Database with fields / Empty importer']['expectedresult'] = true; $basedprovider['Same fields']['expectedresult'] = false; $basedprovider['Fields to create']['expectedresult'] = true; $basedprovider['Fields to remove']['expectedresult'] = true; $basedprovider['Fields to update']['expectedresult'] = true; $basedprovider['Fields to create, remove and update']['expectedresult'] = true; return $basedprovider; } /** * Test for needs_mapping method. * * @dataProvider needs_mapping_provider * @covers ::needs_mapping * * @param array $currentfields Fields of the current activity. * @param array $newfields Fields to be imported. * @param string $pluginname The plugin preset to be imported. * @param bool $expectedresult Expected exception. */ public function test_needs_mapping( array $currentfields, array $newfields, string $pluginname, bool $expectedresult ): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($currentfields as $field) { $plugingenerator->create_field($field, $activity); } $manager = manager::create_from_instance($activity); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($newfields as $field) { $plugingenerator->create_field($field, $presetactivity); } $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); $this->assertEquals($savedimporter->needs_mapping(), $expectedresult); // Create presets and importers. if ($pluginname) { $plugin = preset::create_from_plugin(null, $pluginname); $pluginimporter = new preset_existing_importer($manager, '/' . $pluginname); $this->assertEquals($pluginimporter->needs_mapping(), $expectedresult); } } /** * Data provider for test_set_affected_fields(). * * @return array[] */ public static function set_affected_provider(): array { $basedprovider = static::preset_importer_provider(); $basedprovider['Empty database / Empty importer']['fieldstocreate'] = 0; $basedprovider['Empty database / Empty importer']['fieldstoremove'] = 0; $basedprovider['Empty database / Empty importer']['fieldstoupdate'] = 0; $basedprovider['Empty database / Importer with fields']['fieldstocreate'] = 3; $basedprovider['Empty database / Importer with fields']['fieldstoremove'] = 0; $basedprovider['Empty database / Importer with fields']['fieldstoupdate'] = 0; $basedprovider['Database with fields / Empty importer']['fieldstocreate'] = 0; $basedprovider['Database with fields / Empty importer']['fieldstoremove'] = 3; $basedprovider['Database with fields / Empty importer']['fieldstoupdate'] = 0; $basedprovider['Same fields']['fieldstocreate'] = 0; $basedprovider['Same fields']['fieldstoremove'] = 0; $basedprovider['Same fields']['fieldstoupdate'] = 3; $basedprovider['Fields to create']['fieldstocreate'] = 1; $basedprovider['Fields to create']['fieldstoremove'] = 0; $basedprovider['Fields to create']['fieldstoupdate'] = 2; $basedprovider['Fields to remove']['fieldstocreate'] = 0; $basedprovider['Fields to remove']['fieldstoremove'] = 1; $basedprovider['Fields to remove']['fieldstoupdate'] = 3; $basedprovider['Fields to update']['fieldstocreate'] = 1; $basedprovider['Fields to update']['fieldstoremove'] = 1; $basedprovider['Fields to update']['fieldstoupdate'] = 2; $basedprovider['Fields to create, remove and update']['fieldstocreate'] = 1; $basedprovider['Fields to create, remove and update']['fieldstoremove'] = 2; $basedprovider['Fields to create, remove and update']['fieldstoupdate'] = 2; return $basedprovider; } /** * Test for set_affected_fields method. * * @dataProvider set_affected_provider * @covers ::set_affected_fields * * @param array $currentfields Fields of the current activity. * @param array $newfields Fields to be imported. * @param string $pluginname The plugin preset to be imported. * @param int $fieldstocreate Expected number of fields on $fieldstocreate. * @param int $fieldstoremove Expected number of fields on $fieldstoremove. * @param int $fieldstoupdate Expected number of fields on $fieldstoupdate. */ public function test_set_affected_fields( array $currentfields, array $newfields, string $pluginname, int $fieldstocreate, int $fieldstoremove, int $fieldstoupdate ): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($currentfields as $field) { $plugingenerator->create_field($field, $activity); } $manager = manager::create_from_instance($activity); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($newfields as $field) { $plugingenerator->create_field($field, $presetactivity); } $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); $this->assertEquals(count($savedimporter->fieldstoremove), $fieldstoremove); $this->assertEquals(count($savedimporter->fieldstocreate), $fieldstocreate); $this->assertEquals(count($savedimporter->fieldstoupdate), $fieldstoupdate); // Create presets and importers. if ($pluginname) { $plugin = preset::create_from_plugin(null, $pluginname); $pluginimporter = new preset_existing_importer($manager, '/' . $pluginname); $this->assertEquals(count($pluginimporter->fieldstoremove), $fieldstoremove); $this->assertEquals(count($pluginimporter->fieldstocreate), $fieldstocreate); $this->assertEquals(count($pluginimporter->fieldstoupdate), $fieldstoupdate); } } /** * Test for get_mapping_information method. * * @dataProvider set_affected_provider * @covers ::get_mapping_information * * @param array $currentfields Fields of the current activity. * @param array $newfields Fields to be imported. * @param string $pluginname The plugin preset to be imported. * @param int $fieldstocreate Expected number of fields on $fieldstocreate. * @param int $fieldstoremove Expected number of fields on $fieldstoremove. * @param int $fieldstoupdate Expected number of fields on $fieldstoupdate. */ public function test_get_mapping_information( array $currentfields, array $newfields, string $pluginname, int $fieldstocreate, int $fieldstoremove, int $fieldstoupdate ): void { global $USER; // xdebug_break(); $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($currentfields as $field) { $plugingenerator->create_field($field, $activity); } $manager = manager::create_from_instance($activity); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($newfields as $field) { $plugingenerator->create_field($field, $presetactivity); } $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); $information = $savedimporter->get_mapping_information(); $this->assertEquals($savedimporter->needs_mapping(), $information['needsmapping']); $this->assertEquals(count($savedimporter->fieldstoremove), $fieldstoremove); $this->assertEquals(count($savedimporter->fieldstocreate), $fieldstocreate); $this->assertEquals(count($savedimporter->fieldstoupdate), $fieldstoupdate); // Create presets and importers. if ($pluginname) { $plugin = preset::create_from_plugin(null, $pluginname); $pluginimporter = new preset_existing_importer($manager, '/' . $pluginname); $information = $pluginimporter->get_mapping_information(); $this->assertEquals($pluginimporter->needs_mapping(), $information['needsmapping']); $this->assertEquals(count($pluginimporter->fieldstoremove), $fieldstoremove); $this->assertEquals(count($pluginimporter->fieldstocreate), $fieldstocreate); $this->assertEquals(count($pluginimporter->fieldstoupdate), $fieldstoupdate); } } /** * Data provider for get_field_names(). * * @return array[] */ public static function get_field_names_provider(): array { return [ 'Empty list' => [ 'fields' => [], 'expected' => '', ], 'List with one field' => [ 'fields' => ['fieldname' => 'text'], 'expected' => 'fieldname', ], 'List of fields with same type' => [ 'fields' => ['textfield' => 'text', 'other' => 'text'], 'expected' => 'textfield, other', ], 'List of fields with different type' => [ 'fields' => ['textfield' => 'text', 'number' => 'number'], 'expected' => 'textfield, number', ], ]; } /** * Test for get_field_names method. * * @dataProvider get_field_names_provider * @covers ::get_field_names * * @param array $fields List of fields to get the names from. * @param string $expected The list of field names expected. */ public function test_get_field_names(array $fields, string $expected): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); foreach ($fields as $fieldname => $fieldtype) { $newfield = new \stdClass(); $newfield->name = $fieldname; $newfield->type = $fieldtype; $createdfield = $plugingenerator->create_field($newfield, $presetactivity); } $manager = manager::create_from_instance($presetactivity); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); $this->assertEquals($expected, $savedimporter->get_field_names($manager->get_field_records())); } /** * Test for create_from_plugin_or_directory creation static method. * * @covers ::create_from_plugin_or_directory * */ public function test_create_from_plugin_or_directory(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); // A plugin preset returns an instance of preset_existing_importer. $preset = preset_importer::create_from_plugin_or_directory($manager, '/imagegallery'); $this->assertInstanceOf('\mod_data\local\importer\preset_existing_importer', $preset); // A saved preset returns an instance of preset_existing_importer. $preset = preset_importer::create_from_plugin_or_directory($manager, $USER->id . '/Testing preset name'); $this->assertInstanceOf('\mod_data\local\importer\preset_existing_importer', $preset); // An empty preset name throws an exception. $this->expectException('moodle_exception'); try { preset_importer::create_from_plugin_or_directory($manager, ''); } finally { // A non-existing preset name throws an exception. $this->expectException('moodle_exception'); preset_importer::create_from_plugin_or_directory($manager, $USER->id . '/Non-existing'); } } } tests/entries_import_test.php000064400000045775152265517740012561 0ustar00. namespace mod_data; use coding_exception; use dml_exception; use mod_data\local\importer\csv_entries_importer; use moodle_exception; use zip_archive; /** * Unit tests for import.php. * * @package mod_data * @category test * @covers \mod_data\local\importer\entries_importer * @covers \mod_data\local\importer\csv_entries_importer * @copyright 2019 Tobias Reischmann * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entries_import_test extends \advanced_testcase { /** * Set up function. */ protected function setUp(): void { parent::setUp(); global $CFG; require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->dirroot . '/lib/datalib.php'); require_once($CFG->dirroot . '/lib/csvlib.class.php'); require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); require_once($CFG->dirroot . '/mod/data/tests/generator/lib.php'); } /** * Get the test data. * In this instance we are setting up database records to be used in the unit tests. * * @return array */ protected function get_test_data(): array { $this->resetAfterTest(true); $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->setUser($teacher); $student = $this->getDataGenerator()->create_and_enrol($course, 'student', array('username' => 'student')); $data = $generator->create_instance(array('course' => $course->id)); $cm = get_coursemodule_from_instance('data', $data->id); // Add fields. $fieldrecord = new \stdClass(); $fieldrecord->name = 'ID'; // Identifier of the records for testing. $fieldrecord->type = 'number'; $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'Param2'; $fieldrecord->type = 'text'; $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'filefield'; $fieldrecord->type = 'file'; $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'picturefield'; $fieldrecord->type = 'picture'; $generator->create_field($fieldrecord, $data); return [ 'teacher' => $teacher, 'student' => $student, 'data' => $data, 'cm' => $cm, ]; } /** * Test uploading entries for a data instance without userdata. * * @throws dml_exception */ public function test_import(): void { [ 'data' => $data, 'cm' => $cm, 'teacher' => $teacher, ] = $this->get_test_data(); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import.csv'), 'test_data_import.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); // No userdata is present in the file: Fallback is to assign the uploading user as author. $expecteduserids = array(); $expecteduserids[1] = $teacher->id; $expecteduserids[2] = $teacher->id; $records = $this->get_data_records($data->id); $this->assertCount(2, $records); foreach ($records as $record) { $identifier = $record->items['ID']->content; $this->assertEquals($expecteduserids[$identifier], $record->userid); } } /** * Test uploading entries for a data instance with userdata. * * At least one entry has an identifiable user, which is assigned as author. * * @throws dml_exception */ public function test_import_with_userdata(): void { [ 'data' => $data, 'cm' => $cm, 'teacher' => $teacher, 'student' => $student, ] = $this->get_test_data(); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_userdata.csv'), 'test_data_import_with_userdata.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $expecteduserids = array(); $expecteduserids[1] = $student->id; // User student exists and is assigned as author. $expecteduserids[2] = $teacher->id; // User student2 does not exist. Fallback is the uploading user. $records = $this->get_data_records($data->id); $this->assertCount(2, $records); foreach ($records as $record) { $identifier = $record->items['ID']->content; $this->assertEquals($expecteduserids[$identifier], $record->userid); } } /** * Test uploading entries for a data instance with userdata and a defined field 'Username'. * * This should test the corner case, in which a user has defined a data fields, which has the same name * as the current lang string for username. In that case, the first Username entry is used for the field. * The second one is used to identify the author. * * @throws coding_exception * @throws dml_exception */ public function test_import_with_field_username(): void { [ 'data' => $data, 'cm' => $cm, 'teacher' => $teacher, 'student' => $student, ] = $this->get_test_data(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Add username field. $fieldrecord = new \stdClass(); $fieldrecord->name = 'Username'; $fieldrecord->type = 'text'; $generator->create_field($fieldrecord, $data); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_field_username.csv'), 'test_data_import_with_field_username.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $expecteduserids = array(); $expecteduserids[1] = $student->id; // User student exists and is assigned as author. $expecteduserids[2] = $teacher->id; // User student2 does not exist. Fallback is the uploading user. $expecteduserids[3] = $student->id; // User student exists and is assigned as author. $expectedcontent = array(); $expectedcontent[1] = array( 'Username' => 'otherusername1', 'Param2' => 'My first entry', ); $expectedcontent[2] = array( 'Username' => 'otherusername2', 'Param2' => 'My second entry', ); $expectedcontent[3] = array( 'Username' => 'otherusername3', 'Param2' => 'My third entry', ); $records = $this->get_data_records($data->id); $this->assertCount(3, $records); foreach ($records as $record) { $identifier = $record->items['ID']->content; $this->assertEquals($expecteduserids[$identifier], $record->userid); foreach ($expectedcontent[$identifier] as $field => $value) { $this->assertEquals($value, $record->items[$field]->content, "The value of field \"$field\" for the record at position \"$identifier\" " . "which is \"{$record->items[$field]->content}\" does not match the expected value \"$value\"."); } } } /** * Test uploading entries for a data instance with a field 'Username' but only one occurrence in the csv file. * * This should test the corner case, in which a user has defined a data fields, which has the same name * as the current lang string for username. In that case, the only Username entry is used for the field. * The author should not be set. * * @throws coding_exception * @throws dml_exception */ public function test_import_with_field_username_without_userdata(): void { [ 'data' => $data, 'cm' => $cm, 'teacher' => $teacher, 'student' => $student, ] = $this->get_test_data(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Add username field. $fieldrecord = new \stdClass(); $fieldrecord->name = 'Username'; $fieldrecord->type = 'text'; $generator->create_field($fieldrecord, $data); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_userdata.csv'), 'test_data_import_with_userdata.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); // No userdata is present in the file: Fallback is to assign the uploading user as author. $expecteduserids = array(); $expecteduserids[1] = $teacher->id; $expecteduserids[2] = $teacher->id; $expectedcontent = array(); $expectedcontent[1] = array( 'Username' => 'student', 'Param2' => 'My first entry', ); $expectedcontent[2] = array( 'Username' => 'student2', 'Param2' => 'My second entry', ); $records = $this->get_data_records($data->id); $this->assertCount(2, $records); foreach ($records as $record) { $identifier = $record->items['ID']->content; $this->assertEquals($expecteduserids[$identifier], $record->userid); foreach ($expectedcontent[$identifier] as $field => $value) { $this->assertEquals($value, $record->items[$field]->content, "The value of field \"$field\" for the record at position \"$identifier\" " . "which is \"{$record->items[$field]->content}\" does not match the expected value \"$value\"."); } } } /** * Data provider for {@see test_import_without_approved} * * @return array[] */ public static function import_without_approved_provider(): array { return [ 'Teacher can approve entries' => ['teacher', [1, 1]], 'Student cannot approve entries' => ['student', [0, 0]], ]; } /** * Test importing file without approved status column * * @param string $user * @param int[] $expected * * @dataProvider import_without_approved_provider */ public function test_import_without_approved(string $user, array $expected): void { $testdata = $this->get_test_data(); ['data' => $data, 'cm' => $cm] = $testdata; $this->setUser($testdata[$user]); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import.csv'), 'test_data_import.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $records = $this->get_data_records($data->id); $this->assertEquals($expected, array_column($records, 'approved')); } /** * Data provider for {@see test_import_with_approved} * * @return array[] */ public static function import_with_approved_provider(): array { return [ 'Teacher can approve entries' => ['teacher', [1, 0]], 'Student cannot approve entries' => ['student', [0, 0]], ]; } /** * Test importing file with approved status column * * @param string $user * @param int[] $expected * * @dataProvider import_with_approved_provider */ public function test_import_with_approved(string $user, array $expected): void { $testdata = $this->get_test_data(); ['data' => $data, 'cm' => $cm] = $testdata; $this->setUser($testdata[$user]); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_approved.csv'), 'test_data_import_with_approved.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $records = $this->get_data_records($data->id); $this->assertEquals($expected, array_column($records, 'approved')); } /** * Tests the import including files from a zip archive. */ public function test_import_with_files(): void { [ 'data' => $data, 'cm' => $cm, ] = $this->get_test_data(); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_files.zip'), 'test_data_import_with_files.zip'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $records = $this->get_data_records($data->id); $ziparchive = new zip_archive(); $ziparchive->open(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_files.zip')); $importedcontent = array_values($records)[0]->items; $this->assertEquals(17, $importedcontent['ID']->content); $this->assertEquals('samplefile.png', $importedcontent['filefield']->content); $this->assertEquals('samplepicture.png', $importedcontent['picturefield']->content); // We now check if content of imported file from zip content is identical to the content of the file // stored in the mod_data record in the field 'filefield'. $fileindex = array_values(array_map(fn($file) => $file->index, array_filter($ziparchive->list_files(), fn($file) => $file->pathname === 'files/samplefile.png')))[0]; $filestream = $ziparchive->get_stream($fileindex); $filefield = data_get_field_from_name('filefield', $data); $filefieldfilecontent = fread($filestream, $ziparchive->get_info($fileindex)->size); $this->assertEquals($filefield->get_file(array_keys($records)[0])->get_content(), $filefieldfilecontent); fclose($filestream); // We now check if content of imported picture from zip content is identical to the content of the picture file // stored in the mod_data record in the field 'picturefield'. $fileindex = array_values(array_map(fn($file) => $file->index, array_filter($ziparchive->list_files(), fn($file) => $file->pathname === 'files/samplepicture.png')))[0]; $filestream = $ziparchive->get_stream($fileindex); $filefield = data_get_field_from_name('picturefield', $data); $filefieldfilecontent = fread($filestream, $ziparchive->get_info($fileindex)->size); $this->assertEquals($filefield->get_file(array_keys($records)[0])->get_content(), $filefieldfilecontent); fclose($filestream); $this->assertCount(1, $importer->get_added_records_messages()); $ziparchive->close(); } /** * Tests the import including files from a zip archive. */ public function test_import_with_files_missing_file(): void { [ 'data' => $data, 'cm' => $cm, ] = $this->get_test_data(); $importer = new csv_entries_importer(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_files_missing_file.zip'), 'test_data_import_with_files_missing_file.zip'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $records = $this->get_data_records($data->id); $ziparchive = new zip_archive(); $ziparchive->open(self::get_fixture_path(__NAMESPACE__, 'test_data_import_with_files_missing_file.zip')); $importedcontent = array_values($records)[0]->items; $this->assertEquals(17, $importedcontent['ID']->content); $this->assertFalse(isset($importedcontent['filefield'])); $this->assertEquals('samplepicture.png', $importedcontent['picturefield']->content); $this->assertCount(1, $importer->get_added_records_messages()); $ziparchive->close(); } /** * Returns the records of the data instance. * * Each records has an item entry, which contains all fields associated with this item. * Each fields has the parameters name, type and content. * * @param int $dataid Id of the data instance. * @return array The records of the data instance. * @throws dml_exception */ private function get_data_records(int $dataid): array { global $DB; $records = $DB->get_records('data_records', ['dataid' => $dataid]); foreach ($records as $record) { $sql = 'SELECT f.name, f.type, con.content FROM {data_content} con JOIN {data_fields} f ON con.fieldid = f.id WHERE con.recordid = :recordid'; $items = $DB->get_records_sql($sql, array('recordid' => $record->id)); $record->items = $items; } return $records; } /** * Tests if the amount of imported records is counted properly. * * @dataProvider get_added_record_messages_provider * @param string $datafilecontent the content of the datafile to test as string * @param int $expectedcount the expected count of messages depending on the datafile content */ public function test_get_added_record_messages(string $datafilecontent, int $expectedcount): void { [ 'data' => $data, 'cm' => $cm, ] = $this->get_test_data(); // First we need to create the zip file from the provided data. $tmpdir = make_request_directory(); $datafile = $tmpdir . '/entries_import_test_datafile_tmp_' . time() . '.csv'; file_put_contents($datafile, $datafilecontent); $importer = new csv_entries_importer($datafile, 'testdatafile.csv'); $importer->import_csv($cm, $data, 'UTF-8', 'comma'); $this->assertEquals($expectedcount, count($importer->get_added_records_messages())); } /** * Data provider method for self::test_get_added_record_messages. * * @return array data for testing */ public static function get_added_record_messages_provider(): array { return [ 'only header' => [ 'datafilecontent' => 'ID,Param2,filefield,picturefield' . PHP_EOL, 'expectedcount' => 0 // One line is being assumed to be the header. ], 'one record' => [ 'datafilecontent' => 'ID,Param2,filefield,picturefield' . PHP_EOL . '5,"some short text",testfilename.pdf,testpicture.png', 'expectedcount' => 1 ], 'two records' => [ 'datafilecontent' => 'ID,Param2,filefield,picturefield' . PHP_EOL . '5,"some short text",testfilename.pdf,testpicture.png' . PHP_EOL . '3,"other text",testfilename2.pdf,testpicture2.png', 'expectedcount' => 2 ], ]; } } tests/entries_export_test.php000064400000026512152265517740012554 0ustar00. namespace mod_data; use context_module; use mod_data\local\exporter\csv_entries_exporter; use mod_data\local\exporter\ods_entries_exporter; use mod_data\local\exporter\utils; /** * Unit tests for exporting entries. * * @package mod_data * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entries_export_test extends \advanced_testcase { /** * Get the test data. * * In this instance we are setting up database records to be used in the unit tests. * * @return array of test instances */ protected function get_test_data(): array { $this->resetAfterTest(true); /** @var \mod_data_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->setUser($teacher); $student = $this->getDataGenerator()->create_and_enrol($course, 'student', ['username' => 'student']); $data = $generator->create_instance(['course' => $course->id]); $cm = get_coursemodule_from_instance('data', $data->id); // Add fields. $fieldrecord = new \stdClass(); $fieldrecord->name = 'numberfield'; // Identifier of the records for testing. $fieldrecord->type = 'number'; $numberfield = $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'textfield'; $fieldrecord->type = 'text'; $textfield = $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'filefield1'; $fieldrecord->type = 'file'; $filefield1 = $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'filefield2'; $fieldrecord->type = 'file'; $filefield2 = $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'picturefield'; $fieldrecord->type = 'picture'; $picturefield = $generator->create_field($fieldrecord, $data); $contents[$numberfield->field->id] = '3'; $contents[$textfield->field->id] = 'a simple text'; $contents[$filefield1->field->id] = 'samplefile.png'; $contents[$filefield2->field->id] = 'samplefile.png'; $contents[$picturefield->field->id] = ['picturefile.png', 'this picture shows something']; $generator->create_entry($data, $contents); return [ 'teacher' => $teacher, 'student' => $student, 'data' => $data, 'cm' => $cm, ]; } /** * Tests the exporting of the content of a mod_data instance by using the csv_entries_exporter. * * It also includes more general testing of the functionality of the entries_exporter the csv_entries_exporter * is inheriting from. * * @covers \mod_data\local\exporter\entries_exporter * @covers \mod_data\local\exporter\entries_exporter::get_records_count() * @covers \mod_data\local\exporter\entries_exporter::send_file() * @covers \mod_data\local\exporter\csv_entries_exporter * @covers \mod_data\local\exporter\utils::data_exportdata */ public function test_export_csv(): void { global $DB; [ 'data' => $data, 'cm' => $cm, ] = $this->get_test_data(); $exporter = new csv_entries_exporter(); $exporter->set_export_file_name('testexportfile'); $fieldrecords = $DB->get_records('data_fields', ['dataid' => $data->id], 'id'); $fields = []; foreach ($fieldrecords as $fieldrecord) { $fields[] = data_get_field($fieldrecord, $data); } // We select all fields. $selectedfields = array_map(fn($field) => $field->field->id, $fields); $currentgroup = groups_get_activity_group($cm); $context = context_module::instance($cm->id); $exportuser = false; $exporttime = false; $exportapproval = false; $tags = false; // We first test the export without exporting files. // This means file and picture fields will be exported, but only as text (which is the filename), // so we will receive a csv export file. $includefiles = false; utils::data_exportdata($data->id, $fields, $selectedfields, $exporter, $currentgroup, $context, $exportuser, $exporttime, $exportapproval, $tags, $includefiles); $this->assertEquals(file_get_contents(self::get_fixture_path(__NAMESPACE__, 'test_data_export_without_files.csv')), $exporter->send_file(false)); $this->assertEquals(1, $exporter->get_records_count()); // We now test the export including files. This will generate a zip archive. $includefiles = true; $exporter = new csv_entries_exporter(); $exporter->set_export_file_name('testexportfile'); utils::data_exportdata($data->id, $fields, $selectedfields, $exporter, $currentgroup, $context, $exportuser, $exporttime, $exportapproval, $tags, $includefiles); // We now write the zip archive temporary to disc to be able to parse it and assert it has the correct structure. $tmpdir = make_request_directory(); file_put_contents($tmpdir . '/testexportarchive.zip', $exporter->send_file(false)); $ziparchive = new \zip_archive(); $ziparchive->open($tmpdir . '/testexportarchive.zip'); $expectedfilecontents = [ // The test generator for mod_data uses a copy of field/picture/pix/sample.png as sample file content for the // file stored in a file and picture field. // So we expect that this file has to have the same content as sample.png. // Also, the default value for the subdirectory in the zip archive containing the files is 'files/'. 'files/samplefile.png' => 'mod/data/field/picture/pix/sample.png', 'files/samplefile_1.png' => 'mod/data/field/picture/pix/sample.png', 'files/picturefile.png' => 'mod/data/field/picture/pix/sample.png', // By checking that the content of the exported csv is identical to the fixture file it is verified // that the filenames in the csv file correspond to the names of the exported file. // It also verifies that files with identical file names in different fields (or records) will be numbered // automatically (samplefile.png, samplefile_1.png, ...). 'testexportfile.csv' => self::get_fixture_path(__NAMESPACE__, 'test_data_export_with_files.csv'), ]; for ($i = 0; $i < $ziparchive->count(); $i++) { // We here iterate over all files in the zip archive and check if their content is identical to the files // in the $expectedfilecontents array. $filestream = $ziparchive->get_stream($i); $fileinfo = $ziparchive->get_info($i); $filecontent = fread($filestream, $fileinfo->size); $this->assertEquals(file_get_contents($expectedfilecontents[$fileinfo->pathname]), $filecontent); fclose($filestream); } $ziparchive->close(); unlink($tmpdir . '/testexportarchive.zip'); } /** * Tests specific ODS exporting functionality. * * @covers \mod_data\local\exporter\ods_entries_exporter * @covers \mod_data\local\exporter\utils::data_exportdata */ public function test_export_ods(): void { global $DB; [ 'data' => $data, 'cm' => $cm, ] = $this->get_test_data(); $exporter = new ods_entries_exporter(); $exporter->set_export_file_name('testexportfile'); $fieldrecords = $DB->get_records('data_fields', ['dataid' => $data->id], 'id'); $fields = []; foreach ($fieldrecords as $fieldrecord) { $fields[] = data_get_field($fieldrecord, $data); } // We select all fields. $selectedfields = array_map(fn($field) => $field->field->id, $fields); $currentgroup = groups_get_activity_group($cm); $context = context_module::instance($cm->id); $exportuser = false; $exporttime = false; $exportapproval = false; $tags = false; // We first test the export without exporting files. // This means file and picture fields will be exported, but only as text (which is the filename), // so we will receive an ods export file. $includefiles = false; utils::data_exportdata($data->id, $fields, $selectedfields, $exporter, $currentgroup, $context, $exportuser, $exporttime, $exportapproval, $tags, $includefiles); $odsrows = $this->get_ods_rows_content($exporter->send_file(false)); // Check, if the headings match with the first row of the ods file. $i = 0; foreach ($fields as $field) { $this->assertEquals($field->field->name, $odsrows[0][$i]); $i++; } // Check, if the values match with the field values. $this->assertEquals('3', $odsrows[1][0]); $this->assertEquals('a simple text', $odsrows[1][1]); $this->assertEquals('samplefile.png', $odsrows[1][2]); $this->assertEquals('samplefile.png', $odsrows[1][3]); $this->assertEquals('picturefile.png', $odsrows[1][4]); // As the logic of renaming the files and building a zip archive is implemented in entries_exporter class, we do // not need to test this for the ods_entries_exporter, because entries_export_test::test_export_csv already does this. } /** * Helper function to extract the text data as row arrays from an ODS document. * * @param string $content the file content * @return array two-dimensional row/column array with the text content of the first spreadsheet */ private function get_ods_rows_content(string $content): array { $file = tempnam(make_request_directory(), 'ods_'); $filestream = fopen($file, "w"); fwrite($filestream, $content); $reader = new \OpenSpout\Reader\ODS\Reader(); $reader->open($file); /** @var \OpenSpout\Reader\ODS\Sheet[] $sheets */ $sheets = $reader->getSheetIterator(); $rowscellsvalues = []; foreach ($sheets as $sheet) { /** @var \OpenSpout\Common\Entity\Row[] $rows */ $rows = $sheet->getRowIterator(); foreach ($rows as $row) { $cellvalues = []; foreach ($row->getCells() as $cell) { $cellvalues[] = $cell->getValue(); } $rowscellsvalues[] = $cellvalues; } } return $rowscellsvalues; } } tests/template_test.php000064400000124417152265517740011320 0ustar00. namespace mod_data; use context_module; use rating_manager; use stdClass; /** * Template tests class for mod_data. * * @package mod_data * @category test * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data\template */ final class template_test extends \advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/rating/lib.php'); } /** * Test for static create methods. * * @covers ::parse_entries * @dataProvider parse_entries_provider * @param string $templatecontent the template string * @param string $expected expected output * @param string $rolename the user rolename * @param bool $enableexport is portfolio export is enabled * @param bool $approved if the entry is approved * @param bool $enablecomments is comments are enabled * @param bool $enableratings if ratings are enabled * @param array $options extra parser options * @param bool $otherauthor if the entry is from another user */ public function test_parse_entries( string $templatecontent, string $expected, string $rolename = 'editingteacher', bool $enableexport = false, bool $approved = true, bool $enablecomments = false, bool $enableratings = false, array $options = [], bool $otherauthor = false ): void { global $DB, $PAGE; // Comments, tags, approval, user role. $this->resetAfterTest(); $params = ['approval' => true]; // Enable comments. if ($enablecomments) { set_config('usecomments', 1); $params['comments'] = true; $PAGE->reset_theme_and_output(); $PAGE->set_url('/mod/data/view.php'); } $course = $this->getDataGenerator()->create_course(); $params['course'] = $course; $activity = $this->getDataGenerator()->create_module('data', $params); $cm = get_coursemodule_from_id('data', $activity->cmid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $user = $this->getDataGenerator()->create_user(); $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleids[$rolename]); $author = $user; if ($otherauthor) { $user2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $roleids[$rolename]); $author = $user2; } // Generate an entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $fieldrecord = (object)['name' => 'myfield', 'type' => 'text']; $field = $generator->create_field($fieldrecord, $activity); $otherfieldrecord = (object)['name' => 'otherfield', 'type' => 'text']; $otherfield = $generator->create_field($otherfieldrecord, $activity); $this->setUser($user); $entryid = $generator->create_entry( $activity, [ $field->field->id => 'Example entry', $otherfield->field->id => 'Another example', ], 0, ['Cats', 'Dogs'], ['approved' => $approved] ); if ($enableexport) { $this->enable_portfolio($user); } $manager = manager::create_from_instance($activity); $entry = (object)[ 'id' => $entryid, 'approved' => $approved, 'timecreated' => 1657618639, 'timemodified' => 1657618650, 'userid' => $author->id, 'groupid' => 0, 'dataid' => $activity->id, 'picture' => 0, 'firstname' => $author->firstname, 'lastname' => $author->lastname, 'firstnamephonetic' => $author->firstnamephonetic, 'lastnamephonetic' => $author->lastnamephonetic, 'middlename' => $author->middlename, 'alternatename' => $author->alternatename, 'imagealt' => 'PIXEXAMPLE', 'email' => $author->email, ]; $entries = [$entry]; if ($enableratings) { $entries = $this->enable_ratings($context, $activity, $entries, $user); } // Some cooked variables for the regular expression. $replace = [ '{authorfullname}' => fullname($author), '{timeadded}' => userdate($entry->timecreated, get_string('strftimedatemonthabbr', 'langconfig')), '{timemodified}' => userdate($entry->timemodified, get_string('strftimedatemonthabbr', 'langconfig')), '{fieldid}' => $field->field->id, '{fieldname}' => $field->field->name, '{fielddescription}' => $field->field->description, '{entryid}' => $entry->id, '{cmid}' => $cm->id, '{courseid}' => $course->id, '{authorid}' => $author->id ]; $parser = new template($manager, $templatecontent, $options); $result = $parser->parse_entries($entries); // We don't want line breaks for the validations. $result = str_replace("\n", '', $result); $regexp = str_replace(array_keys($replace), array_values($replace), $expected); $this->assertMatchesRegularExpression($regexp, $result); } /** * Data provider for test_parse_entries(). * * @return array of scenarios */ public static function parse_entries_provider(): array { return [ // Teacher scenarios. 'Teacher id tag' => [ 'templatecontent' => 'Some ##id## tag', 'expected' => '|Some {entryid} tag|', 'rolename' => 'editingteacher', ], 'Teacher delete tag' => [ 'templatecontent' => 'Some ##delete## tag', 'expected' => '|Some .*delete.*{entryid}.*sesskey.*Delete.* tag|', 'rolename' => 'editingteacher', ], 'Teacher edit tag' => [ 'templatecontent' => 'Some ##edit## tag', 'expected' => '|Some .*edit.*{entryid}.*sesskey.*Edit.* tag|', 'rolename' => 'editingteacher', ], 'Teacher more tag' => [ 'templatecontent' => 'Some ##more## tag', 'expected' => '|Some .*more.*{cmid}.*rid.*{entryid}.*More.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Teacher more tag with showmore set to false' => [ 'templatecontent' => 'Some ##more## tag', 'expected' => '|Some tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => false], ], 'Teacher moreurl tag' => [ 'templatecontent' => 'Some ##moreurl## tag', 'expected' => '|Some .*/mod/data/view.*{cmid}.*rid.*{entryid}.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Teacher moreurl tag with showmore set to false' => [ 'templatecontent' => 'Some ##moreurl## tag', 'expected' => '|Some .*/mod/data/view.*{cmid}.*rid.*{entryid}.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => false], ], 'Teacher delcheck tag' => [ 'templatecontent' => 'Some ##delcheck## tag', 'expected' => '|Some .*input.*checkbox.*value.*{entryid}.* tag|', 'rolename' => 'editingteacher', ], 'Teacher user tag' => [ 'templatecontent' => 'Some ##user## tag', 'expected' => '|Some .*user/view.*{authorid}.*course.*{courseid}.*{authorfullname}.* tag|', 'rolename' => 'editingteacher', ], 'Teacher userpicture tag' => [ 'templatecontent' => 'Some ##userpicture## tag', 'expected' => '|Some .*user/view.*{authorid}.*course.*{courseid}.* tag|', 'rolename' => 'editingteacher', ], 'Teacher export tag' => [ 'templatecontent' => 'Some ##export## tag', 'expected' => '|Some .*portfolio/add.* tag|', 'rolename' => 'editingteacher', 'enableexport' => true, ], 'Teacher export tag not configured' => [ 'templatecontent' => 'Some ##export## tag', 'expected' => '|Some tag|', 'rolename' => 'editingteacher', 'enableexport' => false, ], 'Teacher timeadded tag' => [ 'templatecontent' => 'Some ##timeadded## tag', 'expected' => '|Some {timeadded} tag|', 'rolename' => 'editingteacher', ], 'Teacher timemodified tag' => [ 'templatecontent' => 'Some ##timemodified## tag', 'expected' => '|Some {timemodified} tag|', 'rolename' => 'editingteacher', ], 'Teacher approve tag approved entry' => [ 'templatecontent' => 'Some ##approve## tag', 'expected' => '|Some tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, ], 'Teacher approve tag disapproved entry' => [ 'templatecontent' => 'Some ##approve## tag', 'expected' => '|Some .*approve.*{entryid}.*sesskey.*Approve.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => false, ], 'Teacher disapprove tag approved entry' => [ 'templatecontent' => 'Some ##disapprove## tag', 'expected' => '|Some .*disapprove.*{entryid}.*sesskey.*Undo approval.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, ], 'Teacher disapprove tag disapproved entry' => [ 'templatecontent' => 'Some ##disapprove## tag', 'expected' => '|Some tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => false, ], 'Teacher approvalstatus tag approved entry' => [ 'templatecontent' => 'Some ##approvalstatus## tag', 'expected' => '|Some tag|', // We do not display the approval status anymore. 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, ], 'Teacher approvalstatus tag disapproved entry' => [ 'templatecontent' => 'Some ##approvalstatus## tag', 'expected' => '|Some .*Pending approval.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => false, ], 'Teacher approvalstatusclass tag approved entry' => [ 'templatecontent' => 'Some ##approvalstatusclass## tag', 'expected' => '|Some approved tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, ], 'Teacher approvalstatusclass tag disapproved entry' => [ 'templatecontent' => 'Some ##approvalstatusclass## tag', 'expected' => '|Some notapproved tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => false, ], 'Teacher tags tag' => [ 'templatecontent' => 'Some ##tags## tag', 'expected' => '|Some .*Cats.* tag|', 'rolename' => 'editingteacher', ], 'Teacher field name tag' => [ 'templatecontent' => 'Some [[myfield]] tag', 'expected' => '|Some .*Example entry.* tag|', 'rolename' => 'editingteacher', ], 'Teacher field#id tag' => [ 'templatecontent' => 'Some [[myfield#id]] tag', 'expected' => '|Some {fieldid} tag|', 'rolename' => 'editingteacher', ], 'Teacher field#name tag' => [ 'templatecontent' => 'Some [[myfield#name]] tag', 'expected' => '|Some {fieldname} tag|', 'rolename' => 'editingteacher', ], 'Teacher field#description tag' => [ 'templatecontent' => 'Some [[myfield#description]] tag', 'expected' => '|Some {fielddescription} tag|', 'rolename' => 'editingteacher', ], 'Teacher comments name tag with comments enabled' => [ 'templatecontent' => 'Some ##comments## tag', 'expected' => '|Some .*Comments.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => true, ], 'Teacher comments name tag with comments disabled' => [ 'templatecontent' => 'Some ##comments## tag', 'expected' => '|Some tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, ], 'Teacher comment forced with comments enables' => [ 'templatecontent' => 'No tags', 'expected' => '|No tags.*Comments.*|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => true, 'enableratings' => false, 'options' => ['comments' => true], ], 'Teacher comment forced without comments enables' => [ 'templatecontent' => 'No tags', 'expected' => '|^No tags$|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['comments' => true], ], 'Teacher adding ratings without ratings configured' => [ 'templatecontent' => 'No tags', 'expected' => '|^No tags$|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['ratings' => true], ], 'Teacher adding ratings with ratings configured' => [ 'templatecontent' => 'No tags', 'expected' => '|^No tags.*Average of ratings|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => true, 'options' => ['ratings' => true], ], 'Teacher actionsmenu tag with default options' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*edit.*{entryid}.*Edit.* .*delete.*{entryid}.*Delete.* tag|', 'rolename' => 'editingteacher', ], 'Teacher actionsmenu tag with default options (check Show more is not there)' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|^Some((?!Show more).)*tag$|', 'rolename' => 'editingteacher', ], 'Teacher actionsmenu tag with show more enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*view.*{cmid}.*rid.*{entryid}.*Show more.* .*Edit.* .*Delete.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => false, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Teacher actionsmenu tag with export enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*Edit.* .*Delete.* .*portfolio/add.* tag|', 'rolename' => 'editingteacher', 'enableexport' => true, ], 'Teacher actionsmenu tag with approved enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*Edit.* .*Delete.* .*disapprove.*{entryid}.*sesskey.*Undo approval.* tag|', 'rolename' => 'editingteacher', 'enableexport' => false, 'approved' => true, ], 'Teacher actionsmenu tag with export, approved and showmore enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*Show more.* .*Edit.* .*Delete.* .*Undo approval.* .*Export to portfolio.* tag|', 'rolename' => 'editingteacher', 'enableexport' => true, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Teacher otherfields tag' => [ 'templatecontent' => 'Some ##otherfields## tag', 'expected' => '|Some .*{fieldname}.*Example entry.*otherfield.*Another example.* tag|', 'rolename' => 'editingteacher', ], 'Teacher otherfields tag with some field in the template' => [ 'templatecontent' => 'Some [[myfield]] and ##otherfields## tag', 'expected' => '|Some .*Example entry.* and .*otherfield.*Another example.* tag|', 'rolename' => 'editingteacher', ], // Student scenarios. 'Student id tag' => [ 'templatecontent' => 'Some ##id## tag', 'expected' => '|Some {entryid} tag|', 'rolename' => 'student', ], 'Student delete tag' => [ 'templatecontent' => 'Some ##delete## tag', 'expected' => '|Some .*delete.*{entryid}.*sesskey.*Delete.* tag|', 'rolename' => 'student', ], 'Student delete tag on other author entry' => [ 'templatecontent' => 'Some ##delete## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => [], 'otherauthor' => true, ], 'Student edit tag' => [ 'templatecontent' => 'Some ##edit## tag', 'expected' => '|Some .*edit.*{entryid}.*sesskey.*Edit.* tag|', 'rolename' => 'student', ], 'Student edit tag on other author entry' => [ 'templatecontent' => 'Some ##edit## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => [], 'otherauthor' => true, ], 'Student more tag' => [ 'templatecontent' => 'Some ##more## tag', 'expected' => '|Some .*more.*{cmid}.*rid.*{entryid}.*More.* tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Student more tag with showmore set to false' => [ 'templatecontent' => 'Some ##more## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => false], ], 'Student moreurl tag' => [ 'templatecontent' => 'Some ##moreurl## tag', 'expected' => '|Some .*/mod/data/view.*{cmid}.*rid.*{entryid}.* tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Student moreurl tag with showmore set to false' => [ 'templatecontent' => 'Some ##moreurl## tag', 'expected' => '|Some .*/mod/data/view.*{cmid}.*rid.*{entryid}.* tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => false], ], 'Student delcheck tag' => [ 'templatecontent' => 'Some ##delcheck## tag', 'expected' => '|Some tag|', 'rolename' => 'student', ], 'Student user tag' => [ 'templatecontent' => 'Some ##user## tag', 'expected' => '|Some .*user/view.*{authorid}.*course.*{courseid}.*{authorfullname}.* tag|', 'rolename' => 'student', ], 'Student userpicture tag' => [ 'templatecontent' => 'Some ##userpicture## tag', 'expected' => '|Some .*user/view.*{authorid}.*course.*{courseid}.* tag|', 'rolename' => 'student', ], 'Student export tag' => [ 'templatecontent' => 'Some ##export## tag', 'expected' => '|Some .*portfolio/add.* tag|', 'rolename' => 'student', 'enableexport' => true, ], 'Student export tag not configured' => [ 'templatecontent' => 'Some ##export## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, ], 'Student export tag on other user entry' => [ 'templatecontent' => 'Some ##export## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => [], 'otherauthor' => true, ], 'Student timeadded tag' => [ 'templatecontent' => 'Some ##timeadded## tag', 'expected' => '|Some {timeadded} tag|', 'rolename' => 'student', ], 'Student timemodified tag' => [ 'templatecontent' => 'Some ##timemodified## tag', 'expected' => '|Some {timemodified} tag|', 'rolename' => 'student', ], 'Student approve tag approved entry' => [ 'templatecontent' => 'Some ##approve## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, ], 'Student approve tag disapproved entry' => [ 'templatecontent' => 'Some ##approve## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => false, ], 'Student disapprove tag approved entry' => [ 'templatecontent' => 'Some ##disapprove## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, ], 'Student disapprove tag disapproved entry' => [ 'templatecontent' => 'Some ##disapprove## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => false, ], 'Student approvalstatus tag approved entry' => [ 'templatecontent' => 'Some ##approvalstatus## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, ], 'Student approvalstatus tag disapproved entry' => [ 'templatecontent' => 'Some ##approvalstatus## tag', 'expected' => '|Some .*Pending approval.* tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => false, ], 'Student approvalstatusclass tag approved entry' => [ 'templatecontent' => 'Some ##approvalstatusclass## tag', 'expected' => '|Some approved tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, ], 'Student approvalstatusclass tag disapproved entry' => [ 'templatecontent' => 'Some ##approvalstatusclass## tag', 'expected' => '|Some notapproved tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => false, ], 'Student tags tag' => [ 'templatecontent' => 'Some ##tags## tag', 'expected' => '|Some .*Cats.* tag|', 'rolename' => 'student', ], 'Student field name tag' => [ 'templatecontent' => 'Some [[myfield]] tag', 'expected' => '|Some .*Example entry.* tag|', 'rolename' => 'student', ], 'Student field#id name tag' => [ 'templatecontent' => 'Some [[myfield#id]] tag', 'expected' => '|Some {fieldid} tag|', 'rolename' => 'student', ], 'Student field#name tag' => [ 'templatecontent' => 'Some [[myfield#name]] tag', 'expected' => '|Some {fieldname} tag|', 'rolename' => 'student', ], 'Student field#description tag' => [ 'templatecontent' => 'Some [[myfield#description]] tag', 'expected' => '|Some {fielddescription} tag|', 'rolename' => 'student', ], 'Student comments name tag with comments enabled' => [ 'templatecontent' => 'Some ##comments## tag', 'expected' => '|Some .*Comments.* tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => true, ], 'Student comments name tag with comments disabled' => [ 'templatecontent' => 'Some ##comments## tag', 'expected' => '|Some tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, ], 'Student comment forced with comments enables' => [ 'templatecontent' => 'No tags', 'expected' => '|No tags.*Comments.*|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => true, 'enableratings' => false, 'options' => ['comments' => true] ], 'Student comment forced without comments enables' => [ 'templatecontent' => 'No tags', 'expected' => '|^No tags$|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['comments' => true] ], 'Student adding ratings without ratings configured' => [ 'templatecontent' => 'No tags', 'expected' => '|^No tags$|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['ratings' => true] ], 'Student adding ratings with ratings configured' => [ 'templatecontent' => 'No tags', 'expected' => '|^No tags$|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, 'enablecomments' => false, 'enableratings' => true, 'options' => ['ratings' => true] ], 'Student actionsmenu tag with default options' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*edit.*{entryid}.*Edit.* .*delete.*{entryid}.*Delete.* tag|', 'rolename' => 'student', ], 'Student actionsmenu tag with default options (check Show more is not there)' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|^Some((?!Show more).)*tag$|', 'rolename' => 'student', ], 'Student actionsmenu tag with show more enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*view.*{cmid}.*rid.*{entryid}.*Show more.* .*Edit.* .*Delete.* tag|', 'rolename' => 'student', 'enableexport' => false, 'approved' => false, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Student actionsmenu tag with export enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*Edit.* .*Delete.* .*portfolio/add.* tag|', 'rolename' => 'student', 'enableexport' => true, ], 'Student actionsmenu tag with approved enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|^Some((?!Approve).)*tag$|', 'rolename' => 'student', 'enableexport' => false, 'approved' => true, ], 'Student actionsmenu tag with export, approved and showmore enabled' => [ 'templatecontent' => 'Some ##actionsmenu## tag', 'expected' => '|Some .*Show more.* .*Edit.* .*Delete.* .*Export to portfolio.* tag|', 'rolename' => 'student', 'enableexport' => true, 'approved' => true, 'enablecomments' => false, 'enableratings' => false, 'options' => ['showmore' => true], ], 'Student otherfields tag' => [ 'templatecontent' => 'Some ##otherfields## tag', 'expected' => '|Some .*{fieldname}.*Example entry.*otherfield.*Another example.* tag|', 'rolename' => 'student', ], 'Student otherfields tag with some field in the template' => [ 'templatecontent' => 'Some [[myfield]] and ##otherfields## tag', 'expected' => '|Some .*Example entry.* and .*otherfield.*Another example.* tag|', 'rolename' => 'student', ], ]; } /** * Create all the necessary data to enable portfolio export in mod_data * * @param stdClass $user the current user record. */ protected function enable_portfolio(stdClass $user) { global $DB; set_config('enableportfolios', 1); $plugin = 'download'; $name = 'Download'; $portfolioinstance = (object) [ 'plugin' => $plugin, 'name' => $name, 'visible' => 1 ]; $portfolioinstance->id = $DB->insert_record('portfolio_instance', $portfolioinstance); $userinstance = (object) [ 'instance' => $portfolioinstance->id, 'userid' => $user->id, 'name' => 'visible', 'value' => 1 ]; $DB->insert_record('portfolio_instance_user', $userinstance); $DB->insert_record('portfolio_log', [ 'portfolio' => $portfolioinstance->id, 'userid' => $user->id, 'caller_class' => 'data_portfolio_caller', 'caller_component' => 'mod_data', 'time' => time(), ]); } /** * Enable the ratings on the database entries. * * @param context_module $context the activity context * @param stdClass $activity the activity record * @param array $entries database entries * @param stdClass $user the current user record * @return stdClass the entries with the rating attribute */ protected function enable_ratings(context_module $context, stdClass $activity, array $entries, stdClass $user) { global $CFG; $ratingoptions = (object)[ 'context' => $context, 'component' => 'mod_data', 'ratingarea' => 'entry', 'items' => $entries, 'aggregate' => RATING_AGGREGATE_AVERAGE, 'scaleid' => $activity->scale, 'userid' => $user->id, 'returnurl' => $CFG->wwwroot . '/mod/data/view.php', 'assesstimestart' => $activity->assesstimestart, 'assesstimefinish' => $activity->assesstimefinish, ]; $rm = new rating_manager(); return $rm->get_ratings($ratingoptions); } /** * Test parse add entry template parsing. * * @covers ::parse_add_entry * @dataProvider parse_add_entry_provider * @param string $templatecontent the template string * @param string $expected expected output * @param bool $newentry if it is a new entry or editing and existing one */ public function test_parse_add_entry( string $templatecontent, string $expected, bool $newentry = false ): void { global $DB, $PAGE; // Comments, tags, approval, user role. $this->resetAfterTest(); $params = ['approval' => true]; $course = $this->getDataGenerator()->create_course(); $params['course'] = $course; $activity = $this->getDataGenerator()->create_module('data', $params); $author = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); // Generate an entry. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $fieldrecord = (object)[ 'name' => 'myfield', 'type' => 'text', 'description' => 'This is a field' ]; $field = $generator->create_field($fieldrecord, $activity); $otherfieldrecord = (object)['name' => 'otherfield', 'type' => 'text']; $otherfield = $generator->create_field($otherfieldrecord, $activity); if ($newentry) { $entryid = null; $entry = null; } else { $entryid = $generator->create_entry( $activity, [ $field->field->id => 'Example entry', $otherfield->field->id => 'Another example', ], 0, ['Cats', 'Dogs'] ); $entry = (object)[ 'd' => $activity->id, 'rid' => $entryid, "field_{$field->field->id}" => "New value", "field_{$otherfield->field->id}" => "Altered value", ]; } $manager = manager::create_from_instance($activity); // Some cooked variables for the regular expression. $replace = [ '{fieldid}' => $field->field->id, '{fieldname}' => $field->field->name, '{fielddescription}' => $field->field->description, '{otherid}' => $otherfield->field->id, ]; $processdata = (object)[ 'generalnotifications' => ['GENERAL'], 'fieldnotifications' => [ $field->field->name => ['FIELD'], $otherfield->field->name => ['OTHERFIELD'], ], ]; $parser = new template($manager, $templatecontent); $result = $parser->parse_add_entry($processdata, $entryid, $entry); // We don't want line breaks for the validations. $result = str_replace("\n", '', $result); $regexp = str_replace(array_keys($replace), array_values($replace), $expected); $this->assertMatchesRegularExpression($regexp, $result); } /** * Data provider for test_parse_add_entry(). * * @return array of scenarios */ public static function parse_add_entry_provider(): array { return [ // Editing an entry. 'Teacher editing entry tags tag' => [ 'templatecontent' => 'Some ##tags## tag', 'expected' => '|GENERAL.*Some .*select .*tags.*Cats.* tag|', 'newentry' => false, ], 'Teacher editing entry field name tag' => [ 'templatecontent' => 'Some [[myfield]] tag', 'expected' => '|GENERAL.*Some .*FIELD.*field_{fieldid}.*input.*New value.* tag|', 'newentry' => false, ], 'Teacher editing entry field#id tag' => [ 'templatecontent' => 'Some [[myfield#id]] tag', 'expected' => '|GENERAL.*Some field_{fieldid} tag|', 'newentry' => false, ], 'Teacher editing field#name tag' => [ 'templatecontent' => 'Some [[myfield#name]] tag', 'expected' => '|GENERAL.*Some {fieldname} tag|', 'newentry' => false, ], 'Teacher editing field#description tag' => [ 'templatecontent' => 'Some [[myfield#description]] tag', 'expected' => '|GENERAL.*Some {fielddescription} tag|', 'newentry' => false, ], 'Teacher editing entry field otherfields tag' => [ 'templatecontent' => 'Some [[myfield]] and ##otherfields## tag', 'expected' => '|GENERAL.*Some .*FIELD.*field_{fieldid}.*input.*New value.* ' . 'and .*OTHERFIELD.*field_{otherid}.*input.*Altered value.* tag|', 'newentry' => false, ], // New entry. 'Teacher new entry tags tag' => [ 'templatecontent' => 'Some ##tags## tag', 'expected' => '|GENERAL.*Some .*select .*tags\[\].* tag|', 'newentry' => true, ], 'Teacher new entry field name tag' => [ 'templatecontent' => 'Some [[myfield]] tag', 'expected' => '|GENERAL.*Some .*FIELD.*field_{fieldid}.*input.*value="".* tag|', 'newentry' => true, ], 'Teacher new entry field#id name tag' => [ 'templatecontent' => 'Some [[myfield#id]] tag', 'expected' => '|GENERAL.*Some field_{fieldid} tag|', 'newentry' => true, ], 'Teacher new entry field#name tag' => [ 'templatecontent' => 'Some [[myfield#name]] tag', 'expected' => '|GENERAL.*Some {fieldname} tag|', 'newentry' => false, ], 'Teacher new entry field#description tag' => [ 'templatecontent' => 'Some [[myfield#description]] tag', 'expected' => '|GENERAL.*Some {fielddescription} tag|', 'newentry' => false, ], 'Teacher new entry field otherfields tag' => [ 'templatecontent' => 'Some [[myfield]] and ##otherfields## tag', 'expected' => '|GENERAL.*Some .*FIELD.*field_{fieldid}.*input.*New value.* ' . '.* and .*OTHERFIELD.*field_{otherid}.*input.*Altered value.* |', 'newentry' => false, ], ]; } } tests/entries_importer_test.php000064400000017663152265517740013103 0ustar00. namespace mod_data; use context_module; use mod_data\local\exporter\csv_entries_exporter; use mod_data\local\exporter\ods_entries_exporter; use mod_data\local\exporter\utils; use mod_data\local\importer\csv_entries_importer; use zip_archive; /** * Unit tests for entries_importer and csv_entries_importer class. * * Also {@see entries_import_test} class which provides module tests for importing entries. * * @package mod_data * @covers \mod_data\local\importer\entries_importer * @covers \mod_data\local\importer\csv_entries_importer * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entries_importer_test extends \advanced_testcase { /** * Set up function. */ protected function setUp(): void { parent::setUp(); global $CFG; require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->dirroot . '/lib/datalib.php'); require_once($CFG->dirroot . '/lib/csvlib.class.php'); require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); require_once($CFG->dirroot . '/mod/data/tests/generator/lib.php'); } /** * Get the test data. * In this instance we are setting up database records to be used in the unit tests. * * @return array */ protected function get_test_data(): array { $this->resetAfterTest(true); $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->setUser($teacher); $student = $this->getDataGenerator()->create_and_enrol($course, 'student', array('username' => 'student')); $data = $generator->create_instance(array('course' => $course->id)); $cm = get_coursemodule_from_instance('data', $data->id); // Add fields. $fieldrecord = new \stdClass(); $fieldrecord->name = 'ID'; // Identifier of the records for testing. $fieldrecord->type = 'number'; $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'Param2'; $fieldrecord->type = 'text'; $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'filefield'; $fieldrecord->type = 'file'; $generator->create_field($fieldrecord, $data); $fieldrecord->name = 'picturefield'; $fieldrecord->type = 'picture'; $generator->create_field($fieldrecord, $data); return [ 'teacher' => $teacher, 'student' => $student, 'data' => $data, 'cm' => $cm, ]; } /** * Test importing files from zip archive. * * @covers \mod_data\local\importer\entries_importer::get_file_content_from_zip * @covers \mod_data\local\importer\entries_importer::get_data_file_content * @dataProvider get_file_content_from_zip_provider * @param array $files array of filenames and filecontents to test * @param mixed $datafilecontent the expected result returned by the method which is being tested here */ public function test_get_file_content_from_zip(array $files, mixed $datafilecontent): void { // First we need to create the zip file from the provided data. $tmpdir = make_request_directory(); $zipfilepath = $tmpdir . '/entries_importer_test_tmp_' . time() . '.zip'; $ziparchive = new zip_archive(); $ziparchive->open($zipfilepath); foreach ($files as $file) { $localname = empty($file['subdir']) ? $file['filename'] : $file['subdir'] . '/' . $file['filename']; $ziparchive->add_file_from_string($localname, $file['filecontent']); } $ziparchive->close(); // We now created a zip archive according to the data provider's data. We now can test the importer. $importer = new csv_entries_importer($zipfilepath, 'testzip.zip'); foreach ($files as $file) { $subdir = empty($file['subdir']) ? '' : $file['subdir']; $this->assertEquals($file['filecontent'], $importer->get_file_content_from_zip($file['filename'], $subdir)); } // Test the method to retrieve the datafile content. $this->assertEquals($datafilecontent, $importer->get_data_file_content()); unlink($zipfilepath); } /** * Data provider method for self::test_get_file_content_from_zip. * * @return array data for testing */ public static function get_file_content_from_zip_provider(): array { return [ 'some files in the zip archive' => [ 'files' => [ [ 'filename' => 'datafile.csv', 'filecontent' => 'some,csv,data' ], [ 'filename' => 'testfile.txt', 'filecontent' => 'somecontent', 'subdir' => 'files' ], [ 'filename' => 'testfile2.txt', 'filecontent' => 'someothercontent', 'subdir' => 'testsubdir' ] ], // Should be identical with filecontent of 'datafile.csv' above. 'datafilecontent' => 'some,csv,data' ], 'wrongly placed data file' => [ 'files' => [ [ 'filename' => 'datafile.csv', 'filecontent' => 'some,csv,data', 'subdir' => 'wrongsubdir' ], [ 'filename' => 'testfile.txt', 'filecontent' => 'somecontent', 'subdir' => 'files' ], [ 'filename' => 'testfile2.txt', 'filecontent' => 'someothercontent', 'subdir' => 'testsubdir' ] ], // Data file is not in the root directory, though no content should be retrieved. 'datafilecontent' => false ], 'two data files where only one is allowed' => [ 'files' => [ [ 'filename' => 'datafile.csv', 'filecontent' => 'some,csv,data', ], [ 'filename' => 'anothercsvfile.csv', 'filecontent' => 'some,other,csv,data', ], [ 'filename' => 'testfile.txt', 'filecontent' => 'somecontent', 'subdir' => 'files' ], [ 'filename' => 'testfile2.txt', 'filecontent' => 'someothercontent', 'subdir' => 'testsubdir' ] ], // There are two data files in the zip root, so the data cannot be imported. 'datafilecontent' => false ], ]; } } tests/locallib_test.php000064400000022745152265517740011267 0ustar00. namespace mod_data; use mod_data\external\record_exporter; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/data/locallib.php'); /** * Unit tests for locallib.php * * @package mod_data * @copyright 2022 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class locallib_test extends \advanced_testcase { /** * Confirms that search is working * @covers ::data_search_entries */ public function test_data_search_entries(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $record = new \stdClass(); $record->course = $course->id; $record->name = "Mod data delete test"; $record->intro = "Some intro of some sort"; $module = $this->getDataGenerator()->create_module('data', $record); $titlefield = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field( (object) [ 'name' => 'title', 'type' => 'text', 'required' => 1 ], $module); $captionfield = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field( (object) [ 'name' => 'caption', 'type' => 'text', 'required' => 1 ], $module); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($module, [ $titlefield->field->id => 'Entry 1', $captionfield->field->id => 'caption' ]); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($module, [ $titlefield->field->id => 'Entry 2', $captionfield->field->id => '' ]); $cm = get_coursemodule_from_id('data', $module->cmid); // Search for entries without any search query set, we should return them all. list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', 0); $this->assertCount(2, $records); // Search for entries for "caption" we should return only one of them. list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', 0, 'caption'); $this->assertCount(1, $records); // Same search but we order by title. list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', 0, 'caption', $titlefield->field->id, 'ASC'); $this->assertCount(1, $records); $this->assert_record_entries_contains($records, $captionfield->field->id, 'caption'); // Now with advanced search. $defaults = []; $fn = $ln = ''; // Defaults for first and last name. // Force value for advanced search. $_GET['f_' . $captionfield->field->id] = 'caption'; list($searcharray, $searchtext) = data_build_search_array($module, false, [], $defaults, $fn, $ln); list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', 0, $searchtext, $titlefield->field->id, 'ASC', 0, 0, true, $searcharray); $this->assertCount(1, $records); $this->assert_record_entries_contains($records, $captionfield->field->id, 'caption'); } /** * Confirms that search is working with groups * @covers ::data_search_entries */ public function test_data_search_entries_with_groups(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]); $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $student1 = $this->getDataGenerator()->create_and_enrol($course); $student2 = $this->getDataGenerator()->create_and_enrol($course); $student3 = $this->getDataGenerator()->create_and_enrol($course); $teacher1 = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $teacher2 = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $teacher3 = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); groups_add_member($group1->id, $student1->id); groups_add_member($group1->id, $teacher1->id); groups_add_member($group2->id, $student3->id); groups_add_member($group2->id, $teacher3->id); $record = new \stdClass(); $record->course = $course->id; $record->name = "Mod data delete test"; $record->intro = "Some intro of some sort"; $module = $this->getDataGenerator()->create_module('data', $record); $titlefield = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field( (object) [ 'name' => 'title', 'type' => 'text', 'required' => 1, ], $module); $captionfield = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field( (object) [ 'name' => 'caption', 'type' => 'text', 'required' => 1, ], $module); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($module, [ $titlefield->field->id => 'Entry 1 - group 1', $captionfield->field->id => 'caption', ], $group1->id, [], null, $student1->id ); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($module, [ $titlefield->field->id => 'Entry 2 - group 1', $captionfield->field->id => 'caption', ], $group1->id, [], null, $student1->id ); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($module, [ $titlefield->field->id => 'Entry 3 - group 2', $captionfield->field->id => '', ], $group2->id, [], null, $student3->id ); $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($module, [ $titlefield->field->id => 'Entry 3 - no group', $captionfield->field->id => '', ], 0, [], null, $student2->id ); $cm = get_coursemodule_from_id('data', $module->cmid); $this->setUser($teacher1); // As a non editing teacher in group 1, I should see only the entries for group 1. list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', $group1->id); $this->assertCount(3, $records); // Record with group 1 and record with no group. // As a non editing teacher not in a group, I should see the entry from users not in a group. $this->setUser($teacher3); list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', $group2->id); $this->assertCount(2, $records); // Record with group 2 and record with no group. // As a non editing teacher not in a group, I should see the entry from users not in a group. $this->setUser($teacher2); list($records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode) = data_search_entries($module, $cm, \context_course::instance($course->id), 'list', 0); $this->assertCount(1, $records); // Just the record with no group. $this->assert_record_entries_contains($records, $titlefield->field->id, 'Entry 3 - no group'); } /** * Assert that all records contains a value for the matching field id. * * @param array $records * @param int $fieldid * @param string $content * @return void */ private function assert_record_entries_contains($records, $fieldid, $content) { global $DB; foreach ($records as $record) { $fieldscontent = $DB->get_records('data_content', ['recordid' => $record->id]); foreach ($fieldscontent as $fieldcontent) { if ($fieldcontent->id == $fieldid) { $this->assertStringContainsString($fieldcontent->content, $content); } } } } } tests/custom_completion_test.php000064400000016063152265517740013245 0ustar00. /** * Contains unit tests for core_completion/activity_custom_completion. * * @package mod_data * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_data; use advanced_testcase; use cm_info; use coding_exception; use mod_data\completion\custom_completion; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** * Class for unit testing mod_data/activity_custom_completion. * * @package mod_data * @copyright Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class custom_completion_test extends advanced_testcase { /** * Data provider for get_state(). * * @return array[] */ public static function get_state_provider(): array { return [ 'Undefined rule' => [ 'somenonexistentrule', COMPLETION_DISABLED, 0, null, coding_exception::class ], 'Rule not available' => [ 'completionentries', COMPLETION_DISABLED, 0, null, moodle_exception::class ], 'Rule available, user has not created entries' => [ 'completionentries', COMPLETION_ENABLED, 0, COMPLETION_INCOMPLETE, null ], 'Rule available, user has created entries' => [ 'completionentries', COMPLETION_ENABLED, 2, COMPLETION_COMPLETE, null ], ]; } /** * Test for get_state(). * * @dataProvider get_state_provider * @param string $rule The custom completion rule. * @param int $available Whether this rule is available. * @param int $entries The number of entries. * @param int|null $status Expected status. * @param string|null $exception Expected exception. */ public function test_get_state(string $rule, int $available, int $entries, ?int $status, ?string $exception): void { global $DB; if (!is_null($exception)) { $this->expectException($exception); } // Custom completion rule data for cm_info::customdata. $customdataval = [ 'customcompletionrules' => [ $rule => $available ] ]; // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of the magic getter method when fetching the cm_info object's customdata and instance values. $mockcminfo->expects($this->any()) ->method('__get') ->will($this->returnValueMap([ ['customdata', $customdataval], ['instance', 1], ])); // Mock the DB calls. $DB = $this->createMock(get_class($DB)); $DB->expects($this->atMost(1)) ->method('count_records') ->willReturn($entries); $customcompletion = new custom_completion($mockcminfo, 2); $this->assertEquals($status, $customcompletion->get_state($rule)); } /** * Test for get_defined_custom_rules(). */ public function test_get_defined_custom_rules(): void { $rules = custom_completion::get_defined_custom_rules(); $this->assertCount(1, $rules); $this->assertEquals('completionentries', reset($rules)); } /** * Test for get_defined_custom_rule_descriptions(). */ public function test_get_custom_rule_descriptions(): void { // Get defined custom rules. $rules = custom_completion::get_defined_custom_rules(); // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Instantiate a custom_completion object using the mocked cm_info. $customcompletion = new custom_completion($mockcminfo, 1); // Get custom rule descriptions. $ruledescriptions = $customcompletion->get_custom_rule_descriptions(); // Confirm that defined rules and rule descriptions are consistent with each other. $this->assertEquals(count($rules), count($ruledescriptions)); foreach ($rules as $rule) { $this->assertArrayHasKey($rule, $ruledescriptions); } } /** * Test for is_defined(). */ public function test_is_defined(): void { // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->getMock(); $customcompletion = new custom_completion($mockcminfo, 1); // Rule is defined. $this->assertTrue($customcompletion->is_defined('completionentries')); // Undefined rule. $this->assertFalse($customcompletion->is_defined('somerandomrule')); } /** * Data provider for test_get_available_custom_rules(). * * @return array[] */ public static function get_available_custom_rules_provider(): array { return [ 'Completion entries available' => [ COMPLETION_ENABLED, ['completionentries'] ], 'Completion entries not available' => [ COMPLETION_DISABLED, [] ], ]; } /** * Test for get_available_custom_rules(). * * @dataProvider get_available_custom_rules_provider * @param int $status * @param array $expected */ public function test_get_available_custom_rules(int $status, array $expected): void { $customdataval = [ 'customcompletionrules' => [ 'completionentries' => $status ] ]; // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of magic getter for the customdata attribute. $mockcminfo->expects($this->any()) ->method('__get') ->with('customdata') ->willReturn($customdataval); $customcompletion = new custom_completion($mockcminfo, 1); $this->assertEquals($expected, $customcompletion->get_available_custom_rules()); } } tests/search/search_test.php000064400000130356152265517740012216 0ustar00. /** * Unit tests for data_get_all_recordsids(), data_get_advance_search_ids(), data_get_record_ids(), * and data_get_advanced_search_sql() * * @package mod_data * @category phpunit * @copyright 2012 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\search; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/mod/data/lib.php'); require_once($CFG->dirroot . '/lib/datalib.php'); require_once($CFG->dirroot . '/lib/csvlib.class.php'); require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php'); require_once($CFG->dirroot . '/mod/data/tests/generator/lib.php'); /** * Unit tests for {@see data_get_all_recordids()}. * {@see data_get_advanced_search_ids()} * {@see data_get_record_ids()} * {@see data_get_advanced_search_sql()} * * @package mod_data * @copyright 2012 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class search_test extends \advanced_testcase { /** * @var stdObject $recorddata An object that holds information from the table data. */ public $recorddata = null; /** * @var int $recordcontentid The content ID. */ public $recordcontentid = null; /** * @var int $recordrecordid The record ID. */ public $recordrecordid = null; /** * @var int $recordfieldid The field ID. */ public $recordfieldid = null; /** * @var array $recordsearcharray An array of stdClass which contains search criteria. */ public $recordsearcharray = null; // CONSTANTS /** * @var int $datarecordcount The number of records in the database. */ public $datarecordcount = 100; /** * @var int $groupdatarecordcount The number of records in the database in groups 0 and 1. */ public $groupdatarecordcount = 75; /** * @var array $datarecordset Expected record IDs. */ public $datarecordset = array('0' => '6'); /** * @var array $finalrecord Final record for comparison with test four. */ public $finalrecord = array(); /** * @var int $approvedatarecordcount The number of approved records in the database. */ public $approvedatarecordcount = 89; /** * @var string Area id */ protected $databaseentryareaid = null; /** * Set up function. In this instance we are setting up database * records to be used in the unit tests. */ protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); set_config('enableglobalsearch', true); $this->databaseentryareaid = \core_search\manager::generate_areaid('mod_data', 'entry'); // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this. $search = \testable_core_search::instance(); } /** * Test 1: The function data_get_all_recordids. * * Test 2: This tests the data_get_advance_search_ids() function. The function takes a set * of all the record IDs in the database and then with the search details ($this->recordsearcharray) * returns a comma seperated string of record IDs that match the search criteria. * * Test 3: This function tests data_get_recordids(). This is the function that is nested in the last * function (see data_get_advance_search_ids). This function takes a couple of * extra parameters. $alias is the field alias used in the sql query and $commaid * is a comma seperated string of record IDs. * * Test 3.1: This tests that if no recordids are provided (In a situation where a search is done on an empty database) * That an empty array is returned. * * Test 4: data_get_advanced_search_sql provides an array which contains an sql string to be used for displaying records * to the user when they use the advanced search criteria and the parameters that go with the sql statement. This test * takes that information and does a search on the database, returning a record. * * Test 5: Returning to data_get_all_recordids(). Here we are ensuring that the total amount of record ids is reduced to * match the group conditions that are provided. There are 25 entries which relate to group 2. They are removed * from the total so we should only have 75 records total. * * Test 6: data_get_all_recordids() again. This time we are testing approved database records. We only want to * display the records that have been approved. In this record set we have 89 approved records. */ public function test_advanced_search_sql_section(): void { global $DB; // we already have 2 users, we need 98 more - let's ignore the fact that guest can not post anywhere // We reset the user sequence here to ensure we get the expected numbers. // TODO: Invent a better way for managing data file input against database sequence id's. $DB->get_manager()->reset_sequence('user'); for($i=3;$i<=100;$i++) { $this->getDataGenerator()->create_user(); } // create database module - there should be more of these I guess $course = $this->getDataGenerator()->create_course(); $data = $this->getDataGenerator()->create_module('data', array('course'=>$course->id)); $this->recorddata = $data; // Set up data for the test database. $files = array( 'data_fields' => __DIR__.'/../fixtures/test_data_fields.csv', 'data_records' => __DIR__.'/../fixtures/test_data_records.csv', 'data_content' => __DIR__.'/../fixtures/test_data_content.csv', ); $this->dataset_from_files($files)->to_database(); // Set dataid to the correct value now the data has been inserted by csv file. $DB->execute('UPDATE {data_fields} SET dataid = ?', array($data->id)); $DB->execute('UPDATE {data_records} SET dataid = ?', array($data->id)); // Create the search array which contains our advanced search criteria. $fieldinfo = array('0' => new \stdClass(), '1' => new \stdClass(), '2' => new \stdClass(), '3' => new \stdClass(), '4' => new \stdClass()); $fieldinfo['0']->id = 1; $fieldinfo['0']->data = '3.721,46.6126'; $fieldinfo['1']->id = 2; $fieldinfo['1']->data = 'Hahn Premium'; $fieldinfo['2']->id = 5; $fieldinfo['2']->data = 'Female'; $fieldinfo['3']->id = 7; $fieldinfo['3']->data = 'kel'; $fieldinfo['4']->id = 9; $fieldinfo['4']->data = 'VIC'; foreach($fieldinfo as $field) { $searchfield = data_get_field_from_id($field->id, $data); if ($field->id == 2) { $searchfield->field->param1 = 'Hahn Premium'; $val = array(); $val['selected'] = array('0' => 'Hahn Premium'); $val['allrequired'] = 0; } else { $val = $field->data; } $search_array[$field->id] = new \stdClass(); list($search_array[$field->id]->sql, $search_array[$field->id]->params) = $searchfield->generate_sql('c' . $field->id, $val); } $this->recordsearcharray = $search_array; // Setting up the comparison stdClass for the last test. $user = $DB->get_record('user', array('id'=>6)); $this->finalrecord[6] = new \stdClass(); $this->finalrecord[6]->id = 6; $this->finalrecord[6]->approved = 1; $this->finalrecord[6]->timecreated = 1234567891; $this->finalrecord[6]->timemodified = 1234567892; $this->finalrecord[6]->userid = 6; $this->finalrecord[6]->firstname = $user->firstname; $this->finalrecord[6]->lastname = $user->lastname; $this->finalrecord[6]->firstnamephonetic = $user->firstnamephonetic; $this->finalrecord[6]->lastnamephonetic = $user->lastnamephonetic; $this->finalrecord[6]->middlename = $user->middlename; $this->finalrecord[6]->alternatename = $user->alternatename; $this->finalrecord[6]->picture = $user->picture; $this->finalrecord[6]->imagealt = $user->imagealt; $this->finalrecord[6]->email = $user->email; // Test 1 $recordids = data_get_all_recordids($this->recorddata->id); $this->assertEquals(count($recordids), $this->datarecordcount); // Test 2 $key = array_keys($this->recordsearcharray); $alias = $key[0]; $newrecordids = data_get_recordids($alias, $this->recordsearcharray, $this->recorddata->id, $recordids); $this->assertEquals($this->datarecordset, $newrecordids); // Test 3 $newrecordids = data_get_advance_search_ids($recordids, $this->recordsearcharray, $this->recorddata->id); $this->assertEquals($this->datarecordset, $newrecordids); // Test 3.1 $resultrecordids = data_get_advance_search_ids(array(), $this->recordsearcharray, $this->recorddata->id); $this->assertEmpty($resultrecordids); // Test 4 $sortorder = 'ORDER BY r.timecreated ASC , r.id ASC'; $html = data_get_advanced_search_sql('0', $this->recorddata, $newrecordids, '', $sortorder); $allparams = array_merge($html['params'], array('dataid' => $this->recorddata->id)); $records = $DB->get_records_sql($html['sql'], $allparams); $this->assertEquals($records, $this->finalrecord); // Test 5 $groupsql = " AND (r.groupid = :currentgroup OR r.groupid = 0)"; $params = array('currentgroup' => 1); $recordids = data_get_all_recordids($this->recorddata->id, $groupsql, $params); $this->assertEquals($this->groupdatarecordcount, count($recordids)); // Test 6 $approvesql = ' AND r.approved=1 '; $recordids = data_get_all_recordids($this->recorddata->id, $approvesql, $params); $this->assertEquals($this->approvedatarecordcount, count($recordids)); } public function test_advanced_search_tags(): void { $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $course1 = $this->getDataGenerator()->create_course(); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $fieldrecord->titlefield = true; $data1 = $this->getDataGenerator()->create_module('data', array('course' => $course1->id, 'approval' => true)); $field1 = $datagenerator->create_field($fieldrecord, $data1); $record11 = $datagenerator->create_entry($data1, [$field1->field->id => 'value11'], 0, ['Cats', 'Dogs']); $record12 = $datagenerator->create_entry($data1, [$field1->field->id => 'value12'], 0, ['Cats', 'mice']); $record13 = $datagenerator->create_entry($data1, [$field1->field->id => 'value13'], 0, ['Bats']); $searcharray = []; $searcharray[DATA_TAGS] = new \stdClass(); $searcharray[DATA_TAGS]->params = []; $searcharray[DATA_TAGS]->rawtagnames = ['Cats']; $searcharray[DATA_TAGS]->sql = ''; $recordids = data_get_all_recordids($data1->id); $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data1->id); $this->assertContainsEquals($record11, $newrecordids); $this->assertContainsEquals($record12, $newrecordids); $this->assertNotContainsEquals($record13, $newrecordids); } /** * Indexing database entries contents. * * @return void */ public function test_data_entries_indexing(): void { global $DB; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->databaseentryareaid); $this->assertInstanceOf('\mod_data\search\entry', $searcharea); $user1 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; $this->setUser($user1); // Available for both student and teacher. $data1 = $this->getDataGenerator()->create_module('data', $record); // Excluding LatLong and Picture as we aren't indexing LatLong and Picture fields any way // ...and they're complex and not of any use to consider for this test. // Excluding File as we are indexing files seperately and its complex to implement. $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' ); $this->create_default_data_fields($fieldtypes, $data1); $data1record1id = $this->create_default_data_record($data1); // All records. $recordset = $searcharea->get_recordset_by_timestamp(0); $this->assertTrue($recordset->valid()); $nrecords = 0; foreach ($recordset as $record) { $this->assertInstanceOf('stdClass', $record); $doc = $searcharea->get_document($record); $this->assertInstanceOf('\core_search\document', $doc); $nrecords++; } // If there would be an error/failure in the foreach above the recordset would be closed on shutdown. $recordset->close(); $this->assertEquals(1, $nrecords); // The +2 is to prevent race conditions. $recordset = $searcharea->get_recordset_by_timestamp(time() + 2); // No new records. $this->assertFalse($recordset->valid()); $recordset->close(); // Create a second database, also with one record. $data2 = $this->getDataGenerator()->create_module('data', ['course' => $course1->id]); $this->create_default_data_fields($fieldtypes, $data2); $this->create_default_data_record($data2); // Test indexing with contexts. $rs = $searcharea->get_document_recordset(0, \context_module::instance($data1->cmid)); $this->assertEquals(1, iterator_count($rs)); $rs->close(); $rs = $searcharea->get_document_recordset(0, \context_module::instance($data2->cmid)); $this->assertEquals(1, iterator_count($rs)); $rs->close(); $rs = $searcharea->get_document_recordset(0, \context_course::instance($course1->id)); $this->assertEquals(2, iterator_count($rs)); $rs->close(); } /** * Document contents. * * @return void */ public function test_data_entries_document(): void { global $DB; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->databaseentryareaid); $this->assertInstanceOf('\mod_data\search\entry', $searcharea); $user1 = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $record = new \stdClass(); $record->course = $course->id; $this->setAdminUser(); // First Case. $data1 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' ); $this->create_default_data_fields($fieldtypes, $data1); $data1record1id = $this->create_default_data_record($data1); $data1entry1 = $this->get_entry_for_id($data1record1id); $data1doc = $searcharea->get_document($data1entry1); $this->assertEquals($data1doc->get('courseid'), $course->id); $this->assertEquals($data1doc->get('title'), 'text for testing'); $this->assertEquals($data1doc->get('content'), 'menu1'); $this->assertEquals($data1doc->get('description1'), 'radioopt1'); $this->assertEquals($data1doc->get('description2'), 'opt1 opt2 opt3 opt4 multimenu1 multimenu2 multimenu3 multimenu4 text area testing http://example.url'); // Second Case. $data2 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( array('checkbox', 1), array('textarea', 0), array('menu', 0), array('number', 1), array('url', 0), array('text', 0) ); $this->create_default_data_fields($fieldtypes, $data2); $data2record1id = $this->create_default_data_record($data2); $data2entry1 = $this->get_entry_for_id($data2record1id); $data2doc = $searcharea->get_document($data2entry1); $this->assertEquals($data2doc->get('courseid'), $course->id); $this->assertEquals($data2doc->get('title'), 'opt1 opt2 opt3 opt4'); $this->assertEquals($data2doc->get('content'), 'text for testing'); $this->assertEquals($data2doc->get('description1'), 'menu1'); $this->assertEquals($data2doc->get('description2'), 'text area testing http://example.url'); // Third Case. $data3 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( 'url' ); $this->create_default_data_fields($fieldtypes, $data3); $data3record1id = $this->create_default_data_record($data3); $data3entry1 = $this->get_entry_for_id($data3record1id); $this->assertFalse($searcharea->get_document($data3entry1)); // Fourth Case. $data4 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( array('date', 1), array('text', 1)); $this->create_default_data_fields($fieldtypes, $data4); $data4record1id = $this->create_default_data_record($data4); $data4entry1 = $this->get_entry_for_id($data4record1id); $this->assertFalse($searcharea->get_document($data4entry1)); // Fifth Case. $data5 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( array('checkbox', 0), array('number', 1), array('text', 0), array('date', 1), array('textarea', 0), array('url', 1)); $this->create_default_data_fields($fieldtypes, $data5); $data5record1id = $this->create_default_data_record($data5); $data5entry1 = $this->get_entry_for_id($data5record1id); $data5doc = $searcharea->get_document($data5entry1); $this->assertEquals($data5doc->get('courseid'), $course->id); $this->assertEquals($data5doc->get('title'), 'http://example.url'); $this->assertEquals($data5doc->get('content'), 'text for testing'); $this->assertEquals($data5doc->get('description1'), 'opt1 opt2 opt3 opt4'); $this->assertEquals($data5doc->get('description2'), 'text area testing'); // Sixth Case. $data6 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( array('date', 1), array('number', 1)); $this->create_default_data_fields($fieldtypes, $data6); $data6record1id = $this->create_default_data_record($data6); $data6entry1 = $this->get_entry_for_id($data6record1id); $data6doc = $searcharea->get_document($data6entry1); $this->assertFalse($data6doc); // Seventh Case. $data7 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( array('date', 1), array('number', 1), array('text', 0), array('textarea', 0)); $this->create_default_data_fields($fieldtypes, $data7); $data7record1id = $this->create_default_data_record($data7); $data7entry1 = $this->get_entry_for_id($data7record1id); $data7doc = $searcharea->get_document($data7entry1); $this->assertEquals($data7doc->get('courseid'), $course->id); $this->assertEquals($data7doc->get('title'), 'text for testing'); $this->assertEquals($data7doc->get('content'), 'text area testing'); // Eight Case. $data8 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array('url', 'url', 'url', 'text'); $this->create_default_data_fields($fieldtypes, $data8); $data8record1id = $this->create_default_data_record($data8); $data8entry1 = $this->get_entry_for_id($data8record1id); $data8doc = $searcharea->get_document($data8entry1); $this->assertEquals($data8doc->get('courseid'), $course->id); $this->assertEquals($data8doc->get('title'), 'text for testing'); $this->assertEquals($data8doc->get('content'), 'http://example.url'); $this->assertEquals($data8doc->get('description1'), 'http://example.url'); $this->assertEquals($data8doc->get('description2'), 'http://example.url'); // Ninth Case. $data9 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array('radiobutton', 'menu', 'multimenu'); $this->create_default_data_fields($fieldtypes, $data9); $data9record1id = $this->create_default_data_record($data9); $data9entry1 = $this->get_entry_for_id($data9record1id); $data9doc = $searcharea->get_document($data9entry1); $this->assertEquals($data9doc->get('courseid'), $course->id); $this->assertEquals($data9doc->get('title'), 'radioopt1'); $this->assertEquals($data9doc->get('content'), 'menu1'); $this->assertEquals($data9doc->get('description1'), 'multimenu1 multimenu2 multimenu3 multimenu4'); // Tenth Case. $data10 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array('checkbox', 'textarea', 'multimenu'); $this->create_default_data_fields($fieldtypes, $data10); $data10record1id = $this->create_default_data_record($data10); $data10entry1 = $this->get_entry_for_id($data10record1id); $data10doc = $searcharea->get_document($data10entry1); $this->assertEquals($data10doc->get('courseid'), $course->id); $this->assertEquals($data10doc->get('title'), 'opt1 opt2 opt3 opt4'); $this->assertEquals($data10doc->get('content'), 'text area testing'); $this->assertEquals($data10doc->get('description1'), 'multimenu1 multimenu2 multimenu3 multimenu4'); } /** * Group support for data entries. */ public function test_data_entries_group_support(): void { global $DB; // Get the search area and test generators. $searcharea = \core_search\manager::get_search_area($this->databaseentryareaid); $generator = $this->getDataGenerator(); $datagenerator = $generator->get_plugin_generator('mod_data'); // Create a course, a user, and two groups. $course = $generator->create_course(); $user = $generator->create_user(); $generator->enrol_user($user->id, $course->id, 'teacher'); $group1 = $generator->create_group(['courseid' => $course->id]); $group2 = $generator->create_group(['courseid' => $course->id]); // Separate groups database. $data = self::getDataGenerator()->create_module('data', ['course' => $course->id, 'groupmode' => SEPARATEGROUPS]); $fieldtypes = ['text', 'textarea']; $this->create_default_data_fields($fieldtypes, $data); $fields = $DB->get_records('data_fields', array('dataid' => $data->id)); foreach ($fields as $field) { switch ($field->type) { case 'text' : $textid = $field->id; break; case 'textarea' : $textareaid = $field->id; break; } } // As admin, create entries with each group and all groups. $this->setAdminUser(); $fieldvalues = [$textid => 'Title', $textareaid => 'Content']; $e1 = $datagenerator->create_entry($data, $fieldvalues, $group1->id); $e2 = $datagenerator->create_entry($data, $fieldvalues, $group2->id); $e3 = $datagenerator->create_entry($data, $fieldvalues); // Do the indexing of all 3 entries. $rs = $searcharea->get_recordset_by_timestamp(0); $results = []; foreach ($rs as $rec) { $results[$rec->id] = $rec; } $rs->close(); $this->assertCount(3, $results); // Check each has the correct groupid. $doc = $searcharea->get_document($results[$e1]); $this->assertTrue($doc->is_set('groupid')); $this->assertEquals($group1->id, $doc->get('groupid')); $doc = $searcharea->get_document($results[$e2]); $this->assertTrue($doc->is_set('groupid')); $this->assertEquals($group2->id, $doc->get('groupid')); $doc = $searcharea->get_document($results[$e3]); $this->assertFalse($doc->is_set('groupid')); // While we're here, also test that the search area requests restriction by group. $modinfo = get_fast_modinfo($course); $this->assertTrue($searcharea->restrict_cm_access_by_group($modinfo->get_cm($data->cmid))); // In visible groups mode, it won't request restriction by group. set_coursemodule_groupmode($data->cmid, VISIBLEGROUPS); $modinfo = get_fast_modinfo($course); $this->assertFalse($searcharea->restrict_cm_access_by_group($modinfo->get_cm($data->cmid))); } /** * Document accesses. * * @return void */ public function test_data_entries_access(): void { global $DB; // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->databaseentryareaid); $this->assertInstanceOf('\mod_data\search\entry', $searcharea); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $userteacher1 = self::getDataGenerator()->create_user(); $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student'); $this->getDataGenerator()->enrol_user($userteacher1->id, $course1->id, 'teacher'); $this->getDataGenerator()->enrol_user($user3->id, $course2->id, 'student'); $record = new \stdClass(); $record->course = $course1->id; $this->setUser($userteacher1); $data1 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' ); $this->create_default_data_fields($fieldtypes, $data1); $this->setUser($user1); $data1record1id = $this->create_default_data_record($data1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data1record1id)); $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-1)); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data1record1id)); $this->setUser($user3); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data1record1id)); $this->setUser($userteacher1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data1record1id)); $this->setAdminUser(); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data1record1id)); $this->setGuestUser(); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data1record1id)); // Case with groups. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $user3 = self::getDataGenerator()->create_user(); $userteacher1 = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(array('groupmode' => 1, 'groupmodeforce' => 1)); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($userteacher1->id, $course->id, 'teacher'); $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'student'); $groupa = $this->getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'groupA')); $groupb = $this->getDataGenerator()->create_group(array('courseid' => $course->id, 'name' => 'groupB')); $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $groupa->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user2->id, 'groupid' => $groupa->id)); $this->getDataGenerator()->create_group_member(array('userid' => $userteacher1->id, 'groupid' => $groupa->id)); $this->getDataGenerator()->create_group_member(array('userid' => $user3->id, 'groupid' => $groupb->id)); $record = new \stdClass(); $record->course = $course->id; $this->setUser($userteacher1); $data2 = $this->getDataGenerator()->create_module('data', $record); $cm = get_coursemodule_from_instance('data', $data2->id, $course->id); $cm->groupmode = '1'; $cm->effectivegroupmode = '1'; $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' ); $this->create_default_data_fields($fieldtypes, $data2); $this->setUser($user1); $data2record1id = $this->create_default_data_record($data2, $groupa->id); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data2record1id)); $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-1)); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data2record1id)); $this->setUser($user3); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data2record1id)); $this->setUser($userteacher1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data2record1id)); $this->setAdminUser(); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data2record1id)); $this->setGuestUser(); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data2record1id)); // Case with approval. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $userteacher1 = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($userteacher1->id, $course->id, 'teacher'); $record = new \stdClass(); $record->course = $course->id; $this->setUser($userteacher1); $data3 = $this->getDataGenerator()->create_module('data', $record); $DB->update_record('data', array('id' => $data3->id, 'approval' => 1)); $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' ); $this->create_default_data_fields($fieldtypes, $data3); $this->setUser($user1); $data3record1id = $this->create_default_data_record($data3); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-1)); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data3record1id)); $this->setUser($userteacher1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->setAdminUser(); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->setGuestUser(); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data3record1id)); $DB->update_record('data_records', array('id' => $data3record1id, 'approved' => 1)); $this->setUser($user1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-1)); $this->setUser($user2); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->setUser($userteacher1); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->setAdminUser(); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data3record1id)); $this->setGuestUser(); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data3record1id)); // Case with requiredentriestoview. $this->setAdminUser(); $record->requiredentriestoview = 2; $data4 = $this->getDataGenerator()->create_module('data', $record); $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' ); $this->create_default_data_fields($fieldtypes, $data4); $data4record1id = $this->create_default_data_record($data4); $this->setUser($user1); $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($data4record1id)); $data4record2id = $this->create_default_data_record($data4); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data4record1id)); $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($data4record2id)); } /** * Test for file contents. * * @return void */ public function test_attach_files(): void { global $DB, $USER; $fs = get_file_storage(); // Returns the instance as long as the area is supported. $searcharea = \core_search\manager::get_search_area($this->databaseentryareaid); $this->assertInstanceOf('\mod_data\search\entry', $searcharea); $user1 = self::getDataGenerator()->create_user(); $course = self::getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $record = new \stdClass(); $record->course = $course->id; $this->setAdminUser(); // Creating database activity instance. $data1 = $this->getDataGenerator()->create_module('data', $record); // Creating file field. $record = new \stdClass; $record->type = 'file'; $record->dataid = $data1->id; $record->required = 0; $record->name = 'FileFld'; $record->description = 'Just another file field'; $record->param3 = 0; $record->param1 = ''; $record->param2 = ''; $data1filefieldid = $DB->insert_record('data_fields', $record); // Creating text field. $record = new \stdClass; $record->type = 'text'; $record->dataid = $data1->id; $record->required = 0; $record->name = 'TextFld'; $record->description = 'Just another text field'; $record->param3 = 0; $record->param1 = ''; $record->param2 = ''; $data1textfieldid = $DB->insert_record('data_fields', $record); // Creating textarea field. $record = new \stdClass; $record->type = 'textarea'; $record->dataid = $data1->id; $record->required = 0; $record->name = 'TextAreaFld'; $record->description = 'Just another textarea field'; $record->param1 = ''; $record->param2 = 60; $record->param3 = 35; $record->param3 = 1; $record->param3 = 0; $data1textareafieldid = $DB->insert_record('data_fields', $record); // Creating 1st entry. $record = new \stdClass; $record->userid = $USER->id; $record->dataid = $data1->id; $record->groupid = 0; $data1record1id = $DB->insert_record('data_records', $record); $record = new \stdClass; $record->fieldid = $data1filefieldid; $record->recordid = $data1record1id; $record->content = 'myfile1.txt'; $data1contentfileid1 = $DB->insert_record('data_content', $record); $filerecord = [ 'contextid' => \context_module::instance($data1->cmid)->id, 'component' => 'mod_data', 'filearea' => 'content', 'itemid' => $data1contentfileid1, 'filepath' => '/', 'filename' => 'myfile1.txt' ]; $data1record1file = $fs->create_file_from_string($filerecord, 'Some contents 1'); $record = new \stdClass; $record->fieldid = $data1textfieldid; $record->recordid = $data1record1id; $record->content = 'sample text'; $DB->insert_record('data_content', $record); $record = new \stdClass; $record->fieldid = $data1textareafieldid; $record->recordid = $data1record1id; $record->content = '
sample text


'; $record->content1 = 1; $DB->insert_record('data_content', $record); // Creating 2nd entry. $record = new \stdClass; $record->userid = $USER->id; $record->dataid = $data1->id; $record->groupid = 0; $data1record2id = $DB->insert_record('data_records', $record); $record = new \stdClass; $record->fieldid = $data1filefieldid; $record->recordid = $data1record2id; $record->content = 'myfile2.txt'; $data1contentfileid2 = $DB->insert_record('data_content', $record); $filerecord['itemid'] = $data1contentfileid2; $filerecord['filename'] = 'myfile2.txt'; $data1record2file = $fs->create_file_from_string($filerecord, 'Some contents 2'); $record = new \stdClass; $record->fieldid = $data1textfieldid; $record->recordid = $data1record2id; $record->content = 'sample text'; $DB->insert_record('data_content', $record); $record = new \stdClass; $record->fieldid = $data1textareafieldid; $record->recordid = $data1record2id; $record->content = '
sample text


'; $record->content1 = 1; $DB->insert_record('data_content', $record); // Now get all the posts and see if they have the right files attached. $searcharea = \core_search\manager::get_search_area($this->databaseentryareaid); $recordset = $searcharea->get_recordset_by_timestamp(0); $nrecords = 0; foreach ($recordset as $record) { $doc = $searcharea->get_document($record); $searcharea->attach_files($doc); $files = $doc->get_files(); // Now check that each doc has the right files on it. switch ($doc->get('itemid')) { case ($data1record1id): $this->assertCount(1, $files); $this->assertEquals($data1record1file->get_id(), $files[$data1record1file->get_id()]->get_id()); break; case ($data1record2id): $this->assertCount(1, $files); $this->assertEquals($data1record2file->get_id(), $files[$data1record2file->get_id()]->get_id()); break; default: $this->fail('Unexpected entry returned'); break; } $nrecords++; } $recordset->close(); $this->assertEquals(2, $nrecords); } /** * Creates default fields for a database instance * * @param array $fieldtypes * @param mod_data $data * @return void */ protected function create_default_data_fields($fieldtypes, $data) { $count = 1; // Creating test Fields with default parameter values. foreach ($fieldtypes as $fieldtype) { // Creating variables dynamically. $fieldname = 'field-'.$count; $record = new \stdClass(); $record->name = $fieldname; if (is_array($fieldtype)) { $record->type = $fieldtype[0]; $record->required = $fieldtype[1]; } else { $record->type = $fieldtype; $record->required = 0; } ${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data); $count++; } } /** * Creates default database entry content values for default field param values * * @param mod_data $data * @param int $groupid * @return int */ protected function create_default_data_record($data, $groupid = 0) { global $DB; $fields = $DB->get_records('data_fields', array('dataid' => $data->id)); $fieldcontents = array(); foreach ($fields as $fieldrecord) { switch ($fieldrecord->type) { case 'checkbox': $fieldcontents[$fieldrecord->id] = array('opt1', 'opt2', 'opt3', 'opt4'); break; case 'multimenu': $fieldcontents[$fieldrecord->id] = array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4'); break; case 'date': $fieldcontents[$fieldrecord->id] = '27-07-2016'; break; case 'menu': $fieldcontents[$fieldrecord->id] = 'menu1'; break; case 'radiobutton': $fieldcontents[$fieldrecord->id] = 'radioopt1'; break; case 'number': $fieldcontents[$fieldrecord->id] = '12345'; break; case 'text': $fieldcontents[$fieldrecord->id] = 'text for testing'; break; case 'textarea': $fieldcontents[$fieldrecord->id] = '

text area testing

'; break; case 'url': $fieldcontents[$fieldrecord->id] = array('example.url', 'sampleurl'); break; default: $this->fail('Unexpected field type'); break; } } return $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($data, $fieldcontents, $groupid); } /** * Creates default database entry content values for default field param values * * @param int $recordid * @return stdClass */ protected function get_entry_for_id($recordid ) { global $DB; $sql = "SELECT dr.*, d.course FROM {data_records} dr JOIN {data} d ON d.id = dr.dataid WHERE dr.id = :drid"; return $DB->get_record_sql($sql, array('drid' => $recordid)); } } tests/dates_test.php000064400000010372152265517740010577 0ustar00. /** * Contains unit tests for mod_data\dates. * * @package mod_data * @category test * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types=1); namespace mod_data; use advanced_testcase; use cm_info; use core\activity_dates; /** * Class for unit testing mod_data\dates. * * @copyright 2021 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class dates_test extends advanced_testcase { /** * Data provider for get_dates_for_module(). * @return array[] */ public static function get_dates_for_module_provider(): array { $now = time(); $before = $now - DAYSECS; $earlier = $before - DAYSECS; $after = $now + DAYSECS; $later = $after + DAYSECS; return [ 'without any dates' => [ null, null, [] ], 'only with opening time' => [ $after, null, [ ['label' => 'Opens:', 'timestamp' => $after, 'dataid' => 'timeavailablefrom'], ] ], 'only with closing time' => [ null, $after, [ ['label' => 'Closes:', 'timestamp' => $after, 'dataid' => 'timeavailableto'], ] ], 'with both times' => [ $after, $later, [ ['label' => 'Opens:', 'timestamp' => $after, 'dataid' => 'timeavailablefrom'], ['label' => 'Closes:', 'timestamp' => $later, 'dataid' => 'timeavailableto'], ] ], 'between the dates' => [ $before, $after, [ ['label' => 'Opened:', 'timestamp' => $before, 'dataid' => 'timeavailablefrom'], ['label' => 'Closes:', 'timestamp' => $after, 'dataid' => 'timeavailableto'], ] ], 'dates are past' => [ $earlier, $before, [ ['label' => 'Opened:', 'timestamp' => $earlier, 'dataid' => 'timeavailablefrom'], ['label' => 'Closed:', 'timestamp' => $before, 'dataid' => 'timeavailableto'], ] ], ]; } /** * Test for get_dates_for_module(). * * @dataProvider get_dates_for_module_provider * @param int|null $availablefrom The "available from" time in the database activity. * @param int|null $availableto The "available to" time in the database activity. * @param array $expected The expected value of calling get_dates_for_module() */ public function test_get_dates_for_module(?int $availablefrom, ?int $availableto, array $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); $data = ['course' => $course->id]; if ($availablefrom) { $data['timeavailablefrom'] = $availablefrom; } if ($availableto) { $data['timeavailableto'] = $availableto; } $moddata = $this->getDataGenerator()->create_module('data', $data); $this->setUser($user); $cm = get_coursemodule_from_instance('data', $moddata->id); // Make sure we're using a cm_info object. $cm = cm_info::create($cm); $dates = activity_dates::get_dates_for_module($cm, (int) $user->id); $this->assertEquals($expected, $dates); } } tests/externallib_test.php000064400000202657152265517740012021 0ustar00. namespace mod_data; use externallib_advanced_testcase; use mod_data_external; use core_external\external_api; use core_external\external_settings; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * Database module external functions tests * * @package mod_data * @category external * @copyright 2015 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 * @coversDefaultClass \mod_data_external */ final class externallib_test extends externallib_advanced_testcase { /** @var stdClass Test module context. */ protected $context; /** @var stdClass Test course.*/ protected $course; /** @var stdClass Test course module. */ protected $cm; /** @var stdClass Test database activity. */ protected $database; /** @var stdClass Test group 1. */ protected $group1; /** @var stdClass Test group 2. */ protected $group2; /** @var stdClass Test student 1. */ protected $student1; /** @var stdClass Test student 2. */ protected $student2; /** @var stdClass Test student 3. */ protected $student3; /** @var stdClass Test student 4. */ protected $student4; /** @var stdClass Student role. */ protected $studentrole; /** @var stdClass Test teacher. */ protected $teacher; /** @var stdClass Teacher role. */ protected $teacherrole; /** * Set up for every test */ public function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = new \stdClass(); $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; $this->course = $this->getDataGenerator()->create_course($course); $this->database = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id)); $this->context = \context_module::instance($this->database->cmid); $this->cm = get_coursemodule_from_instance('data', $this->database->id); // Create users. $this->student1 = self::getDataGenerator()->create_user(['firstname' => 'Olivia', 'lastname' => 'Smith']); $this->student2 = self::getDataGenerator()->create_user(['firstname' => 'Ezra', 'lastname' => 'Johnson']); $this->student3 = self::getDataGenerator()->create_user(['firstname' => 'Amelia', 'lastname' => 'Williams']); $this->teacher = self::getDataGenerator()->create_user(['firstname' => 'Asher', 'lastname' => 'Jones']); // Users enrolments. $this->studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($this->student1->id, $this->course->id, $this->studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($this->student2->id, $this->course->id, $this->studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($this->student3->id, $this->course->id, $this->studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual'); $this->group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id)); $this->group2 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id)); groups_add_member($this->group1, $this->student1); groups_add_member($this->group1, $this->student2); groups_add_member($this->group2, $this->student3); } /** * Add a test field to the database activity instance to be used in the unit tests. * * @return \data_field_base */ protected function add_test_field(): \data_field_base { $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Add fields. $fieldrecord = new \stdClass(); $fieldrecord->name = 'Test field'; // Identifier of the records for testing. $fieldrecord->type = 'text'; return $generator->create_field($fieldrecord, $this->database); } /** * Test get databases by courses */ public function test_mod_data_get_databases_by_courses(): void { global $DB; $this->resetAfterTest(true); // Create users. $student = self::getDataGenerator()->create_user(); $teacher = self::getDataGenerator()->create_user(); // Set to the student user. self::setUser($student); // Create courses to add the modules. $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); // First database. $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $course1->id; // Set multilang text to check that is properly filtered to "en" only. $record->name = 'EnglishEspañol'; $record->intro = ''; $database1 = self::getDataGenerator()->create_module('data', $record); // Second database. $record = new \stdClass(); $record->introformat = FORMAT_HTML; $record->course = $course2->id; $database2 = self::getDataGenerator()->create_module('data', $record); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); // Users enrolments. $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, $teacherrole->id, 'manual'); // Execute real Moodle enrolment as we'll call unenrol() method on the instance later. $enrol = enrol_get_plugin('manual'); $enrolinstances = enrol_get_instances($course2->id, true); foreach ($enrolinstances as $courseenrolinstance) { if ($courseenrolinstance->enrol == "manual") { $instance2 = $courseenrolinstance; break; } } $enrol->enrol_user($instance2, $student->id, $studentrole->id); // Enable multilang filter to on content and heading. \filter_manager::reset_caches(); filter_set_global_state('multilang', TEXTFILTER_ON); filter_set_applies_to_strings('multilang', true); // Set WS filtering. $wssettings = external_settings::get_instance(); $wssettings->set_filter(true); // Create what we expect to be returned when querying the two courses. // First for the student user. $expectedfields = array('id', 'coursemodule', 'course', 'name', 'comments', 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto', 'requiredentries', 'requiredentriestoview', 'intro', 'introformat', 'introfiles', 'lang', 'maxentries', 'rssarticles', 'singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate', 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'defaultsort', 'defaultsortdir', 'manageapproved'); // Add expected coursemodule. $database1->coursemodule = $database1->cmid; $database1->introfiles = []; $database1->lang = ''; $database2->coursemodule = $database2->cmid; $database2->introfiles = []; $database2->lang = ''; $expected1 = array(); $expected2 = array(); foreach ($expectedfields as $field) { if ($field == 'approval' or $field == 'manageapproved') { $database1->{$field} = (bool) $database1->{$field}; $database2->{$field} = (bool) $database2->{$field}; } $expected1[$field] = $database1->{$field}; $expected2[$field] = $database2->{$field}; } $expected1['name'] = 'English'; // Lang filtered expected. $expected1['comments'] = (bool) $expected1['comments']; $expected2['comments'] = (bool) $expected2['comments']; $expecteddatabases = array(); $expecteddatabases[] = $expected2; $expecteddatabases[] = $expected1; // Call the external function passing course ids. $result = mod_data_external::get_databases_by_courses(array($course2->id, $course1->id)); $result = external_api::clean_returnvalue(mod_data_external::get_databases_by_courses_returns(), $result); $this->assertEquals($expecteddatabases, $result['databases']); // Call the external function without passing course id. $result = mod_data_external::get_databases_by_courses(); $result = external_api::clean_returnvalue(mod_data_external::get_databases_by_courses_returns(), $result); $this->assertEquals($expecteddatabases, $result['databases']); // Unenrol user from second course and alter expected databases. $enrol->unenrol_user($instance2, $student->id); array_shift($expecteddatabases); // Call the external function without passing course id. $result = mod_data_external::get_databases_by_courses(); $result = external_api::clean_returnvalue(mod_data_external::get_databases_by_courses_returns(), $result); $this->assertEquals($expecteddatabases, $result['databases']); // Call for the second course we unenrolled the user from, expected warning. $result = mod_data_external::get_databases_by_courses(array($course2->id)); $this->assertCount(1, $result['warnings']); $this->assertEquals('1', $result['warnings'][0]['warningcode']); $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); // Now, try as a teacher for getting all the additional fields. self::setUser($teacher); $additionalfields = array('scale', 'assessed', 'assesstimestart', 'assesstimefinish', 'editany', 'notification', 'timemodified'); foreach ($additionalfields as $field) { if ($field == 'editany') { $database1->{$field} = (bool) $database1->{$field}; } $expecteddatabases[0][$field] = $database1->{$field}; } $result = mod_data_external::get_databases_by_courses(); $result = external_api::clean_returnvalue(mod_data_external::get_databases_by_courses_returns(), $result); $this->assertEquals($expecteddatabases, $result['databases']); // Admin should get all the information. self::setAdminUser(); $result = mod_data_external::get_databases_by_courses(array($course1->id)); $result = external_api::clean_returnvalue(mod_data_external::get_databases_by_courses_returns(), $result); $this->assertEquals($expecteddatabases, $result['databases']); } /** * Test view_database invalid id. */ public function test_view_database_invalid_id(): void { // Test invalid instance id. $this->expectException('moodle_exception'); mod_data_external::view_database(0); } /** * Test view_database not enrolled user. */ public function test_view_database_not_enrolled_user(): void { $usernotenrolled = self::getDataGenerator()->create_user(); $this->setUser($usernotenrolled); $this->expectException('moodle_exception'); mod_data_external::view_database(0); } /** * Test view_database no capabilities. */ public function test_view_database_no_capabilities(): void { // Test user with no capabilities. // We need a explicit prohibit since this capability is allowed for students by default. assign_capability('mod/data:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id); accesslib_clear_all_caches_for_unit_testing(); $this->expectException('moodle_exception'); mod_data_external::view_database(0); } /** * Test view_database. */ public function test_view_database(): void { // Test user with full capabilities. $this->setUser($this->student1); // Trigger and capture the event. $sink = $this->redirectEvents(); $result = mod_data_external::view_database($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::view_database_returns(), $result); $events = $sink->get_events(); $this->assertCount(1, $events); $event = array_shift($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_data\event\course_module_viewed', $event); $this->assertEquals($this->context, $event->get_context()); $moodledata = new \moodle_url('/mod/data/view.php', array('id' => $this->cm->id)); $this->assertEquals($moodledata, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test get_data_access_information for student. */ public function test_get_data_access_information_student(): void { global $DB; // Add a field to database to let users add new entries. $this->add_test_field(); // Modify the database to add access restrictions. $this->database->timeavailablefrom = time() + DAYSECS; $this->database->requiredentries = 2; $this->database->requiredentriestoview = 2; $DB->update_record('data', $this->database); // Test user with full capabilities. $this->setUser($this->student1); $result = mod_data_external::get_data_access_information($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result); $this->assertEquals($this->group1->id, $result['groupid']); $this->assertFalse($result['canmanageentries']); $this->assertFalse($result['canapprove']); $this->assertTrue($result['canaddentry']); // It return true because it doen't check time restrictions. $this->assertFalse($result['timeavailable']); $this->assertFalse($result['inreadonlyperiod']); $this->assertEquals(0, $result['numentries']); $this->assertEquals($this->database->requiredentries, $result['entrieslefttoadd']); $this->assertEquals($this->database->requiredentriestoview, $result['entrieslefttoview']); } /** * Test get_data_access_information for teacher. */ public function test_get_data_access_information_teacher(): void { global $DB; // Add a field to database to let users add new entries. $this->add_test_field(); // Modify the database to add access restrictions. $this->database->timeavailablefrom = time() + DAYSECS; $this->database->requiredentries = 2; $this->database->requiredentriestoview = 2; $DB->update_record('data', $this->database); // Test user with full capabilities. $this->setUser($this->teacher); $result = mod_data_external::get_data_access_information($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result); $this->assertEquals(0, $result['groupid']); $this->assertTrue($result['canmanageentries']); $this->assertTrue($result['canapprove']); $this->assertTrue($result['canaddentry']); // It return true because it doen't check time restrictions. $this->assertTrue($result['timeavailable']); $this->assertFalse($result['inreadonlyperiod']); $this->assertEquals(0, $result['numentries']); $this->assertEquals(0, $result['entrieslefttoadd']); $this->assertEquals(0, $result['entrieslefttoview']); } /** * Test get_data_access_information with groups. */ public function test_get_data_access_information_groups(): void { global $DB; // Add a field to database to let users add new entries. $this->add_test_field(); $DB->set_field('course', 'groupmode', VISIBLEGROUPS, ['id' => $this->course->id]); // Check I can see my group. $this->setUser($this->student1); $result = mod_data_external::get_data_access_information($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result); $this->assertEquals($this->group1->id, $result['groupid']); // My group is correctly found. $this->assertFalse($result['canmanageentries']); $this->assertFalse($result['canapprove']); $this->assertTrue($result['canaddentry']); // I can entries in my groups. $this->assertTrue($result['timeavailable']); $this->assertFalse($result['inreadonlyperiod']); $this->assertEquals(0, $result['numentries']); $this->assertEquals(0, $result['entrieslefttoadd']); $this->assertEquals(0, $result['entrieslefttoview']); // Check the other course group in visible groups mode. $result = mod_data_external::get_data_access_information($this->database->id, $this->group2->id); $result = external_api::clean_returnvalue(mod_data_external::get_data_access_information_returns(), $result); $this->assertEquals($this->group2->id, $result['groupid']); // The group is correctly found. $this->assertFalse($result['canmanageentries']); $this->assertFalse($result['canapprove']); $this->assertFalse($result['canaddentry']); // I cannot add entries in other groups. $this->assertTrue($result['timeavailable']); $this->assertFalse($result['inreadonlyperiod']); $this->assertEquals(0, $result['numentries']); $this->assertEquals(0, $result['entrieslefttoadd']); $this->assertEquals(0, $result['entrieslefttoview']); } /** * Helper method to populate the database with some entries. * * @return array the entry ids created */ public function populate_database_with_entries() { global $DB; // Force approval. $DB->set_field('data', 'approval', 1, array('id' => $this->database->id)); $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $fieldtypes = array('checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url'); $count = 1; // Creating test Fields with default parameter values. foreach ($fieldtypes as $fieldtype) { $fieldname = 'field-' . $count; $record = new \stdClass(); $record->name = $fieldname; $record->type = $fieldtype; $record->required = 1; $generator->create_field($record, $this->database); $count++; } // Get all the fields created. $fields = $DB->get_records('data_fields', array('dataid' => $this->database->id), 'id'); // Populate with contents, creating a new entry. $contents = array(); $contents[] = array('opt1', 'opt2', 'opt3', 'opt4'); $contents[] = '01-01-2037'; // It should be lower than 2038, to avoid failing on 32-bit windows. $contents[] = 'menu1'; $contents[] = array('multimenu1', 'multimenu2', 'multimenu3', 'multimenu4'); $contents[] = '12345'; $contents[] = 'radioopt1'; $contents[] = 'text for testing'; $contents[] = '

text area testing

'; $contents[] = array('example.url', 'sampleurl'); $count = 0; $fieldcontents = array(); foreach ($fields as $fieldrecord) { $fieldcontents[$fieldrecord->id] = $contents[$count++]; } $this->setUser($this->student1); $entry11 = $generator->create_entry($this->database, $fieldcontents, $this->group1->id, ['Cats', 'Dogs']); $this->setUser($this->student2); $entry12 = $generator->create_entry($this->database, $fieldcontents, $this->group1->id, ['Cats']); $entry13 = $generator->create_entry($this->database, $fieldcontents, $this->group1->id); // Entry not in group. $entry14 = $generator->create_entry($this->database, $fieldcontents, 0); $this->setUser($this->student3); $entry21 = $generator->create_entry($this->database, $fieldcontents, $this->group2->id); // Approve all except $entry13. $DB->set_field('data_records', 'approved', 1, ['id' => $entry11]); $DB->set_field('data_records', 'approved', 1, ['id' => $entry12]); $DB->set_field('data_records', 'approved', 1, ['id' => $entry14]); $DB->set_field('data_records', 'approved', 1, ['id' => $entry21]); return [$entry11, $entry12, $entry13, $entry14, $entry21]; } /** * Test get_entries */ public function test_get_entries(): void { global $DB; // Check the behaviour when the database has no entries. $result = mod_data_external::get_entries($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertEmpty($result['entries']); $result = mod_data_external::get_entries($this->database->id, 0, true); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertEmpty($result['entries']); $this->assertEmpty($result['listviewcontents']); // Add a few fields to the database. list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // First of all, expect to see only my group entries (not other users in other groups ones). // We may expect entries without group also. $this->setUser($this->student1); $result = mod_data_external::get_entries($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(3, $result['entries']); $this->assertEquals(3, $result['totalcount']); $this->assertEquals($entry11, $result['entries'][0]['id']); $this->assertCount(2, $result['entries'][0]['tags']); $this->assertEquals($this->student1->id, $result['entries'][0]['userid']); $this->assertEquals($this->group1->id, $result['entries'][0]['groupid']); $this->assertEquals($this->database->id, $result['entries'][0]['dataid']); $this->assertEquals($entry12, $result['entries'][1]['id']); $this->assertCount(1, $result['entries'][1]['tags']); $this->assertEquals('Cats', $result['entries'][1]['tags'][0]['rawname']); $this->assertEquals($this->student2->id, $result['entries'][1]['userid']); $this->assertEquals($this->group1->id, $result['entries'][1]['groupid']); $this->assertEquals($this->database->id, $result['entries'][1]['dataid']); $this->assertEquals($entry14, $result['entries'][2]['id']); $this->assertEquals($this->student2->id, $result['entries'][2]['userid']); $this->assertEquals(0, $result['entries'][2]['groupid']); $this->assertEquals($this->database->id, $result['entries'][2]['dataid']); // Other user in same group. $this->setUser($this->student2); $result = mod_data_external::get_entries($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(4, $result['entries']); // I can see my entry is pending approval. $this->assertEquals(4, $result['totalcount']); // Now try with the user in the second group that must see only two entries (his group entry and the one without group). $this->setUser($this->student3); $result = mod_data_external::get_entries($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(2, $result['entries']); $this->assertEquals(2, $result['totalcount']); $this->assertEquals($entry14, $result['entries'][0]['id']); $this->assertEquals($this->student2->id, $result['entries'][0]['userid']); $this->assertEquals(0, $result['entries'][0]['groupid']); $this->assertEquals($this->database->id, $result['entries'][0]['dataid']); $this->assertEquals($entry21, $result['entries'][1]['id']); $this->assertEquals($this->student3->id, $result['entries'][1]['userid']); $this->assertEquals($this->group2->id, $result['entries'][1]['groupid']); $this->assertEquals($this->database->id, $result['entries'][1]['dataid']); // Now, as teacher we should see all (we have permissions to view all groups). $this->setUser($this->teacher); $result = mod_data_external::get_entries($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(5, $result['entries']); // I can see the not approved one. $this->assertEquals(5, $result['totalcount']); $entries = $DB->get_records('data_records', array('dataid' => $this->database->id), 'id'); $this->assertCount(5, $entries); $count = 0; foreach ($entries as $entry) { $this->assertEquals($entry->id, $result['entries'][$count]['id']); $count++; } // Basic test passing the parameter (instead having to calculate it). $this->setUser($this->student1); $result = mod_data_external::get_entries($this->database->id, $this->group1->id); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(3, $result['entries']); $this->assertEquals(3, $result['totalcount']); // Test ordering (reverse). $this->setUser($this->student1); $result = mod_data_external::get_entries($this->database->id, $this->group1->id, false, null, 'DESC'); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(3, $result['entries']); $this->assertEquals(3, $result['totalcount']); $this->assertEquals($entry14, $result['entries'][0]['id']); // Test pagination. $this->setUser($this->student1); $result = mod_data_external::get_entries($this->database->id, $this->group1->id, false, null, null, 0, 1); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['entries']); $this->assertEquals(3, $result['totalcount']); $this->assertEquals($entry11, $result['entries'][0]['id']); $result = mod_data_external::get_entries($this->database->id, $this->group1->id, false, null, null, 1, 1); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['entries']); $this->assertEquals(3, $result['totalcount']); $this->assertEquals($entry12, $result['entries'][0]['id']); // Now test the return contents. data_generate_default_template($this->database, 'listtemplate', 0, false, true); // Generate a default list template. $result = mod_data_external::get_entries($this->database->id, $this->group1->id, true, null, null, 0, 2); $result = external_api::clean_returnvalue(mod_data_external::get_entries_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(2, $result['entries']); $this->assertEquals(3, $result['totalcount']); $this->assertCount(9, $result['entries'][0]['contents']); $this->assertCount(9, $result['entries'][1]['contents']); // Search for some content. $this->assertTrue(strpos($result['listviewcontents'], 'opt1') !== false); $this->assertTrue(strpos($result['listviewcontents'], 'January') !== false); $this->assertTrue(strpos($result['listviewcontents'], 'menu1') !== false); $this->assertTrue(strpos($result['listviewcontents'], 'text for testing') !== false); $this->assertTrue(strpos($result['listviewcontents'], 'sampleurl') !== false); } /** * Test get_entry_visible_groups. */ public function test_get_entry_visible_groups(): void { global $DB; $DB->set_field('course', 'groupmode', VISIBLEGROUPS, ['id' => $this->course->id]); list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // Check I can see my approved group entries. $this->setUser($this->student1); $result = mod_data_external::get_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals($entry11, $result['entry']['id']); $this->assertTrue($result['entry']['approved']); $this->assertTrue($result['entry']['canmanageentry']); // Is mine, I can manage it. // Entry from other group. $result = mod_data_external::get_entry($entry21); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals($entry21, $result['entry']['id']); } /** * Test get_entry_separated_groups. */ public function test_get_entry_separated_groups(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // Check I can see my approved group entries. $this->setUser($this->student1); $result = mod_data_external::get_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals($entry11, $result['entry']['id']); $this->assertTrue($result['entry']['approved']); $this->assertTrue($result['entry']['canmanageentry']); // Is mine, I can manage it. // Retrieve contents. data_generate_default_template($this->database, 'singletemplate', 0, false, true); $result = mod_data_external::get_entry($entry11, true); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(9, $result['entry']['contents']); $this->assertTrue(strpos($result['entryviewcontents'], 'opt1') !== false); $this->assertTrue(strpos($result['entryviewcontents'], 'January') !== false); $this->assertTrue(strpos($result['entryviewcontents'], 'menu1') !== false); $this->assertTrue(strpos($result['entryviewcontents'], 'text for testing') !== false); $this->assertTrue(strpos($result['entryviewcontents'], 'sampleurl') !== false); // This is in my group but I'm not the author. $result = mod_data_external::get_entry($entry12); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals($entry12, $result['entry']['id']); $this->assertTrue($result['entry']['approved']); $this->assertFalse($result['entry']['canmanageentry']); // Not mine. $this->setUser($this->student3); $result = mod_data_external::get_entry($entry21); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertEquals($entry21, $result['entry']['id']); $this->assertTrue($result['entry']['approved']); $this->assertTrue($result['entry']['canmanageentry']); // Is mine, I can manage it. // As teacher I should be able to see all the entries. $this->setUser($this->teacher); $result = mod_data_external::get_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertEquals($entry11, $result['entry']['id']); $result = mod_data_external::get_entry($entry12); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertEquals($entry12, $result['entry']['id']); // This is the not approved one. $result = mod_data_external::get_entry($entry13); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertEquals($entry13, $result['entry']['id']); $result = mod_data_external::get_entry($entry21); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertEquals($entry21, $result['entry']['id']); // Now, try to get a pending approval. $this->setUser($this->student1); $this->expectException('moodle_exception'); $result = mod_data_external::get_entry($entry13); } /** * Test get_entry from other group in separated groups. */ public function test_get_entry_other_group_separated_groups(): void { list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // We should not be able to view other gropu entries (in separated groups). $this->setUser($this->student1); $this->expectException('moodle_exception'); $result = mod_data_external::get_entry($entry21); } /** * Test get_fields. */ public function test_get_fields(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $result = mod_data_external::get_fields($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_fields_returns(), $result); // Basically compare we retrieve all the fields and the correct values. $fields = $DB->get_records('data_fields', array('dataid' => $this->database->id), 'id'); foreach ($result['fields'] as $field) { $this->assertEquals($field, (array) $fields[$field['id']]); } } /** * Test get_fields_database_without_fields. */ public function test_get_fields_database_without_fields(): void { $this->setUser($this->student1); $result = mod_data_external::get_fields($this->database->id); $result = external_api::clean_returnvalue(mod_data_external::get_fields_returns(), $result); $this->assertEmpty($result['fields']); } /** * Test search_entries. */ public function test_search_entries(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); // Empty search, it should return all the visible entries. $result = mod_data_external::search_entries($this->database->id, 0, false); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(3, $result['entries']); $this->assertEquals(3, $result['totalcount']); // Search for something that does not exists. $result = mod_data_external::search_entries($this->database->id, 0, false, 'abc'); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(0, $result['entries']); $this->assertEquals(0, $result['totalcount']); // Search by text matching all the entries. $result = mod_data_external::search_entries($this->database->id, 0, false, 'text'); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(3, $result['entries']); $this->assertEquals(3, $result['totalcount']); $this->assertEquals(3, $result['maxcount']); // Now as the other student I should receive my not approved entry. Apply ordering here. $this->setUser($this->student2); $result = mod_data_external::search_entries($this->database->id, 0, false, 'text', [], DATA_APPROVED, 'ASC'); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(4, $result['entries']); $this->assertEquals(4, $result['totalcount']); $this->assertEquals(4, $result['maxcount']); // The not approved one should be the first. $this->assertEquals($entry13, $result['entries'][0]['id']); // Now as the other group student. $this->setUser($this->student3); $result = mod_data_external::search_entries($this->database->id, 0, false, 'text'); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(2, $result['entries']); $this->assertEquals(2, $result['totalcount']); $this->assertEquals(2, $result['maxcount']); $this->assertEquals($this->student2->id, $result['entries'][0]['userid']); $this->assertEquals($this->student3->id, $result['entries'][1]['userid']); // Same normal text search as teacher. $this->setUser($this->teacher); $result = mod_data_external::search_entries($this->database->id, 0, false, 'text'); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(5, $result['entries']); // I can see all groups and non approved. $this->assertEquals(5, $result['totalcount']); $this->assertEquals(5, $result['maxcount']); // Pagination. $this->setUser($this->teacher); $result = mod_data_external::search_entries($this->database->id, 0, false, 'text', [], DATA_TIMEADDED, 'ASC', 0, 2); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(2, $result['entries']); // Only 2 per page. $this->assertEquals(5, $result['totalcount']); $this->assertEquals(5, $result['maxcount']); // Now advanced search or not dinamic fields (user firstname for example). $this->setUser($this->student1); $advsearch = [ ['name' => 'fn', 'value' => json_encode($this->student2->firstname)] ]; $result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(2, $result['entries']); $this->assertEquals(2, $result['totalcount']); $this->assertEquals(3, $result['maxcount']); $this->assertEquals($this->student2->id, $result['entries'][0]['userid']); // I only found mine! // Advanced search for fields. $field = $DB->get_record('data_fields', array('type' => 'url')); $advsearch = [ ['name' => 'f_' . $field->id , 'value' => 'sampleurl'] ]; $result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(3, $result['entries']); // Found two entries matching this. $this->assertEquals(3, $result['totalcount']); $this->assertEquals(3, $result['maxcount']); // Combined search. $field2 = $DB->get_record('data_fields', array('type' => 'number')); $advsearch = [ ['name' => 'f_' . $field->id , 'value' => 'sampleurl'], ['name' => 'f_' . $field2->id , 'value' => '12345'], ['name' => 'ln', 'value' => json_encode($this->student2->lastname)] ]; $result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(2, $result['entries']); // Only one matching everything. $this->assertEquals(2, $result['totalcount']); $this->assertEquals(3, $result['maxcount']); // Combined search (no results). $field2 = $DB->get_record('data_fields', array('type' => 'number')); $advsearch = [ ['name' => 'f_' . $field->id , 'value' => 'sampleurl'], ['name' => 'f_' . $field2->id , 'value' => '98780333'], // Non existent number. ]; $result = mod_data_external::search_entries($this->database->id, 0, false, '', $advsearch); $result = external_api::clean_returnvalue(mod_data_external::search_entries_returns(), $result); $this->assertCount(0, $result['entries']); // Only one matching everything. $this->assertEquals(0, $result['totalcount']); $this->assertEquals(3, $result['maxcount']); } /** * Test approve_entry. */ public function test_approve_entry(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->teacher); $this->assertEquals(0, $DB->get_field('data_records', 'approved', array('id' => $entry13))); $result = mod_data_external::approve_entry($entry13); $result = external_api::clean_returnvalue(mod_data_external::approve_entry_returns(), $result); $this->assertEquals(1, $DB->get_field('data_records', 'approved', array('id' => $entry13))); } /** * Test unapprove_entry. */ public function test_unapprove_entry(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->teacher); $this->assertEquals(1, $DB->get_field('data_records', 'approved', array('id' => $entry11))); $result = mod_data_external::approve_entry($entry11, false); $result = external_api::clean_returnvalue(mod_data_external::approve_entry_returns(), $result); $this->assertEquals(0, $DB->get_field('data_records', 'approved', array('id' => $entry11))); } /** * Test approve_entry missing permissions. */ public function test_approve_entry_missing_permissions(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $this->expectException('moodle_exception'); mod_data_external::approve_entry($entry13); } /** * Test delete_entry as teacher. Check I can delete any entry. */ public function test_delete_entry_as_teacher(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->teacher); $result = mod_data_external::delete_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::delete_entry_returns(), $result); $this->assertEquals(0, $DB->count_records('data_records', array('id' => $entry11))); // Entry in other group. $result = mod_data_external::delete_entry($entry21); $result = external_api::clean_returnvalue(mod_data_external::delete_entry_returns(), $result); $this->assertEquals(0, $DB->count_records('data_records', array('id' => $entry21))); } /** * Test delete_entry as student. Check I can delete my own entries. */ public function test_delete_entry_as_student(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $result = mod_data_external::delete_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::delete_entry_returns(), $result); $this->assertEquals(0, $DB->count_records('data_records', array('id' => $entry11))); } /** * Test delete_entry as student in read only mode period. Check I cannot delete my own entries in that period. */ public function test_delete_entry_as_student_in_read_only_period(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // Set a time period. $this->database->timeviewfrom = time() - HOURSECS; $this->database->timeviewto = time() + HOURSECS; $DB->update_record('data', $this->database); $this->setUser($this->student1); $this->expectException('moodle_exception'); mod_data_external::delete_entry($entry11); } /** * Test delete_entry with an user missing permissions. */ public function test_delete_entry_missing_permissions(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $this->expectException('moodle_exception'); mod_data_external::delete_entry($entry21); } /** * Test add_entry. */ public function test_add_entry(): void { global $DB; // First create the record structure and add some entries. list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $newentrydata = []; $fields = $DB->get_records('data_fields', array('dataid' => $this->database->id), 'id'); // Prepare the new entry data. foreach ($fields as $field) { $subfield = $value = ''; switch ($field->type) { case 'checkbox': $value = ['opt1', 'opt2']; break; case 'date': // Add two extra. $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'day', 'value' => json_encode('5') ]; $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'month', 'value' => json_encode('1') ]; $subfield = 'year'; $value = '1981'; break; case 'menu': $value = 'menu1'; break; case 'multimenu': $value = ['multimenu1', 'multimenu4']; break; case 'number': $value = 6; break; case 'radiobutton': $value = 'radioopt1'; break; case 'text': $value = 'some text'; break; case 'textarea': $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'content1', 'value' => json_encode(FORMAT_MOODLE) ]; $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'itemid', 'value' => json_encode(0) ]; $value = 'more text'; break; case 'url': $value = 'https://moodle.org'; $subfield = 0; break; } $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => $subfield, 'value' => json_encode($value) ]; } $result = mod_data_external::add_entry($this->database->id, 0, $newentrydata); $result = external_api::clean_returnvalue(mod_data_external::add_entry_returns(), $result); $newentryid = $result['newentryid']; $result = mod_data_external::get_entry($newentryid, true); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertEquals($this->student1->id, $result['entry']['userid']); $this->assertCount(9, $result['entry']['contents']); foreach ($result['entry']['contents'] as $content) { $field = $fields[$content['fieldid']]; // Stored content same that the one retrieved by WS. $dbcontent = $DB->get_record('data_content', array('fieldid' => $field->id, 'recordid' => $newentryid)); $this->assertEquals($dbcontent->content, $content['content']); // Now double check everything stored is correct. if ($field->type == 'checkbox') { $this->assertEquals('opt1##opt2', $content['content']); continue; } if ($field->type == 'date') { $this->assertEquals(347500800, $content['content']); // Date in gregorian format. continue; } if ($field->type == 'menu') { $this->assertEquals('menu1', $content['content']); continue; } if ($field->type == 'multimenu') { $this->assertEquals('multimenu1##multimenu4', $content['content']); continue; } if ($field->type == 'number') { $this->assertEquals(6, $content['content']); continue; } if ($field->type == 'radiobutton') { $this->assertEquals('radioopt1', $content['content']); continue; } if ($field->type == 'text') { $this->assertEquals('some text', $content['content']); continue; } if ($field->type == 'textarea') { $this->assertEquals('more text', $content['content']); $this->assertEquals(FORMAT_MOODLE, $content['content1']); continue; } if ($field->type == 'url') { $this->assertEquals('https://moodle.org', $content['content']); continue; } $this->assertEquals('multimenu1##multimenu4', $content['content']); } // Now, try to add another entry but removing some required data. unset($newentrydata[0]); $result = mod_data_external::add_entry($this->database->id, 0, $newentrydata); $result = external_api::clean_returnvalue(mod_data_external::add_entry_returns(), $result); $this->assertEquals(0, $result['newentryid']); $this->assertCount(0, $result['generalnotifications']); $this->assertCount(1, $result['fieldnotifications']); $this->assertEquals('field-1', $result['fieldnotifications'][0]['fieldname']); $this->assertEquals(get_string('errormustsupplyvalue', 'data'), $result['fieldnotifications'][0]['notification']); } /** * Test add_entry empty_form. */ public function test_add_entry_empty_form(): void { // Add a field to database to let users add new entries. $this->add_test_field(); $result = mod_data_external::add_entry($this->database->id, 0, []); $result = external_api::clean_returnvalue(mod_data_external::add_entry_returns(), $result); $this->assertEquals(0, $result['newentryid']); $this->assertCount(1, $result['generalnotifications']); $this->assertCount(0, $result['fieldnotifications']); $this->assertEquals(get_string('emptyaddform', 'data'), $result['generalnotifications'][0]); } /** * Test add_entry read_only_period. */ public function test_add_entry_read_only_period(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // Set a time period. $this->database->timeviewfrom = time() - HOURSECS; $this->database->timeviewto = time() + HOURSECS; $DB->update_record('data', $this->database); $this->setUser($this->student1); $this->expectExceptionMessage(get_string('noaccess', 'data')); $this->expectException('moodle_exception'); mod_data_external::add_entry($this->database->id, 0, []); } /** * Test add_entry max_num_entries. */ public function test_add_entry_max_num_entries(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // Set a time period. $this->database->maxentries = 1; $DB->update_record('data', $this->database); $this->setUser($this->student1); $this->expectExceptionMessage(get_string('noaccess', 'data')); $this->expectException('moodle_exception'); mod_data_external::add_entry($this->database->id, 0, []); } /** * Test add_entry invalid group. */ public function test_add_entry_invalid_group(): void { // Add a field to database to let users add new entries. $this->add_test_field(); $this->setUser($this->student1); $this->expectExceptionMessage(get_string('noaccess', 'data')); $this->expectException('moodle_exception'); mod_data_external::add_entry($this->database->id, $this->group2->id, []); } /** * Test add_entry for an empty database (no fields). * * @covers ::add_entry */ public function test_add_entry_empty_database(): void { $this->expectException('moodle_exception'); mod_data_external::add_entry($this->database->id, 0, []); } /** * Test update_entry. */ public function test_update_entry(): void { global $DB; // First create the record structure and add some entries. list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $newentrydata = []; $fields = $DB->get_records('data_fields', array('dataid' => $this->database->id), 'id'); // Prepare the new entry data. foreach ($fields as $field) { $subfield = $value = ''; switch ($field->type) { case 'checkbox': $value = ['opt1', 'opt2']; break; case 'date': // Add two extra. $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'day', 'value' => json_encode('5') ]; $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'month', 'value' => json_encode('1') ]; $subfield = 'year'; $value = '1981'; break; case 'menu': $value = 'menu1'; break; case 'multimenu': $value = ['multimenu1', 'multimenu4']; break; case 'number': $value = 6; break; case 'radiobutton': $value = 'radioopt2'; break; case 'text': $value = 'some text'; break; case 'textarea': $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'content1', 'value' => json_encode(FORMAT_MOODLE) ]; $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => 'itemid', 'value' => json_encode(0) ]; $value = 'more text'; break; case 'url': $value = 'https://moodle.org'; $subfield = 0; break; } $newentrydata[] = [ 'fieldid' => $field->id, 'subfield' => $subfield, 'value' => json_encode($value) ]; } $result = mod_data_external::update_entry($entry11, $newentrydata); $result = external_api::clean_returnvalue(mod_data_external::update_entry_returns(), $result); $this->assertTrue($result['updated']); $this->assertCount(0, $result['generalnotifications']); $this->assertCount(0, $result['fieldnotifications']); $result = mod_data_external::get_entry($entry11, true); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertEquals($this->student1->id, $result['entry']['userid']); $this->assertCount(9, $result['entry']['contents']); foreach ($result['entry']['contents'] as $content) { $field = $fields[$content['fieldid']]; // Stored content same that the one retrieved by WS. $dbcontent = $DB->get_record('data_content', array('fieldid' => $field->id, 'recordid' => $entry11)); $this->assertEquals($dbcontent->content, $content['content']); // Now double check everything stored is correct. if ($field->type == 'checkbox') { $this->assertEquals('opt1##opt2', $content['content']); continue; } if ($field->type == 'date') { $this->assertEquals(347500800, $content['content']); // Date in gregorian format. continue; } if ($field->type == 'menu') { $this->assertEquals('menu1', $content['content']); continue; } if ($field->type == 'multimenu') { $this->assertEquals('multimenu1##multimenu4', $content['content']); continue; } if ($field->type == 'number') { $this->assertEquals(6, $content['content']); continue; } if ($field->type == 'radiobutton') { $this->assertEquals('radioopt2', $content['content']); continue; } if ($field->type == 'text') { $this->assertEquals('some text', $content['content']); continue; } if ($field->type == 'textarea') { $this->assertEquals('more text', $content['content']); $this->assertEquals(FORMAT_MOODLE, $content['content1']); continue; } if ($field->type == 'url') { $this->assertEquals('https://moodle.org', $content['content']); continue; } $this->assertEquals('multimenu1##multimenu4', $content['content']); } // Now, try to update the entry but removing some required data. unset($newentrydata[0]); $result = mod_data_external::update_entry($entry11, $newentrydata); $result = external_api::clean_returnvalue(mod_data_external::update_entry_returns(), $result); $this->assertFalse($result['updated']); $this->assertCount(0, $result['generalnotifications']); $this->assertCount(1, $result['fieldnotifications']); $this->assertEquals('field-1', $result['fieldnotifications'][0]['fieldname']); $this->assertEquals(get_string('errormustsupplyvalue', 'data'), $result['fieldnotifications'][0]['notification']); } /** * Test update_entry sending empty data. */ public function test_update_entry_empty_data(): void { list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student1); $result = mod_data_external::update_entry($entry11, []); $result = external_api::clean_returnvalue(mod_data_external::update_entry_returns(), $result); $this->assertFalse($result['updated']); $this->assertCount(1, $result['generalnotifications']); $this->assertCount(9, $result['fieldnotifications']); $this->assertEquals(get_string('emptyaddform', 'data'), $result['generalnotifications'][0]); } /** * Test update_entry in read only period. */ public function test_update_entry_read_only_period(): void { global $DB; list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); // Set a time period. $this->database->timeviewfrom = time() - HOURSECS; $this->database->timeviewto = time() + HOURSECS; $DB->update_record('data', $this->database); $this->setUser($this->student1); $this->expectExceptionMessage(get_string('noaccess', 'data')); $this->expectException('moodle_exception'); mod_data_external::update_entry($entry11, []); } /** * Test update_entry other_user. */ public function test_update_entry_other_user(): void { // Try to update other user entry. list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $this->setUser($this->student2); $this->expectExceptionMessage(get_string('noaccess', 'data')); $this->expectException('moodle_exception'); mod_data_external::update_entry($entry11, []); } /** * Test get_entry_rating_information. */ public function test_get_entry_rating_information(): void { global $DB, $CFG; require_once($CFG->dirroot . '/rating/lib.php'); $DB->set_field('data', 'assessed', RATING_AGGREGATE_SUM, array('id' => $this->database->id)); $DB->set_field('data', 'scale', 100, array('id' => $this->database->id)); list($entry11, $entry12, $entry13, $entry14, $entry21) = self::populate_database_with_entries(); $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user1->id, $this->course->id, $this->studentrole->id, 'manual'); $this->getDataGenerator()->enrol_user($user2->id, $this->course->id, $this->studentrole->id, 'manual'); // Rate the entry as user1. $rating1 = new \stdClass(); $rating1->contextid = $this->context->id; $rating1->component = 'mod_data'; $rating1->ratingarea = 'entry'; $rating1->itemid = $entry11; $rating1->rating = 50; $rating1->scaleid = 100; $rating1->userid = $user1->id; $rating1->timecreated = time(); $rating1->timemodified = time(); $rating1->id = $DB->insert_record('rating', $rating1); // Rate the entry as user2. $rating2 = new \stdClass(); $rating2->contextid = $this->context->id; $rating2->component = 'mod_data'; $rating2->ratingarea = 'entry'; $rating2->itemid = $entry11; $rating2->rating = 100; $rating2->scaleid = 100; $rating2->userid = $user2->id; $rating2->timecreated = time() + 1; $rating2->timemodified = time() + 1; $rating2->id = $DB->insert_record('rating', $rating2); // As student, retrieve ratings information. $this->setUser($this->student2); $result = mod_data_external::get_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(1, $result['ratinginfo']['ratings']); $this->assertFalse($result['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertFalse($result['ratinginfo']['canviewall']); $this->assertFalse($result['ratinginfo']['ratings'][0]['canrate']); $this->assertTrue(!isset($result['ratinginfo']['ratings'][0]['count'])); // Now, as teacher, I should see the info correctly. $this->setUser($this->teacher); $result = mod_data_external::get_entry($entry11); $result = external_api::clean_returnvalue(mod_data_external::get_entry_returns(), $result); $this->assertCount(1, $result['ratinginfo']['ratings']); $this->assertTrue($result['ratinginfo']['ratings'][0]['canviewaggregate']); $this->assertTrue($result['ratinginfo']['canviewall']); $this->assertTrue($result['ratinginfo']['ratings'][0]['canrate']); $this->assertEquals(2, $result['ratinginfo']['ratings'][0]['count']); $this->assertEquals(100, $result['ratinginfo']['ratings'][0]['aggregate']); // Expect maximium scale value. } } tests/preset_test.php000064400000116603152265517740011005 0ustar00. namespace mod_data; use file_archive; use stdClass; use zip_archive; /** * Preset tests class for mod_data. * * @package mod_data * @category test * @copyright 2022 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data\preset */ final class preset_test extends \advanced_testcase { /** * Test for static create_from_plugin method. * * @covers ::create_from_plugin */ public function test_create_from_plugin(): void { $this->resetAfterTest(); $this->setAdminUser(); // Check create_from_plugin is working as expected when an existing plugin is given. $pluginname = 'imagegallery'; $result = preset::create_from_plugin(null, $pluginname); $this->assertTrue($result->isplugin); $this->assertEquals(get_string('modulename', "datapreset_$pluginname"), $result->name); $this->assertEquals($pluginname, $result->shortname); $this->assertEquals(get_string('modulename_help', "datapreset_$pluginname"), $result->description); $this->assertEmpty($result->get_userid()); $this->assertEmpty($result->storedfile); $this->assertNull($result->get_path()); // Check create_from_plugin is working as expected when an unexisting plugin is given. $pluginname = 'unexisting'; $result = preset::create_from_plugin(null, $pluginname); $this->assertNull($result); } /** * Test for static create_from_storedfile method. * * @covers ::create_from_storedfile */ public function test_create_from_storedfile(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $plugingenerator->create_preset($activity, $record); $savedpresets = $manager->get_available_saved_presets(); $savedpreset = reset($savedpresets); // Check create_from_storedfile is working as expected with a valid preset file. $result = preset::create_from_storedfile($manager, $savedpreset->storedfile); $this->assertFalse($result->isplugin); $this->assertEquals($record->name, $result->name); $this->assertEquals($record->name, $result->shortname); $this->assertEquals($record->description, $result->description); $this->assertEquals($savedpreset->storedfile->get_userid(), $result->get_userid()); $this->assertNotEmpty($result->storedfile); $this->assertEquals('/' . $record->name . '/', $result->get_path()); // Check create_from_storedfile is not creating a preset object when an invalid file is given. $draftid = file_get_unused_draft_itemid(); $filerecord = [ 'component' => 'user', 'filearea' => 'draft', 'contextid' => \context_user::instance($USER->id)->id, 'itemid' => $draftid, 'filename' => 'preset.xml', 'filepath' => '/' ]; $fs = get_file_storage(); $file = $fs->create_file_from_string($filerecord, 'This is the file content'); $result = preset::create_from_storedfile($manager, $file); $this->assertNull($result); } /** * Test for static create_from_instance method. * * @covers ::create_from_instance */ public function test_create_from_instance(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $plugingenerator->create_preset($activity, $record); $savedpresets = $manager->get_available_saved_presets(); $savedpreset = reset($savedpresets); // Check create_from_instance is working as expected when a preset with this name exists. $result = preset::create_from_instance($manager, $record->name, $record->description); $this->assertFalse($result->isplugin); $this->assertEquals($record->name, $result->name); $this->assertEquals($record->name, $result->shortname); $this->assertEquals($record->description, $result->description); $this->assertEquals($savedpreset->storedfile->get_userid(), $result->get_userid()); $this->assertNotEmpty($result->storedfile); $this->assertEquals('/' . $record->name . '/', $result->get_path()); // Check create_from_instance is working as expected when there is no preset with the given name. $presetname = 'Unexisting preset'; $presetdescription = 'This is the description for the unexisting preset'; $result = preset::create_from_instance($manager, $presetname, $presetdescription); $this->assertFalse($result->isplugin); $this->assertEquals($presetname, $result->name); $this->assertEquals($presetname, $result->shortname); $this->assertEquals($presetdescription, $result->description); $this->assertEmpty($result->get_userid()); $this->assertEmpty($result->storedfile); $this->assertEquals('/' . $presetname . '/', $result->get_path()); } /** * Test for static create_from_fullname method. * * @covers ::create_from_fullname */ public function test_create_from_fullname(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $savedpreset = $plugingenerator->create_preset($activity, $record); // Check instantiate from plugin. $pluginname = 'imagegallery'; $fullname = '0/imagegallery'; $result = preset::create_from_fullname($manager, $fullname); $this->assertTrue($result->isplugin); $this->assertEquals(get_string('modulename', "datapreset_$pluginname"), $result->name); $this->assertEquals($pluginname, $result->shortname); $this->assertEquals(get_string('modulename_help', "datapreset_$pluginname"), $result->description); $this->assertEmpty($result->get_userid()); $this->assertEmpty($result->storedfile); $this->assertNull($result->get_path()); // Check instantiate from user preset // Check create_from_instance is working as expected when a preset with this name exists. $fullname = $savedpreset->get_userid() . '/' . $savedpreset->name; $result = preset::create_from_fullname($manager, $fullname); $this->assertFalse($result->isplugin); $this->assertEquals($savedpreset->name, $result->name); $this->assertEquals($savedpreset->shortname, $result->shortname); $this->assertEquals($savedpreset->description, $savedpreset->description); $this->assertEquals($savedpreset->storedfile->get_userid(), $result->get_userid()); $this->assertNotEmpty($result->storedfile); $this->assertEquals('/' . $savedpreset->name . '/', $result->get_path()); } /** * Test for the save a preset method when the preset hasn't been saved before. * * @covers ::save */ public function test_save_new_preset(): void { $this->resetAfterTest(); $this->setAdminUser(); // Save should return false when trying to save a plugin preset. $preset = preset::create_from_plugin(null, 'imagegallery'); $result = $preset->save(); $this->assertFalse($result); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $plugingenerator->create_preset($activity, $record); // Save should return false when trying to save an existing saved preset. $preset = preset::create_from_instance($manager, $record->name, $record->description); $result = $preset->save(); $this->assertFalse($result); // The preset should be saved when it's new and there is no any other having the same name. $savedpresets = $manager->get_available_saved_presets(); $this->assertCount(1, $savedpresets); $presetname = 'New preset'; $presetdescription = 'This is the description for the new preset'; $preset = preset::create_from_instance($manager, $presetname, $presetdescription); $result = $preset->save(); $this->assertTrue($result); // Check the preset has been created. $savedpresets = $manager->get_available_saved_presets(); $this->assertCount(2, $savedpresets); $savedpresetsnames = array_map(function($preset) { return $preset->name; }, $savedpresets); $this->assertContains($presetname, $savedpresetsnames); } /** * Test for the save a preset method when is an existing preset that has been saved before. * * @covers ::save */ public function test_save_existing_preset(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $oldpresetname = $record->name; $plugingenerator->create_preset($activity, $record); // Save should return false when trying to save an existing preset. $preset = preset::create_from_instance($manager, $record->name, $record->description); $result = $preset->save(); $this->assertFalse($result); // Check no new preset has been created. $this->assertCount(1, $manager->get_available_saved_presets()); // Save should overwrite existing preset if name or description have changed. $preset->name = 'New preset name'; $preset->description = 'New preset description'; $result = $preset->save(); $this->assertTrue($result); // Check the preset files have been renamed. $presetfiles = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml', '.']); foreach ($presetfiles as $templatefile) { $file = preset::get_file($preset->get_path(), $templatefile); $this->assertNotNull($file); } // Check old preset files have been removed. $oldpath = "{$oldpresetname}"; foreach ($presetfiles as $templatefile) { $file = preset::get_file($oldpath, $templatefile); $this->assertNull($file); } // Check no new preset has been created. $savedpresets = $manager->get_available_saved_presets(); $this->assertCount(1, $savedpresets); // Check the preset has the expected values. $savedpreset = reset($savedpresets); $this->assertEquals($preset->name, $savedpreset->name); $this->assertEquals($preset->description, $savedpreset->description); $this->assertNotEmpty($preset->storedfile); // Check the storedfile has been updated properly. $this->assertEquals($preset->name, trim($savedpreset->storedfile->get_filepath(), '/')); // Create another saved preset with empty description. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset 2', ]; $plugingenerator->create_preset($activity, $record); $this->assertCount(2, $manager->get_available_saved_presets()); // Description should be saved too when it was empty in the original preset and a new value is assigned to it. $preset = preset::create_from_instance($manager, $record->name); $preset->description = 'New preset description'; $result = $preset->save(); $this->assertTrue($result); $savedpresets = $manager->get_available_saved_presets(); $this->assertCount(2, $savedpresets); foreach ($savedpresets as $savedpreset) { if ($savedpreset->name == $record->name) { $this->assertEquals($preset->description, $savedpreset->description); } } } /** * Test for the export a preset method. * * @covers ::export */ public function test_export(): void { $this->resetAfterTest(); $this->setAdminUser(); // Export should return empty string when trying to export a plugin preset. $preset = preset::create_from_plugin(null, 'imagegallery'); $result = $preset->export(); $this->assertEmpty($result); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); // For now, default templates are not created automatically. This will be changed in MDL-75234. foreach (manager::TEMPLATES_LIST as $templatename => $notused) { data_generate_default_template($activity, $templatename); } $preset = preset::create_from_instance($manager, $activity->name); $result = $preset->export(); $presetfilenames = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml']); $ziparchive = new zip_archive(); $ziparchive->open($result, file_archive::OPEN); $files = $ziparchive->list_files(); foreach ($files as $file) { $this->assertContains($file->pathname, $presetfilenames); // Check the file is not empty (except CSS, JS and listtemplateheader/footer files which are empty by default). $extension = pathinfo($file->pathname, PATHINFO_EXTENSION); $ishtmlorxmlfile = in_array($extension, ['html', 'xml']); $expectedemptyfiles = array_intersect_key(manager::TEMPLATES_LIST, array_flip([ 'listtemplateheader', 'listtemplatefooter', 'rsstitletemplate', ])); if ($ishtmlorxmlfile && !in_array($file->pathname, $expectedemptyfiles)) { $this->assertGreaterThan(0, $file->size); } else { $this->assertEquals(0, $file->size); } } $ziparchive->close(); } /** * Test for get_userid(). * * @covers ::get_userid */ public function test_get_userid(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->setUser($user); // Check userid is null for plugin preset. $manager = manager::create_from_instance($activity); $pluginpresets = $manager->get_available_plugin_presets(); $pluginpreset = reset($pluginpresets); $this->assertNull($pluginpreset->get_userid()); // Check userid meets the user that has created the preset when it's a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $savedpreset = (object) [ 'name' => 'Preset created by teacher', ]; $plugingenerator->create_preset($activity, $savedpreset); $savedpresets = $manager->get_available_saved_presets(); $savedpreset = reset($savedpresets); $this->assertEquals($user->id, $savedpreset->get_userid()); // Check userid is null when preset hasn't any file associated. $preset = preset::create_from_instance($manager, 'Unexisting preset'); $this->assertNull($preset->get_userid()); } /** * Test for get_path(). * * @covers ::get_path */ public function test_get_path(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Check path is null for plugin preset. $manager = manager::create_from_instance($activity); $pluginpresets = $manager->get_available_plugin_presets(); $pluginpreset = reset($pluginpresets); $this->assertNull($pluginpreset->get_path()); // Check path meets expected value when it's a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $savedpreset = (object) [ 'name' => 'Saved preset', ]; $plugingenerator->create_preset($activity, $savedpreset); $savedpresets = $manager->get_available_saved_presets(); $savedpreset = reset($savedpresets); $this->assertEquals("/{$savedpreset->name}/", $savedpreset->get_path()); // Check path is /presetname/ when preset hasn't any file associated. $presetname = 'Unexisting preset'; $preset = preset::create_from_instance($manager, $presetname); $this->assertEquals("/{$presetname}/", $preset->get_path()); } /** * Test for is_directory_a_preset(). * * @dataProvider is_directory_a_preset_provider * @covers ::is_directory_a_preset * @param string $directory * @param bool $expected */ public function test_is_directory_a_preset(string $directory, bool $expected): void { $this->resetAfterTest(); $this->setAdminUser(); $result = preset::is_directory_a_preset($directory); $this->assertEquals($expected, $result); } /** * Data provider for test_is_directory_a_preset(). * * @return array */ public static function is_directory_a_preset_provider(): array { global $CFG; return [ 'Valid preset directory' => [ 'directory' => $CFG->dirroot . '/mod/data/preset/imagegallery', 'expected' => true, ], 'Invalid preset directory' => [ 'directory' => $CFG->dirroot . '/mod/data/field/checkbox', 'expected' => false, ], 'Unexisting preset directory' => [ 'directory' => $CFG->dirroot . 'unexistingdirectory', 'expected' => false, ], ]; } /** * Test for get_name_from_plugin(). * * @covers ::get_name_from_plugin */ public function test_get_name_from_plugin(): void { $this->resetAfterTest(); $this->setAdminUser(); // The expected name for plugins with modulename in lang is this value. $name = preset::get_name_from_plugin('imagegallery'); $this->assertEquals('Image gallery', $name); // However, if the plugin doesn't exist or the modulename is not defined, the preset shortname will be returned. $presetshortname = 'nonexistingpreset'; $name = preset::get_name_from_plugin($presetshortname); $this->assertEquals($presetshortname, $name); } /** * Test for get_description_from_plugin(). * * @covers ::get_description_from_plugin */ public function test_get_description_from_plugin(): void { $this->resetAfterTest(); $this->setAdminUser(); // The expected name for plugins with modulename in lang is this value. $description = preset::get_description_from_plugin('imagegallery'); $this->assertEquals('Use this preset to collect images.', $description); // However, if the plugin doesn't exist or the modulename is not defined, empty string will be returned. $presetshortname = 'nonexistingpreset'; $description = preset::get_description_from_plugin($presetshortname); $this->assertEmpty($description); } /** * Test for generate_preset_xml(). * * @covers ::generate_preset_xml * @dataProvider generate_preset_xml_provider * @param array $params activity config settings * @param string|null $description preset description */ public function test_generate_preset_xml(array $params, ?string $description): void { $this->resetAfterTest(); $this->setAdminUser(); // Make accessible the method. $reflection = new \ReflectionClass(preset::class); $method = $reflection->getMethod('generate_preset_xml'); // The method should return empty string when trying to generate preset.xml for a plugin preset. $preset = preset::create_from_plugin(null, 'imagegallery'); $result = $method->invokeArgs($preset, []); $this->assertEmpty($result); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, array_merge(['course' => $course], $params)); // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); $manager = manager::create_from_instance($activity); $preset = preset::create_from_instance($manager, $activity->name, $description); // Call the generate_preset_xml method. $result = $method->invokeArgs($preset, []); // Check is a valid XML. $parsedxml = simplexml_load_string($result); // Check the description has the expected value. $this->assertEquals($description, strval($parsedxml->description)); // Check settings have the expected values. foreach ($params as $paramname => $paramvalue) { $this->assertEquals($paramvalue, strval($parsedxml->settings->{$paramname})); } // Check field have the expected values. $this->assertEquals($fieldrecord->name, strval($parsedxml->field->name)); $this->assertEquals($fieldrecord->type, strval($parsedxml->field->type)); } /** * Data provider for generate_preset_xml(). * * @return array */ public static function generate_preset_xml_provider(): array { return [ 'Generate preset.xml with the default params and empty description' => [ 'params' => [], 'description' => null, ], 'Generate preset.xml with a description but the default params' => [ 'params' => [], 'description' => 'This is a description', ], 'Generate preset.xml with empty description but changing some params' => [ 'params' => [ 'requiredentries' => 2, 'approval' => 1, ], 'description' => null, ], 'Generate preset.xml with a description and changing some params' => [ 'params' => [ 'maxentries' => 5, 'manageapproved' => 0, ], 'description' => 'This is a description', ], ]; } /** * Test for get_file(). * * @covers ::get_file */ public function test_get_file(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); $presetname = 'Saved preset'; // Check file doesn't exist if the preset hasn't been saved yet. $preset = preset::create_from_instance($manager, $presetname); $file = preset::get_file($preset->get_path(), 'preset.xml'); $this->assertNull($file); // Check file is not empty when there is a saved preset with this name. $preset->save(); $file = preset::get_file($preset->get_path(), 'preset.xml'); $this->assertNotNull($file); $this->assertStringContainsString($presetname, $file->get_filepath()); $this->assertEquals('preset.xml', $file->get_filename()); // Check invalid preset file name doesn't exist. $file = preset::get_file($preset->get_path(), 'unexistingpreset.xml'); $this->assertNull($file); } /** * Test for can_manage(). * * @covers ::can_manage */ public function test_can_manage(): void { $this->resetAfterTest(); // Create course, database activity and users. $course = $this->getDataGenerator()->create_course(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $manager = manager::create_from_instance($data); $preset1name = 'Admin preset'; $preset2name = 'Teacher preset'; // Create a saved preset by admin. $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => $preset1name, 'description' => 'Testing preset description', ]; $adminpreset = $plugingenerator->create_preset($data, $record); // Create a saved preset by teacher. $this->setUser($teacher); $record = (object) [ 'name' => $preset2name, 'description' => 'Testing preset description', ]; $teacherpreset = $plugingenerator->create_preset($data, $record); // Plugins can't be deleted. $pluginpresets = manager::get_available_plugin_presets(); $pluginpreset = reset($pluginpresets); $this->assertFalse($pluginpreset->can_manage()); // Admin can delete all saved presets. $this->setAdminUser(); $this->assertTrue($adminpreset->can_manage()); $this->assertTrue($teacherpreset->can_manage()); // Teacher can delete their own preset only. $this->setUser($teacher); $this->assertFalse($adminpreset->can_manage()); $this->assertTrue($teacherpreset->can_manage()); // Student can't delete any of the presets. $this->setUser($student); $this->assertFalse($adminpreset->can_manage()); $this->assertFalse($teacherpreset->can_manage()); } /** * Test for delete(). * * @covers ::delete */ public function test_delete(): void { $this->resetAfterTest(); // Create course, database activity and users. $course = $this->getDataGenerator()->create_course(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id]); $manager = manager::create_from_instance($data); $presetname = 'Admin preset'; // Create a saved preset by admin. $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => $presetname, 'description' => 'Testing preset description', ]; $adminpreset = $plugingenerator->create_preset($data, $record); $initialpresets = $manager->get_available_presets(); // Plugins can't be deleted. $pluginpresets = manager::get_available_plugin_presets(); $pluginpreset = reset($pluginpresets); $result = $pluginpreset->delete(); $currentpluginpresets = manager::get_available_plugin_presets(); $this->assertEquals(count($pluginpresets), count($currentpluginpresets)); $result = $adminpreset->delete(); $this->assertTrue($result); // After deleting the preset, there is no file linked. $adminpreset = preset::create_from_instance($manager, $presetname); $this->assertEmpty($adminpreset->storedfile); // Check the preset has been deleted. $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); // The behavior of trying to delete a preset twice. $result = $adminpreset->delete(); $this->assertFalse($result); // Check the preset has not been deleted. $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); $emptypreset = preset::create_from_instance($manager, $presetname); // The behavior of deleting an empty preset. $result = $emptypreset->delete(); $this->assertFalse($result); // Check the preset has not been deleted. $currentpresets = $manager->get_available_presets(); $this->assertEquals(count($initialpresets) - 1, count($currentpresets)); } /** * Test for the get_fields method. * * @covers ::get_fields */ public function test_get_fields(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $preset = $plugingenerator->create_preset($activity, $record); // Check regular fields. $fields = $preset->get_fields(); $this->assertCount(1, $fields); $this->assertArrayHasKey('field-1', $fields); $field = $fields['field-1']; $this->assertEquals('text', $field->type); $this->assertEquals('field-1', $field->get_name()); $this->assertEquals(false, $field->get_preview()); // Check preview fields. $savedpresets = $manager->get_available_saved_presets(); $preset = reset($savedpresets); $fields = $preset->get_fields(true); $this->assertCount(1, $fields); $this->assertArrayHasKey('field-1', $fields); $field = $fields['field-1']; $this->assertEquals('text', $field->type); $this->assertEquals('field-1', $field->get_name()); $this->assertEquals(true, $field->get_preview()); } /** * Test for the get_sample_entries method. * * @covers ::get_sample_entries */ public function test_get_sample_entries(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Add a field to the activity. $fieldrecord = new stdClass(); $fieldrecord->name = 'field-1'; $fieldrecord->type = 'text'; $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $datagenerator->create_field($fieldrecord, $activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $preset = $plugingenerator->create_preset($activity, $record); $entries = $preset->get_sample_entries(3); $this->assertCount(3, $entries); foreach ($entries as $entry) { $this->assertEquals($user->id, $entry->userid); $this->assertEquals($user->email, $entry->email); $this->assertEquals($user->firstname, $entry->firstname); $this->assertEquals($user->lastname, $entry->lastname); $this->assertEquals($activity->id, $entry->dataid); $this->assertEquals(0, $entry->groupid); $this->assertEquals(1, $entry->approved); } } /** * Test for the get_template_content method. * * @covers ::get_template_content */ public function test_get_template_content(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $course = $this->getDataGenerator()->create_course(); // Module data with templates. $templates = [ 'singletemplate' => 'Single template content', 'listtemplate' => 'List template content', 'listtemplateheader' => 'List template content header', 'listtemplatefooter' => 'List template content footer', 'addtemplate' => 'Add template content', 'rsstemplate' => 'RSS template content', 'rsstitletemplate' => 'RSS title template content', 'csstemplate' => 'CSS template content', 'jstemplate' => 'JS template content', 'asearchtemplate' => 'Advanced search template content', ]; $params = array_merge(['course' => $course], $templates); // Create a database activity. $activity = $this->getDataGenerator()->create_module(manager::MODULE, $params); $manager = manager::create_from_instance($activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $preset = $plugingenerator->create_preset($activity, $record); // Test user preset templates. foreach ($templates as $templatename => $templatecontent) { $content = $preset->get_template_content($templatename); $this->assertEquals($templatecontent, $content); } // Test plugin preset content. $pluginname = 'imagegallery'; $preset = preset::create_from_plugin($manager, $pluginname); foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { // Get real file contents. $path = $manager->path . '/preset/' . $pluginname . '/' . $templatefile; $templatecontent = file_get_contents($path); $content = $preset->get_template_content($templatename); $this->assertEquals($templatecontent, $content); } } /** * Test for the get_fullname method. * * @covers ::get_fullname */ public function test_get_fullname(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $course = $this->getDataGenerator()->create_course(); // Create a database activity. $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Create a saved preset. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $preset = $plugingenerator->create_preset($activity, $record); // Test user preset templates. $this->assertEquals("{$user->id}/Testing preset name", $preset->get_fullname()); // Test plugin preset content. $pluginname = 'imagegallery'; $preset = preset::create_from_plugin($manager, $pluginname); $this->assertEquals("0/imagegallery", $preset->get_fullname()); } } tests/event/events_test.php000064400000043202152265517740012122 0ustar00. /** * Events tests. * * @package mod_data * @category test * @copyright 2014 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_data\event; use mod_data\local\importer\preset_existing_importer; use mod_data\manager; use mod_data\preset; final class events_test extends \advanced_testcase { /** * Test set up. * * This is executed before running any test in this file. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } /** * Test the field created event. */ public function test_field_created(): void { $this->setAdminUser(); // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Now we want to create a field. $field = data_get_field_new('text', $data); $fielddata = new \stdClass(); $fielddata->name = 'Test'; $fielddata->description = 'Test description'; $field->define_field($fielddata); // Trigger and capture the event for creating a field. $sink = $this->redirectEvents(); $field->insert_field(); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\field_created', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/field.php', array('d' => $data->id)); $this->assertEquals($url, $event->get_url()); } /** * Test the field updated event. */ public function test_field_updated(): void { $this->setAdminUser(); // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Now we want to create a field. $field = data_get_field_new('text', $data); $fielddata = new \stdClass(); $fielddata->name = 'Test'; $fielddata->description = 'Test description'; $field->define_field($fielddata); $field->insert_field(); // Trigger and capture the event for updating the field. $sink = $this->redirectEvents(); $field->update_field(); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\field_updated', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/field.php', array('d' => $data->id)); $this->assertEquals($url, $event->get_url()); } /** * Test the field deleted event. */ public function test_field_deleted(): void { $this->setAdminUser(); // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Now we want to create a field. $field = data_get_field_new('text', $data); $fielddata = new \stdClass(); $fielddata->name = 'Test'; $fielddata->description = 'Test description'; $field->define_field($fielddata); $field->insert_field(); // Trigger and capture the event for deleting the field. $sink = $this->redirectEvents(); $field->delete_field(); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\field_deleted', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/field.php', array('d' => $data->id)); $this->assertEquals($url, $event->get_url()); } /** * Test the record created event. */ public function test_record_created(): void { // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Trigger and capture the event for creating the record. $sink = $this->redirectEvents(); $recordid = data_add_record($data); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\record_created', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/view.php', array('d' => $data->id, 'rid' => $recordid)); $this->assertEquals($url, $event->get_url()); } /** * Test the record updated event. * * There is no external API for updating a record, so the unit test will simply create * and trigger the event and ensure the legacy log data is returned as expected. */ public function test_record_updated(): void { // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Trigger an event for updating this record. $event = \mod_data\event\record_updated::create(array( 'objectid' => 1, 'context' => \context_module::instance($data->cmid), 'courseid' => $course->id, 'other' => array( 'dataid' => $data->id ) )); // Trigger and capture the event for updating the data record. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\record_updated', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/view.php', array('d' => $data->id, 'rid' => $event->objectid)); $this->assertEquals($url, $event->get_url()); } /** * Test the record deleted event. */ public function test_record_deleted(): void { global $DB; // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Now we want to create a field. $field = data_get_field_new('text', $data); $fielddata = new \stdClass(); $fielddata->name = 'Test'; $fielddata->description = 'Test description'; $field->define_field($fielddata); $field->insert_field(); // Create data record. $datarecords = new \stdClass(); $datarecords->userid = '2'; $datarecords->dataid = $data->id; $datarecords->id = $DB->insert_record('data_records', $datarecords); // Create data content. $datacontent = new \stdClass(); $datacontent->fieldid = $field->field->id; $datacontent->recordid = $datarecords->id; $datacontent->id = $DB->insert_record('data_content', $datacontent); // Trigger and capture the event for deleting the data record. $sink = $this->redirectEvents(); data_delete_record($datarecords->id, $data, $course->id, $data->cmid); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\record_deleted', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/view.php', array('d' => $data->id)); $this->assertEquals($url, $event->get_url()); } /** * Test the template viewed event. * * There is no external API for viewing templates, so the unit test will simply create * and trigger the event and ensure the legacy log data is returned as expected. */ public function test_template_viewed(): void { // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Trigger an event for updating this record. $event = \mod_data\event\template_viewed::create(array( 'context' => \context_module::instance($data->cmid), 'courseid' => $course->id, 'other' => array( 'dataid' => $data->id ) )); // Trigger and capture the event for updating the data record. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\template_viewed', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/templates.php', array('d' => $data->id)); $this->assertEquals($url, $event->get_url()); } /** * Test the template updated event. * * There is no external API for updating a template, so the unit test will simply create * and trigger the event and ensure the legacy log data is returned as expected. */ public function test_template_updated(): void { // Create a course we are going to add a data module to. $course = $this->getDataGenerator()->create_course(); // The generator used to create a data module. $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a data module. $data = $generator->create_instance(array('course' => $course->id)); // Trigger an event for updating this record. $event = \mod_data\event\template_updated::create(array( 'context' => \context_module::instance($data->cmid), 'courseid' => $course->id, 'other' => array( 'dataid' => $data->id, ) )); // Trigger and capture the event for updating the data record. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\template_updated', $event); $this->assertEquals(\context_module::instance($data->cmid), $event->get_context()); $url = new \moodle_url('/mod/data/templates.php', array('d' => $data->id)); $this->assertEquals($url, $event->get_url()); } /** * Data provider for build providers for test_needs_mapping and test_set_affected_fields. * * @return array[] */ public static function preset_importer_provider(): array { // Image gallery preset is: ['title' => 'text', 'description' => 'textarea', 'image' => 'picture']; $titlefield = new \stdClass(); $titlefield->name = 'title'; $titlefield->type = 'text'; $descfield = new \stdClass(); $descfield->name = 'description'; $descfield->type = 'textarea'; $imagefield = new \stdClass(); $imagefield->name = 'image'; $imagefield->type = 'picture'; $difffield = new \stdClass(); $difffield->name = 'title'; $difffield->type = 'textarea'; $newfield = new \stdClass(); $newfield->name = 'number'; $newfield->type = 'number'; return [ 'Empty database / Importer with fields' => [ 'currentfields' => [], 'newfields' => [$titlefield, $descfield, $imagefield], 'expected' => ['field_created' => 3], ], 'Database with fields / Empty importer' => [ 'currentfields' => [$titlefield, $descfield, $imagefield], 'newfields' => [], 'expected' => ['field_deleted' => 3], ], 'Fields to create' => [ 'currentfields' => [$titlefield, $descfield], 'newfields' => [$titlefield, $descfield, $imagefield], 'expected' => ['field_updated' => 2, 'field_created' => 1], ], 'Fields to remove' => [ 'currentfields' => [$titlefield, $descfield, $imagefield, $difffield], 'newfields' => [$titlefield, $descfield, $imagefield], 'expected' => ['field_updated' => 2, 'field_deleted' => 1], ], 'Fields to update' => [ 'currentfields' => [$difffield, $descfield, $imagefield], 'newfields' => [$titlefield, $descfield, $imagefield], 'expected' => ['field_updated' => 1, 'field_created' => 1, 'field_deleted' => 1], ], 'Fields to create, remove and update' => [ 'currentfields' => [$titlefield, $descfield, $imagefield, $difffield], 'newfields' => [$titlefield, $descfield, $newfield], 'expected' => ['field_updated' => 2, 'field_created' => 1, 'field_deleted' => 2], ], ]; } /** * Test for needs_mapping method. * * @dataProvider preset_importer_provider * * @param array $currentfields Fields of the current activity. * @param array $newfields Fields to be imported. * @param array $expected Expected events. */ public function test_importing_events( array $currentfields, array $newfields, array $expected ): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); // Create a course and a database activity. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($currentfields as $field) { $plugingenerator->create_field($field, $activity); } $manager = manager::create_from_instance($activity); $presetactivity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Add current fields to the activity. foreach ($newfields as $field) { $plugingenerator->create_field($field, $presetactivity); } $record = (object) [ 'name' => 'Testing preset name', 'description' => 'Testing preset description', ]; $saved = $plugingenerator->create_preset($presetactivity, $record); $savedimporter = new preset_existing_importer($manager, $USER->id . '/Testing preset name'); // Trigger and capture the event for deleting the field. $sink = $this->redirectEvents(); $savedimporter->import(false); $events = $sink->get_events(); foreach ($expected as $triggeredevent => $count) { for ($i = 0; $i < $count; $i++) { $event = array_shift($events); // Check that the event data is valid. $this->assertInstanceOf('\mod_data\event\\'.$triggeredevent, $event); $this->assertEquals(\context_module::instance($activity->cmid), $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/mod/data/field.php', ['d' => $activity->id]); $this->assertEquals($url, $event->get_url()); } } } } tests/entries_exporter_test.php000064400000021347152265517740013104 0ustar00. namespace mod_data; use context_module; use mod_data\local\exporter\csv_entries_exporter; use mod_data\local\exporter\ods_entries_exporter; use mod_data\local\exporter\utils; /** * Unit tests for entries_exporter and csv_entries_exporter classes. * * Also {@see entries_export_test} class which provides module tests for exporting entries. * * @package mod_data * @covers \mod_data\local\exporter\entries_exporter * @covers \mod_data\local\exporter\csv_entries_exporter * @copyright 2023 ISB Bayern * @author Philipp Memmel * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class entries_exporter_test extends \advanced_testcase { /** * Tests get_records_count method. * * @covers \mod_data\local\exporter\entries_exporter::get_records_count * @dataProvider get_records_count_provider * @param array $rows the rows from the data provider to be tested by the exporter * @param int $expectedcount the expected count of records to be exported */ public function test_get_records_count(array $rows, int $expectedcount): void { $exporter = new csv_entries_exporter(); foreach ($rows as $row) { $exporter->add_row($row); } $this->assertEquals($expectedcount, $exporter->get_records_count()); } /** * Data provider method for self::test_get_records_count. * * @return array data for testing */ public static function get_records_count_provider(): array { return [ 'onlyheader' => [ 'rows' => [ ['numberfield', 'textfield', 'filefield1', 'filefield2', 'picturefield'] ], 'expectedcount' => 0 // Only header present, so we expect record count 0. ], 'onerecord' => [ 'rows' => [ ['numberfield', 'textfield', 'filefield1', 'filefield2', 'picturefield'], ['3', 'a simple text', 'samplefile.png', 'samplefile_1.png', 'picturefile.png'] ], 'expectedcount' => 1 ], 'tworecords' => [ 'rows' => [ ['numberfield', 'textfield', 'filefield1', 'filefield2', 'picturefield'], ['3', 'a simple text', 'samplefile.png', 'samplefile_1.png', 'picturefile.png'], ['5', 'a supersimple text', 'anotherfile.png', 'someotherfile.png', 'andapicture.png'] ], 'expectedcount' => 2 ] ]; } /** * Tests adding of files to the exporter to be included in the exported zip archive. * * @dataProvider add_file_from_string_provider * @covers \mod_data\local\exporter\entries_exporter::add_file_from_string * @covers \mod_data\local\exporter\entries_exporter::file_exists * @param array $files array of filename and filecontent to be tested for exporting * @param bool $success if the exporting of files should be successful */ public function test_add_file_from_string(array $files, bool $success): void { $exporter = new csv_entries_exporter(); foreach ($files as $file) { if (empty($file['subdir'])) { $exporter->add_file_from_string($file['filename'], $file['filecontent']); $this->assertEquals($exporter->file_exists($file['filename']), $success); } else { $exporter->add_file_from_string($file['filename'], $file['filecontent'], $file['subdir']); $this->assertEquals($exporter->file_exists($file['filename'], $file['subdir']), $success); } } } /** * Data provider method for self::test_add_file_from_string. * * @return array data for testing */ public static function add_file_from_string_provider(): array { return [ 'one file' => [ 'files' => [ [ 'filename' => 'testfile.txt', 'filecontent' => 'somecontent' ], ], 'success' => true ], 'more files, also with subdirs' => [ 'files' => [ [ 'filename' => 'testfile.txt', 'filecontent' => 'somecontent' ], [ 'filename' => 'testfile2.txt', 'filecontent' => 'someothercontent', 'subdir' => 'testsubdir' ], [ 'filename' => 'testfile3.txt', 'filecontent' => 'someverydifferentcontent', 'subdir' => 'files/foo/bar' ], [ 'filename' => 'testfile4.txt', 'filecontent' => 'someverydifferentcontent', 'subdir' => 'files/foo/bar/' ], [ 'filename' => 'testfile5.txt', 'filecontent' => 'someverydifferentcontent', 'subdir' => '/files/foo/bar/' ], ], 'success' => true ], 'nocontent' => [ 'files' => [ [ 'filename' => '', 'filecontent' => '' ] ], 'success' => false ] ]; } /** * Tests if unique filenames are being created correctly. * * @covers \mod_data\local\exporter\entries_exporter::create_unique_filename * @dataProvider create_unique_filename_provider * @param string $inputfilename the name of the file which should be converted into a unique filename * @param string $resultfilename the maybe changed $inputfilename, so that it is unique in the exporter */ public function test_create_unique_filename(string $inputfilename, string $resultfilename): void { $exporter = new csv_entries_exporter(); $exporter->add_file_from_string('test.txt', 'somecontent'); $exporter->add_file_from_string('foo.txt', 'somecontent'); $exporter->add_file_from_string('foo_1.txt', 'somecontent'); $exporter->add_file_from_string('foo_2.txt', 'somecontent'); $exporter->add_file_from_string('foo', 'somecontent'); $exporter->add_file_from_string('foo_1', 'somecontent'); $exporter->add_file_from_string('sample_5.txt', 'somecontent'); $exporter->add_file_from_string('bar_1.txt', 'somecontent'); $this->assertEquals($resultfilename, $exporter->create_unique_filename($inputfilename)); } /** * Data provider method for self::test_create_unique_filename. * * @return array data for testing */ public static function create_unique_filename_provider(): array { return [ 'does not exist yet' => [ 'inputfilename' => 'someuniquename.txt', 'resultfilename' => 'someuniquename.txt' ], 'already exists' => [ 'inputfilename' => 'test.txt', 'resultfilename' => 'test_1.txt' ], 'already exists, other numbers as well' => [ 'inputfilename' => 'foo.txt', 'resultfilename' => 'foo_3.txt' ], 'file with _5 suffix already exists' => [ 'inputfilename' => 'sample_5.txt', 'resultfilename' => 'sample_5_1.txt' ], 'file with _1 suffix already exists' => [ 'inputfilename' => 'bar_1.txt', 'resultfilename' => 'bar_1_1.txt' ], 'file without extension unique' => [ 'inputfilename' => 'test', 'resultfilename' => 'test' ], 'file without extension not unique' => [ 'inputfilename' => 'foo', 'resultfilename' => 'foo_2' ] ]; } } tests/generator_test.php000064400000034132152265517740011465 0ustar00. namespace mod_data; use stdClass; /** * PHPUnit data generator testcase. * * @package mod_data * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data_generator */ final class generator_test extends \advanced_testcase { /** * @covers ::create_instance */ public function test_generator(): void { global $DB; $this->resetAfterTest(true); $this->assertEquals(0, $DB->count_records('data')); $course = $this->getDataGenerator()->create_course(); /** @var mod_data_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $this->assertInstanceOf('mod_data_generator', $generator); $this->assertEquals('data', $generator->get_modulename()); $generator->create_instance(['course' => $course->id]); $generator->create_instance(['course' => $course->id]); $data = $generator->create_instance(['course' => $course->id]); $this->assertEquals(3, $DB->count_records('data')); $cm = get_coursemodule_from_instance('data', $data->id); $this->assertEquals($data->id, $cm->instance); $this->assertEquals('data', $cm->modname); $this->assertEquals($course->id, $cm->course); $context = \context_module::instance($cm->id); $this->assertEquals($data->cmid, $context->instanceid); // Test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE! $data = $generator->create_instance(['course' => $course->id, 'assessed' => 1, 'scale' => 100]); $gitem = $DB->get_record('grade_items', [ 'courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'data', 'iteminstance' => $data->id, ]); $this->assertNotEmpty($gitem); $this->assertEquals(100, $gitem->grademax); $this->assertEquals(0, $gitem->grademin); $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype); } /** * @covers ::create_field */ public function test_create_field(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $this->assertEquals(0, $DB->count_records('data')); $course = $this->getDataGenerator()->create_course(); /** @var mod_data_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $this->assertInstanceOf('mod_data_generator', $generator); $this->assertEquals('data', $generator->get_modulename()); $data = $generator->create_instance(['course' => $course->id]); $this->assertEquals(1, $DB->count_records('data')); $cm = get_coursemodule_from_instance('data', $data->id); $this->assertEquals($data->id, $cm->instance); $this->assertEquals('data', $cm->modname); $this->assertEquals($course->id, $cm->course); $context = \context_module::instance($cm->id); $this->assertEquals($data->cmid, $context->instanceid); $fieldtypes = ['checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url']; $count = 1; // Creating test Fields with default parameter values. foreach ($fieldtypes as $fieldtype) { // Creating variables dynamically. $fieldname = 'field-' . $count; $record = new \stdClass(); $record->name = $fieldname; $record->type = $fieldtype; ${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data); $this->assertInstanceOf('data_field_' . $fieldtype, ${$fieldname}); $count++; } $this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', ['dataid' => $data->id])); } /** * @covers ::create_entry */ public function test_create_entry(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $this->assertEquals(0, $DB->count_records('data')); $user1 = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'groupA']); $this->getDataGenerator()->create_group_member(['userid' => $user1->id, 'groupid' => $groupa->id]); /** @var mod_data_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $this->assertInstanceOf('mod_data_generator', $generator); $this->assertEquals('data', $generator->get_modulename()); $data = $generator->create_instance(['course' => $course->id]); $this->assertEquals(1, $DB->count_records('data')); $cm = get_coursemodule_from_instance('data', $data->id); $this->assertEquals($data->id, $cm->instance); $this->assertEquals('data', $cm->modname); $this->assertEquals($course->id, $cm->course); $context = \context_module::instance($cm->id); $this->assertEquals($data->cmid, $context->instanceid); $fieldtypes = ['checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url', 'latlong', 'file', 'picture', ]; $count = 1; // Creating test Fields with default parameter values. foreach ($fieldtypes as $fieldtype) { // Creating variables dynamically. $fieldname = 'field-' . $count; $record = new \stdClass(); $record->name = $fieldname; $record->type = $fieldtype; $record->required = 1; $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data); $count++; } $fields = $DB->get_records('data_fields', ['dataid' => $data->id], 'id'); $contents = []; $contents[] = ['opt1', 'opt2', 'opt3', 'opt4']; $contents[] = '01-01-2037'; // It should be lower than 2038, to avoid failing on 32-bit windows. $contents[] = 'menu1'; $contents[] = ['multimenu1', 'multimenu2', 'multimenu3', 'multimenu4']; $contents[] = '12345'; $contents[] = 'radioopt1'; $contents[] = 'text for testing'; $contents[] = '

text area testing

'; $contents[] = ['example.url', 'sampleurl']; $contents[] = [-31.9489873, 115.8382036]; // Latlong. $contents[] = 'Filename.pdf'; // File - filename. $contents[] = ['Cat1234.jpg', 'Cat']; // Picture - filename with alt text. $count = 0; $fieldcontents = []; foreach ($fields as $fieldrecord) { $fieldcontents[$fieldrecord->id] = $contents[$count++]; } $tags = ['Cats', 'mice']; $this->setUser($user1); $datarecordid = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry( $data, $fieldcontents, $groupa->id, $tags ); $this->assertEquals(1, $DB->count_records('data_records', ['dataid' => $data->id])); $this->assertEquals(count($contents), $DB->count_records('data_content', ['recordid' => $datarecordid])); $entry = $DB->get_record('data_records', ['id' => $datarecordid]); $this->assertEquals($entry->groupid, $groupa->id); $contents = $DB->get_records('data_content', ['recordid' => $datarecordid], 'id'); $contentstartid = 0; $flag = 0; foreach ($contents as $key => $content) { if (!$flag++) { $contentstartid = $key; } $this->assertFalse($content->content == null); } $this->assertEquals($contents[$contentstartid]->content, 'opt1##opt2##opt3##opt4'); $this->assertEquals($contents[++$contentstartid]->content, '2114380800'); $this->assertEquals($contents[++$contentstartid]->content, 'menu1'); $this->assertEquals($contents[++$contentstartid]->content, 'multimenu1##multimenu2##multimenu3##multimenu4'); $this->assertEquals($contents[++$contentstartid]->content, '12345'); $this->assertEquals($contents[++$contentstartid]->content, 'radioopt1'); $this->assertEquals($contents[++$contentstartid]->content, 'text for testing'); $this->assertEquals($contents[++$contentstartid]->content, '

text area testing

'); $this->assertEquals($contents[$contentstartid]->content1, '1'); $this->assertEquals($contents[++$contentstartid]->content, 'http://example.url'); $this->assertEquals($contents[$contentstartid]->content1, 'sampleurl'); $this->assertEquals( ['Cats', 'mice'], array_values(\core_tag_tag::get_item_tags_array('mod_data', 'data_records', $datarecordid)) ); } /** * Test for create_preset(). * * @dataProvider create_preset_provider * @covers ::create_preset * @param stdClass|null $record data for the preset that will be created (like name or description) */ public function test_create_preset(?stdClass $record): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); if (!is_null($record) && property_exists($record, 'user')) { $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher', (object) ['username' => $record->user]); $record->userid = $user->id; unset($record->user); } // Check initially there are no saved presets. $manager = manager::create_from_coursemodule($cm); $savedpresets = $manager->get_available_saved_presets(); $this->assertEmpty($savedpresets); // Create one preset with the configuration in $record. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $preset = $plugingenerator->create_preset($activity, $record); // Check the preset has been saved. $savedpresets = $manager->get_available_saved_presets(); $this->assertCount(1, $savedpresets); // Check the preset name has the expected value. if (is_null($record) || !property_exists($record, 'name')) { $this->assertStringStartsWith('New preset', $preset->name); } else { $this->assertEquals($record->name, $preset->name); } // Check the preset description has the expected value. if (is_null($record) || !property_exists($record, 'description')) { $this->assertEmpty($preset->description); } else { $this->assertEquals($record->description, $preset->description); } // Check the preset author has the expected value. if (is_null($record) || !property_exists($record, 'userid')) { $this->assertEquals($USER->id, $preset->get_userid()); } else { $this->assertEquals($record->userid, $preset->get_userid()); } // Check the file has been updated properly. $this->assertNotNull($preset->storedfile); } /** * Data provider for test_create_preset(). * * @return array */ public static function create_preset_provider(): array { return [ 'Create using the default configuration' => [ 'record' => null, ], 'Create with a given name but no description' => [ 'record' => (object) [ 'name' => 'World recipes preset', ], ], 'Create with a given description but no name' => [ 'record' => (object) [ 'description' => 'This is a preset to collect the most popular world recipes.', ], ], 'Create with a given name and description' => [ 'record' => (object) [ 'name' => 'World recipes preset', 'description' => 'This is a preset to collect the most popular world recipes.', ], ], 'Create with a given user but no description or name' => [ 'record' => (object) [ 'user' => 'teacher1', ], ], 'Create with a given name and user but no description' => [ 'record' => (object) [ 'name' => 'World recipes preset', 'user' => 'teacher1', ], ], 'Create with a given description and user but no name' => [ 'record' => (object) [ 'description' => 'This is a preset to collect the most popular world recipes.', 'user' => 'teacher1', ], ], 'Create with a given name, description and user' => [ 'record' => (object) [ 'name' => 'World recipes preset', 'description' => 'This is a preset to collect the most popular world recipes.', 'user' => 'teacher1', ], ], ]; } } tests/behat/data_no_calendar_capabilities.feature000064400000004731152265517740016340 0ustar00@mod @mod_data Feature: Database with no calendar capabilites In order to allow work effectively As a teacher I need to be able to create databases even when I cannot edit calendar events Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "admin" And I am on the "Course 1" "permissions" page And I override the system permissions of "Teacher" role with: | capability | permission | | moodle/calendar:manageentries | Prohibit | And I log out Scenario: Editing a database Given I log in as "admin" And the following "activities" exist: | activity | name | intro | course | section | idnumber | | data | Test database name | Test database description | C1 | 1 | data1 | And I am on "Course 1" course homepage And I follow "Test database name" And I navigate to "Settings" in current page administration And I set the following fields to these values: | id_timeavailablefrom_enabled | 1 | | id_timeavailablefrom_day | 1 | | id_timeavailablefrom_month | 1 | | id_timeavailablefrom_year | 2017 | | id_timeavailableto_enabled | 1 | | id_timeavailableto_day | 1 | | id_timeavailableto_month | 4 | | id_timeavailableto_year | 2017 | | id_timeviewfrom_enabled | 1 | | id_timeviewfrom_day | 1 | | id_timeviewfrom_month | 3 | | id_timeviewfrom_year | 2017 | | id_timeviewto_enabled | 1 | | id_timeviewto_day | 1 | | id_timeviewto_month | 4 | | id_timeviewto_year | 2017 | And I press "Save and return to course" And I log out When I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I follow "Test database name" And I navigate to "Settings" in current page administration And I set the following fields to these values: | id_timeavailablefrom_year | 2018 | | id_timeavailableto_year | 2018 | | id_timeviewfrom_year | 2018 | | id_timeviewto_year | 2018 | And I press "Save and return to course" Then I should see "Test database name" tests/behat/data_activity_rating.feature000064400000004505152265517740014561 0ustar00@mod @mod_data Feature: Enable activity rating according to chosen grading scale In order to have ratings appear in the course gradebook As a teacher I need to enable activity rating according to chosen grading scale Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | Student | One | student1@example.com | | student2 | Student | Two | student2@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | course | name | idnumber | | data | C1 | DB activity 1 | db1 | And the following "mod_data > fields" exist: | database | type | name | | db1 | text | DB field | @javascript Scenario: View ratings in the course gradebook Given I am on the "DB activity 1" "data activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Aggregate type | Count of ratings | | scale[modgrade_type] | Point | | scale[modgrade_point] | 10 | And I press "Save and display" And the following "mod_data > entries" exist: | database | user | DB field | | db1 | student1 | S1 entry 1 | | db1 | student1 | S1 entry 2 | | db1 | student2 | S2 entry 1 | And I am on the "DB activity 1" "data activity" page And I select "Single view" from the "jump" singleselect And I set the field "rating" to "5" And I follow "Next page" And I set the field "rating" to "7" And I follow "Next page" And I set the field "rating" to "10" When I am on the "Course 1" "grades > Grader report > View" page Then the following should exist in the "user-grades" table: | -1- | -2- | -3- | | Student One | student1@example.com | 2.00 | | Student Two | student2@example.com | 1.00 | tests/behat/data_activity_read_only_dates.feature000064400000003151152265517740016425 0ustar00@mod @mod_data Feature: Control database activity entry based on read-only dates In order to restrict or allow student entries based on specific dates As a teacher I need to be able to set read-only dates for the database activity Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | Scenario Outline: Student can add entries only when the current date falls outside the read-only date range Given the following "activities" exist: | activity | course | name | idnumber | timeviewfrom | timeviewto | | data | C1 | Data Activity 1 | DB1 | | | And the following "mod_data > fields" exist: | database | type | name | | DB1 | text | DB Field 1 | When I am on the "Data Activity 1" "data activity" page logged in as student1 # The "Add entry" button is visible only when the current date falls outside the read-only date range. Then "Add entry" "button" exist Examples: | viewfrom | viewto | btnvisibility | | ##yesterday## | ##tomorrow## | should not | | ##tomorrow## | ##tomorrow +1day## | should | | ##1 week ago## | ##yesterday## | should | tests/behat/data_activity_timeline.feature000064400000005106152265517740015101 0ustar00@mod @mod_data Feature: Students can view upcoming data activities in the timeline block In order for student to see upcoming data activities in timeline block As a teacher I should be able to set the availability dates of data activities Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | One | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | @javascript Scenario Outline: Student can view upcoming data activities in the timeline block Given the following "activities" exist: | activity | course | name | id_timeavailablefrom_enabled | timeavailablefrom | id_timeavailableto_enabled | timeavailableto | | data | C1 | DB Past | 1 | | 1 | | | data | C1 | DB Future | 1 | | 1 | | | data | C1 | DB No date | 0 | | 0 | | # Confirm that student can see future but not past db activity in the timeline block When I log in as "student 1" Then I should not see "DB Past" in the "Timeline" "block" # Also confirm that student can't see db activity where availability is disabled And I should not see "DB No Date" in the "Timeline" "block" And I should see "DB Future" in the "Timeline" "block" # Confirm link works and redirects to db activity And I click on "DB Future" "link" in the "Timeline" "block" And the activity date in "DB Future" should contain "Opens:" And the activity date in "DB Future" should contain "%A, %d %B %Y, %I:%M##" And the activity date in "DB Future" should contain "Closes:" And the activity date in "DB Future" should contain "%A, %d %B %Y, %I:%M##" Examples: | pastfrom | pastto | futurefrom | futureto | | ##1 month ago## | ##yesterday## | ##tomorrow## | ##tomorrow +1day## | | ##yesterday## | ##yesterday +3hours## | ##tomorrow noon## | ##tomorrow noon +3hours## | | ##6 months ago## | ##1 week ago## | ##tomorrow +5days## | ##tomorrow +6days## | tests/behat/entry_approval.feature000064400000004535152265517740013440 0ustar00@mod @mod_data Feature: User can see the entry approval status on the single view and list view in the default template. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | approval | | data | Test database name | Database intro | C1 | data1 | 1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | | data1 | text | field2 | Test field 2 description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | user | field1 | field2 | | data1 | student1 | Student entry 1 | Some student content 1 | | data1 | teacher1 | Teacher entry 1 | Some teacher content 1 | @javascript Scenario Outline: The approval status is displayed in the single view and list view next to the action menu # List view. Given I am on the "Test database name" "data activity" page logged in as Then I should see "Pending approval" in the "region-main" "region" # Single view. And I select "Single view" from the "jump" singleselect And I should see "Pending approval" in the "region-main" "region" And I click on "2" "link" in the ".pagination" "css_element" And I should not see "Pending approval" in the "region-main" "region" And I should not see "Approved" in the "region-main" "region" Examples: | user | | student1 | | teacher1 | tests/behat/data_activities.feature000064400000004573152265517740013532 0ustar00@mod @mod_data Feature: Users can view the list of data activities and their formatted descriptions Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Bob | 1 | student1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database 1 | This is an intro without an image | C1 | data1 | | data | Test database 2 | This is an intro with an image: | C1 | data2 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | activity_modules | Course | C1 | course-view-* | side-pre | Scenario: Teachers can view the list of data activities and their formatted descriptions Given I am on the "Course 1" course page logged in as teacher1 When I follow "Databases" Then I should see "Test database 1" And I should see "Test database 2" And I should see "This is an intro without an image" And I should see "This is an intro with an image: " And "//img[contains(@src, 'some_image.jpg')]" "xpath_element" should exist And "//img[contains(@src, '@@PLUGINFILE@@/some_image.jpg')]" "xpath_element" should not exist Scenario: Students can view the list of data activities and their formatted descriptions Given I am on the "Course 1" course page logged in as student1 When I follow "Databases" Then I should see "Test database 1" And I should see "Test database 2" And I should see "This is an intro without an image" And I should see "This is an intro with an image: " And "//img[contains(@src, 'some_image.jpg')]" "xpath_element" should exist And "//img[contains(@src, '@@PLUGINFILE@@/some_image.jpg')]" "xpath_element" should not exist tests/behat/data_activity_completion_pass_grade.feature000064400000020713152265517740017635 0ustar00@mod @mod_data @core_completion Feature: Completion pass grade View activity completion in the database activity In order to have visibility of database completion requirements As a student I need to be able to view my database completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | student2 | Vinnie | Student2 | student2@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | data | | course | C1 | | idnumber | mh1 | | name | Music history | | section | 1 | And the following "mod_data > fields" exist: | database | type | name | | mh1 | text | Instrument types | And I am on the "Music history" "data activity" page logged in as teacher1 And I navigate to "Settings" in current page administration And I expand all fieldsets And I set the following fields to these values: | Aggregate type | Average of ratings | | scale[modgrade_type] | Point | | scale[modgrade_point] | 100 | | gradepass | 50 | | Add requirements | 1 | | View the activity | 1 | | Receive a grade | 1 | | Passing grade | 1 | | completionentriesenabled | 1 | | completionentries | 2 | And I press "Save and display" And I log out @javascript Scenario: Database module completion conditions are displayed regardless of the view # We add an entry to let the user change to a different view. Given the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | teacher1 | Drums | When I am on the "Music history" "data activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Make entries: 2" completion condition And "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "Receive a passing grade" completion condition And I select "Single view" from the "jump" singleselect And "Music history" should have the "View" completion condition And "Music history" should have the "Make entries: 2" completion condition And "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "Receive a passing grade" completion condition @javascript Scenario: Student cannot complete a database activity if one of the conditions are not met Given I am on the "Music history" "data activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | student1 | Drums | And I am on "Course 1" course homepage # One entry is not enough to mark as complete. And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | student1 | Hurdygurdy | And I am on "Course 1" course homepage Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I log out And I am on the "Music history" "data activity" page logged in as teacher1 And I select "Single view" from the "jump" singleselect And I set the field "rating" to "3" And I log out When I am on the "Music history" "data activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "failed" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And "Vinnie Student1" user has completed "Music history" activity @javascript Scenario: Student can complete a database activity when all conditions are met Given I am on the "Music history" "data activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | student1 | Drums | And I am on "Course 1" course homepage # One entry is not enough to mark as complete. And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | student1 | Hurdygurdy | And I am on "Course 1" course homepage And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the "Receive a passing grade" completion condition of "Music history" is displayed as "todo" And I log out And I am on the "Music history" "data activity" page logged in as teacher1 And I select "Single view" from the "jump" singleselect And I set the field "rating" to "60" And I log out When I am on the "Music history" "data activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "done" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And "Vinnie Student1" user has completed "Music history" activity tests/behat/manageapproved.feature000064400000007721152265517740013364 0ustar00@mod @mod_data Feature: Users can edit approved entries in database activities In order to control whether approved database entries can be changed As a teacher I need to be able to enable or disable management of approved entries Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Students can manage their approved entries to a database Given the following "activity" exists: | activity | data | | course | C1 | | idnumber | Test database name | | name | Test database name | | approval | 1 | | manageapproved | 1 | And the following "mod_data > fields" exist: | database | type | name | description | | Test database name | text | Test field name | Test field description | And the following "mod_data > templates" exist: | database | name | | Test database name | singletemplate | | Test database name | listtemplate | | Test database name | addtemplate | | Test database name | asearchtemplate | | Test database name | rsstemplate | And the following "mod_data > entries" exist: | database | user | Test field name | | Test database name | student1 | Student entry | # Approve the student's entry as a teacher. And I am on the "Test database name" "data activity" page logged in as teacher1 And I open the action menu in ".defaulttemplate-listentry" "css_element" And I choose "Approve" in the open action menu And I log out # Make sure the student can still edit their entry after it's approved. When I am on the "Test database name" "data activity" page logged in as student1 Then I should see "Student entry" And "Edit" "link" should exist @javascript Scenario: Students can not manage their approved entries to a database # Create database activity and don't allow editing of approved entries. Given the following "activity" exists: | activity | data | | course | C1 | | idnumber | Test database name | | name | Test database name | | approval | 1 | | manageapproved | 0 | And the following "mod_data > fields" exist: | database | type | name | description | | Test database name | text | Test field name | Test field description | And the following "mod_data > templates" exist: | database | name | | Test database name | singletemplate | | Test database name | listtemplate | | Test database name | addtemplate | | Test database name | asearchtemplate | | Test database name | rsstemplate | And the following "mod_data > entries" exist: | database | user | Test field name | | Test database name | student1 | Student entry | # Approve the student's entry as a teacher. And I am on the "Test database name" "data activity" page logged in as teacher1 And I open the action menu in ".defaulttemplate-listentry" "css_element" And I choose "Approve" in the open action menu And I log out # Make sure the student isn't able to edit their entry after it's approved. When I am on the "Test database name" "data activity" page logged in as student1 Then "Edit" "link" should not exist And I should see "Student entry" tests/behat/group_mode.feature000064400000014222152265517740012525 0ustar00@mod @mod_data Feature: Group data activity In order to create a database with my group As a student I need to add and view entries for the groups I am a member of Background: And the following "courses" exist: | fullname | shortname | | Test Course 1 | C1 | And the following "groups" exist: | name | course | idnumber | participation | | Group 1 | C1 | G1 | 1 | | Group 2 | C1 | G2 | 1 | | Group 3 | C1 | G3 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | TeacherG1 | 1 | teacher1@example.com | | teacher2 | TeacherGNone | 2 | teacher2@example.com | | teacher3 | TeacherGNone | 3 | teacher3@example.com | | user1 | User1G1 | 1 | user1@example.com | | user2 | User2G2 | 2 | user2@example.com | | user3 | User3None | 3 | user3@example.com | | user4 | User4NPgroup | 4 | user4@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | | teacher3 | C1 | teacher | | user1 | C1 | student | | user2 | C1 | student | | user3 | C1 | student | | user4 | C1 | student | And the following "group members" exist: | user | group | | teacher1 | G1 | | user1 | G1 | | user2 | G2 | | user4 | G3 | And the following "activities" exist: | activity | name | intro | course | idnumber | groupmode | | data | Separate data | Data with separate groups | C1 | data1 | 1 | | data | Visible data | Data with visible groups | C1 | data2 | 2 | And the following "mod_data > fields" exist: | database | type | name | | data1 | shorttext | separatetext | | data2 | shorttext | visibletext | And the following "mod_data > entries" exist: | database | user | group | separatetext | | data1 | user1 | G1 | I am user 1 | | data1 | user2 | G2 | I am user 2 | And the following "mod_data > entries" exist: | database | user | separatetext | | data1 | user3 | I am user 3 | And the following "mod_data > entries" exist: | database | user | group | visibletext | | data2 | user1 | G1 | I am user 1 | | data2 | user2 | G2 | I am user 2 | And the following "mod_data > entries" exist: | database | user | visibletext | | data2 | user3 | I am user 3 | | data2 | user4 | I am user 4 | Scenario Outline: Users should see their own participation groups in "separate groups" mode, and all participation groups in "visible groups" mode. Given I am on the "" "data activity" page logged in as "" Then I "All participants" And I "Group 1" And I "I am user 1" And I "Group 2" And I "I am user 2" # All users should see entries with no group. And I should see "I am user 3" # No-one should see non-participation groups. And I should not see "Group 3" Examples: | data | user | all | G1 | G2 | user1 | user2 | | data1 | teacher1 | should see | should see | should see | should see | should see | | data1 | teacher2 | should see | should see | should see | should see | should see | | data1 | teacher3 | should see | should not see | should not see | should not see | should not see | | data1 | user1 | should not see | should see | should not see | should see | should not see | | data1 | user2 | should not see | should not see | should see | should not see | should see | | data1 | user3 | should see | should not see | should not see | should not see | should not see | | data1 | user4 | should see | should not see | should not see | should not see | should not see | | data2 | teacher1 | should see | should see | should see | should see | should see | | data2 | teacher2 | should see | should see | should see | should see | should see | | data2 | teacher3 | should see | should see | should see | should see | should not see | | data2 | user1 | should see | should see | should see | should see | should not see | | data2 | user2 | should see | should see | should see | should not see | should see | | data2 | user3 | should see | should see | should see | should see | should not see | | data2 | user4 | should see | should see | should see | should see | should not see | Scenario Outline: When viewing a database in visible groups mode, a user should only have the "Add entry" button for their own participation groups Given I am on the "data2" "data activity" page logged in as "" And I select "" from the "group" singleselect And I should see "Add entry" When I select "" from the "group" singleselect Then I should not see "Add entry" Examples: | user | mygroup | othergroup | | user1 | Group 1 | Group 2 | | user2 | Group 2 | Group 1 | | user1 | All participants | Group 2 | | user2 | All participants | Group 1 | Scenario Outline: Users in no groups or non-participation groups should not be able to add entries Given I am on the "" "data activity" page logged in as "" Then I should not see "Add entry" Examples: | data | user | | data1 | user3 | | data1 | user4 | | data2 | user3 | | data2 | user4 | tests/behat/use_presets.feature000064400000027707152265517740012742 0ustar00@mod @mod_data @javascript Feature: Users can use predefined presets In order to use presets As a user I need to select an existing preset Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Mountain landscapes | introduction... | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | Scenario: Using a preset on a non empty database could create new fields Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | title | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "title" And I should not see "Description" And I should not see "image" And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" And I should see "Apply preset Image gallery" And I click on "Map fields" "button" And I should see "Field mappings" When I click on "Continue" "button" And I should see "Preset applied" Then I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" Scenario: Using a preset on a non-empty database could show the option to map fields Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | oldtitle | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "oldtitle" And I should not see "Description" And I should not see "image" And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" # Let's map a field that is not mapped by default And I should see "Apply preset Image gallery" And I should see "Fields to be created: image, title, description" And I should see "Existing fields to be deleted: Test field name, oldtitle" When I click on "Map fields" "button" And I should see "Create a new field" in the "oldtitle" "table_row" And I set the field "id_title" to "Map to oldtitle" And I click on "Continue" "button" And I should see "Preset applied" Then I should not see "oldtitle" And I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" Scenario: Teacher can use a preset from presets page on a database with existing entries # Creating an entry to test use a preset feature with databases with entries. Given the following "mod_data > entries" exist: | database | Test field name | | data1 | Student entry 1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And the "Use this preset" "button" should be enabled When I click on "Use this preset" "button" And I should see "Apply preset Image gallery" And I click on "Map fields" "button" Then I should see "Field mappings" And I should see "title" And I should see "Create a new field" in the "title" "table_row" # We map existing field to keep the entry data And I set the field "id_title" to "Map to Test field name" And I click on "Continue" "button" And I should see "Preset applied" And I follow "Fields" And I should see "title" And I follow "Database" And I should see "Student entry 1" Scenario: Using same preset twice doesn't show mapping dialogue and applies the preset directly Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" When I click on "Use this preset" "button" And I should see "Apply preset" And I click on "Map fields" "button" And I set the field "id_title" to "Map to Test field name" And I click on "Continue" "button" And I should see "Preset applied" And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" Then I should not see "Apply preset Image gallery" And I should see "Preset applied" Scenario: Using a preset from preset preview page on a non empty database could create new fields Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | title | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "title" And I should not see "Description" And I should not see "image" And I follow "Presets" And I click on "Image gallery" "link" And I click on "Use this preset" "button" And I should see "Apply preset Image gallery" When I click on "Apply preset" "button" And I should see "Preset applied" And I follow "Fields" Then I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" Scenario: Using a preset from preset preview page on a non-empty database could show the option to map fields Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | oldtitle | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "oldtitle" And I should not see "Description" And I should not see "image" And I follow "Presets" And I click on "Image gallery" "link" And I click on "Use this preset" "button" # Let's map a field that is not mapped by default And I should see "Apply preset Image gallery" And I should see "Fields to be created: image, title, description" And I should see "Existing fields to be deleted: Test field name, oldtitle" When I click on "Map fields" "button" And I should see "Create a new field" in the "oldtitle" "table_row" And I set the field "id_title" to "Map to oldtitle" And I click on "Continue" "button" And I should see "Preset applied" Then I should not see "oldtitle" And I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" Scenario: Teacher can use a preset from preset preview page on a database with existing entries # Creating an entry to test use a preset feature with databases with entries. Given the following "mod_data > entries" exist: | database | Test field name | | data1 | Student entry 1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "Image gallery" "link" And the "Use this preset" "button" should be enabled When I click on "Use this preset" "button" And I should see "Apply preset Image gallery" And I click on "Map fields" "button" Then I should see "Field mappings" And I should see "title" And I should see "Create a new field" in the "title" "table_row" # We map existing field to keep the entry data And I set the field "id_title" to "Map to Test field name" And I click on "Continue" "button" And I should see "Preset applied" And I follow "Fields" And I should see "title" And I follow "Database" And I should see "Student entry 1" Scenario: Using same preset twice from preset preview page doesn't show mapping dialogue and applies the preset directly Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "Image gallery" "link" When I click on "Use this preset" "button" And I should see "Apply preset Image gallery" And I click on "Map fields" "button" And I should see "Field mappings" And I set the field "id_title" to "Map to Test field name" And I click on "Continue" "button" And I should see "Preset applied" And I follow "Presets" And I click on "Image gallery" "link" And I click on "Use this preset" "button" Then I should not see "Field mappings" And I should see "Preset applied" Scenario: Apply preset dialogue should show helpful information to the user Given the following "activities" exist: | activity | name | intro | course | idnumber | | data | Sea landscapes | introduction... | C1 | data2 | And the following "mod_data > fields" exist: | database | type | name | | data2 | text | title | And I am on the "Sea landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "Image gallery" "link" When I click on "Use this preset" "button" And I should see "Apply preset Image gallery" # Fields to be created only. Then I should see "Fields to be created: image, description" And I should not see "If fields to be deleted are of the same type as fields to be created" And I should not see "If fields to be deleted are of the same type as new fields in the preset" And I click on "Cancel" "button" in the "Apply preset Image gallery?" "dialogue" And I follow "Presets" And the following "mod_data > fields" exist: | database | type | name | | data2 | number | number | And I click on "Image gallery" "link" And I click on "Use this preset" "button" And I should see "Apply preset Image gallery" # Fields to be created and fields to be deleted. And I should see "Fields to be created: image, description" And I should see "Existing fields to be deleted: number" And I should see "If fields to be deleted are of the same type as fields to be created" And I should not see "If fields to be deleted are of the same type as new fields in the preset" And I click on "Cancel" "button" in the "Apply preset Image gallery?" "dialogue" And I follow "Presets" And the following "mod_data > fields" exist: | database | type | name | | data2 | textarea | description | | data2 | picture | image | And I click on "Image gallery" "link" And I click on "Use this preset" "button" And I should see "Apply preset Image gallery" # Fields to be deleted only. And I should see "Existing fields to be deleted: number" And I should not see "If fields to be deleted are of the same type as fields to be created" And I should see "If fields to be deleted are of the same type as new fields in the preset" Scenario: Teacher can use a preset on a non-empty database and previous fields will be removed Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "Test field name" And I follow "Presets" And I click on "fullname" "radio" in the "Image gallery" "table_row" And I click on "Use this preset" "button" And I should see "Existing fields to be deleted: Test field name" When I click on "Apply preset" "button" Then I should see "Preset applied." And I should see "image" And I should see "title" And I should not see "Test field name" tests/behat/default_templates.feature000064400000016252152265517740014074 0ustar00@mod @mod_data Feature: Users can use mod_data without editing the templates In order to use the database module As a teacher I need to manage fields and entries using always the default templates. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | | data1 | text | field2 | Test field 2 description | And the following "mod_data > entries" exist: | database | field1 | field2 | | data1 | Student entry 1 | Some content 1 | | data1 | Student entry 2 | Some content 2 | And I am on the "Test database name" "data activity" page logged in as teacher1 @javascript Scenario: The default view templates should be updated when a field is added. Given I navigate to "Database" in current page administration And I should see "field1" And I should see "field2" And I should see "Student entry 1" And I should see "Some content 1" And I should see "Student entry 2" And I should see "Some content 2" And I set the field "View mode tertiary navigation" to "Single view" And I should see "field1" And I should see "field2" And I should see "Student entry 1" And I should see "Some content 1" And I should not see "Student entry 2" And I should not see "Some content 2" When the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field3 | Test field 3 description | Then I navigate to "Database" in current page administration And I should see "field1" And I should see "field2" And I should see "field3" And I should see "Student entry 1" And I should see "Some content 1" And I should see "Student entry 2" And I should see "Some content 2" And I set the field "View mode tertiary navigation" to "Single view" And I should see "field1" And I should see "field2" And I should see "field3" And I should see "Student entry 1" And I should see "Some content 1" And I should not see "Student entry 2" And I should not see "Some content 2" Scenario: The default add templates should be updated when a field is added. Given I navigate to "Database" in current page administration And I click on "Add entry" "button" And I should see "field1" And I should see "field2" When the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field3 | Test field 3 description | Then I navigate to "Database" in current page administration And I click on "Add entry" "button" And I should see "field1" And I should see "field2" And I should see "field3" @javascript Scenario: The default view templates should be updated when a field is deleted. Given I navigate to "Database" in current page administration And I should see "field1" And I should see "field2" And I should see "Student entry 1" And I should see "Some content 1" And I should see "Student entry 2" And I should see "Some content 2" And I set the field "View mode tertiary navigation" to "Single view" And I should see "field1" And I should see "field2" And I should see "Student entry 1" And I should see "Some content 1" And I should not see "Student entry 2" And I should not see "Some content 2" When I navigate to "Fields" in current page administration And I click on "Actions" "button" in the "field2" "table_row" And I choose "Delete" in the open action menu And I click on "Continue" "button" Then I navigate to "Database" in current page administration And I should see "field1" And I should not see "field2" And I should see "Student entry 1" And I should not see "Some content 1" And I should see "Student entry 2" And I should not see "Some content 2" And I set the field "View mode tertiary navigation" to "Single view" And I should see "field1" And I should not see "field2" And I should see "Student entry 1" And I should not see "Some content 1" And I should not see "Student entry 2" And I should not see "Some content 2" Scenario: The default add templates should be updated when a field is deleted. Given I navigate to "Database" in current page administration And I click on "Add entry" "button" And I should see "field1" And I should see "field2" When I navigate to "Fields" in current page administration And I click on "Delete" "link" in the "field2" "table_row" And I click on "Continue" "button" Then I navigate to "Database" in current page administration And I click on "Add entry" "button" And I should see "field1" And I should not see "field2" @javascript Scenario: The dynamic default templates can be reset after a manual edition. Given I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "List view template" And I set the following fields to these values: | Header | New header! | | Repeated entry | This is the template content | | Footer | New footer! | And I click on "Save" "button" in the "sticky-footer" "region" And I navigate to "Database" in current page administration And I should see "New header!" And I should see "This is the template content" And I should see "New footer!" And I should not see "Student entry 1" And I should not see "Some content 1" When I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "List view template" And I click on "Actions" "button" And I choose "Reset current template" in the open action menu And I click on "Reset" "button" in the "Reset template?" "dialogue" And I should see "Template reset" And I navigate to "Database" in current page administration And I should not see "New header!" And I should not see "This is the template content" And I should not see "New footer!" And I should see "Student entry 1" And I should see "Some content 1" Then the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field3 | Test field 3 description | And I navigate to "Database" in current page administration And I click on "Add entry" "button" And I should see "field1" And I should see "field2" And I should see "field3" tests/behat/view_entries.feature000064400000025211152265517740013070 0ustar00@mod @mod_data Feature: Users can view and search database entries In order to find the database entries that I am looking for As a user I need to list and search the database entries Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Bob | 1 | student1@example.com | | student2 | Alice | 2 | student2@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "tags" exist: | name | isstandard | | Tag1 | 1 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | | data1 | text | Test field 2 name | Test field 2 description | | data1 | url | Test field 3 name | Test field 3 description | @javascript Scenario: Students can view, list and search entries Given the following "mod_data > entries" exist: | database | Test field name | Test field 2 name | Test field 3 name | | data1 | Student entry 1 | | https://moodledev.io | | data1 | Student entry 2 | | | | data1 | Student entry 3 | | | When I log in as "student1" And I am on the "Test database name" "data activity" page Then I should see "Student entry 1" # Confirm that the URL field is displayed as a link. And "https://moodledev.io" "link" should exist And I should see "Student entry 2" And I should see "Student entry 3" And I select "Single view" from the "jump" singleselect And I should see "Student entry 1" # Confirm that the URL field is displayed as a link. And "https://moodledev.io" "link" should exist And I should not see "Student entry 2" And "2" "link" should exist And "3" "link" should exist And I follow "Next" And I should see "Student entry 2" And I should not see "Student entry 1" And I click on "3" "link" in the "region-main" "region" And I should see "Student entry 3" And I should not see "Student entry 2" And I follow "Previous" And I should see "Student entry 2" And I should not see "Student entry 1" And I should not see "Student entry 3" And I select "List view" from the "jump" singleselect And I click on "Advanced search" "checkbox" And I set the field "Test field name" to "Student entry 1" And I click on "Save settings" "button" in the "data_adv_form" "region" And I should see "Student entry 1" And I should not see "Student entry 2" And I should not see "Student entry 3" And I set the field "Test field name" to "Student entry" And I set the field "Order" to "Descending" And I click on "Save settings" "button" in the "data_adv_form" "region" And "Student entry 3" "text" should appear before "Student entry 2" "text" And "Student entry 2" "text" should appear before "Student entry 1" "text" @javascript Scenario: Check that searching by tags works as expected Given the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | Test field 3 name | | data1 | student1 | Student original entry untagged | Student original entry untagged 2 | | And I am on the "Test database name" "data activity" page logged in as student1 And I click on "Add entry" "button" # This is required for now to prevent the tag suggestion menu from overlapping over the Save & view button. And I change window size to "large" And I set the following fields to these values: | Test field name | Student original entry tagged | | Test field 2 name | Student original entry tagged 2 | And I set the field with xpath "//div[@class='datatagcontrol']//input[@type='text']" to "Tag1" And I press "Save" And I should see "Student original entry" And I should see "Tag1" in the "div.tag_list" "css_element" And I open the action menu in "#defaulttemplate-single" "css_element" And I choose "Edit" in the open action menu And I should see "Tag1" in the ".form-autocomplete-selection" "css_element" And I follow "Cancel" And I select "List view" from the "jump" singleselect And I should see "Tag1" in the "div.tag_list" "css_element" And I click on "Advanced search" "checkbox" And I set the field with xpath "//div[@class='datatagcontrol']//input[@type='text']" to "Tag1" And I click on "#page-content" "css_element" When I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "Student original entry tagged" And I should see "Student original entry tagged 2" And I should not see "Student original entry untagged" And I should not see "Student original entry untagged 2" @javascript Scenario: Check that searching by first and last name works as expected Given the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | Test field 3 name | | data1 | student1 | Student entry 1 | | | | data1 | student2 | Student entry 2 | | | When I am on the "Test database name" "data activity" page logged in as teacher1 And I click on "Advanced search" "checkbox" And I set the field "First name" to "Bob" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "Found 1 out of 2 records." And I should see "Student entry 1" And I should not see "Student entry 2" And I set the field "First name" to "" And I set the field "Last name" to "2" And I click on "Save settings" "button" in the "data_adv_form" "region" And I should see "Found 1 out of 2 records." And I should not see "Student entry 1" And I should see "Student entry 2" # Search: no records found. But I set the field "Last name" to "" And I set the field "Test field name" to "Student entry 0" And I click on "Save settings" "button" in the "data_adv_form" "region" And I should see "No records found." And I should not see "Student entry 1" And I should not see "Student entry 2" # Search all the entries. And I set the field "Test field name" to "Student entry" And I click on "Save settings" "button" in the "data_adv_form" "region" And I should not see "Found 2 out of 2 records." And I should see "Student entry 1" And I should see "Student entry 2" @javascript Scenario: Database entries can be deleted in batch if delcheck is present Given the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | Test field 3 name | | data1 | student1 | Student entry 1 | Some student content 1 | http://moodle.com | | data1 | teacher1 | Teacher entry 2 | Some teacher content 2 | http://moodle.com | And I am on the "Test database name" "data activity" page logged in as teacher1 And I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "List view template" And I set the following fields to these values: | Repeated entry | ##delcheck##[[Test field name]]! | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration When I click on "Select all" "button" And I click on "Delete selected" "button" And I press "Delete" And I should see "No entries yet" @javascript Scenario: Database entries cannot be deleted in batch if delcheck is not present Given the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | Test field 3 name | | data1 | student1 | Student entry 1 | Some student content 1 | http://moodle.com | | data1 | teacher1 | Teacher entry 2 | Some teacher content 2 | http://moodle.com | And I am on the "Test database name" "data activity" page logged in as teacher1 Then I should not see "Select all" And I should not see "Delete selected" Scenario Outline: Entries are linked based on autolink and open in new window settings # Param1 refers to `Autolink`, param3 refers to `Open in new window`. Given the following "mod_data > fields" exist: | database | type | name | param1 | param3 | | data1 | url | URL field name | | | And the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | Test field 3 name | URL field name | | data1 | teacher1 | Test field entry | Test field 2 entry | http://example.com/ | www.moodle.org | When I am on the "Test database name" "data activity" page logged in as teacher1 Then "www.moodle.org" "link" exist # Verify that the URL field is rendered as a link with the correct href attribute and target set to _blank. And "a[target='_blank'][href='http://www.moodle.org']" "css_element" exist Examples: | param1 | param3 | autolink | | 0 | 0 | should not | | 1 | 1 | should | @javascript @accessibility Scenario: Check the accessibility of the database entries page (zero state) When I am on the "Test database name" "data activity" page logged in as "teacher1" Then I should see "No entries yet" And the page should meet accessibility standards @javascript @accessibility Scenario: Check the accessibility of the database entries page Given the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | Test field 3 name | | data1 | student1 | Student entry 1 | Some student content 1 | http://moodle.com | | data1 | teacher1 | Teacher entry 2 | Some teacher content 2 | http://moodle.com | When I am on the "Test database name" "data activity" page logged in as teacher1 Then the page should meet accessibility standards tests/behat/zero_state.feature000064400000006555152265517740012556 0ustar00@mod @mod_data Feature: Zero state page (no fields created) Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | n | C1 | data1 | Scenario: Teachers see buttons to manage database when there is no field created on view page Given I am on the "Test database name" "data activity" page logged in as "teacher1" And "Import a preset" "button" should exist And I am on the "Test database name" "data activity" page And "Create a field" "button" should exist And I click on "Create a field" "button" And I click on "Short text" "link" And I should see "Create a field" And I am on the "Test database name" "data activity" page And "Use a preset" "button" should exist And I click on "Use a preset" "button" And I should see "Presets" @javascript Scenario: Teachers see buttons to manage database when there is no field created on templates page Given I am on the "Test database name" "data activity" page logged in as "teacher1" And "Import a preset" "button" should exist When I click on "Import a preset" "button" Then I should see "Preset file" And I am on the "Test database name" "data activity" page And I click on "Templates" "link" And "Create a field" "button" should exist And I click on "Create a field" "button" And I click on "Short text" "link" And I should see "Create a field" And I am on the "Test database name" "data activity" page And I click on "Templates" "link" And "Use a preset" "button" should exist And I click on "Use a preset" "button" And I should see "Presets" @javascript @_file_upload Scenario: Teachers can import preset from the zero state page Given I am on the "Test database name" "data activity" page logged in as "teacher1" And "Import a preset" "button" should exist When I click on "Import a preset" "button" And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager Then I click on "Import preset and apply" "button" in the ".modal-dialog" "css_element" And I should see "Manage fields" Then I should see "Preset applied" @javascript Scenario: Teacher can use a preset from zero state page on an empty database Given I am on the "Test database name" "data activity" page logged in as "teacher1" And I click on "Use a preset" "button" And I click on "fullname" "radio" in the "Image gallery" "table_row" And the "Use this preset" "button" should be enabled Then I click on "Use this preset" "button" And I should not see "Field mappings" And I should see "Image" in the "image" "table_row" @javascript @accessibility Scenario: Check the accessibility of the database activity zero state When I am on the "Test database name" "data activity" page logged in as "teacher1" Then I should see "Start building your activity" And the page should meet accessibility standards tests/behat/import_presets.feature000064400000021277152265517740013454 0ustar00@mod @mod_data @javascript @_file_upload Feature: Users can import presets In order to use presets As a user I need to import and apply presets from zip files Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Mountain landscapes | n | C1 | data1 | Scenario: Teacher can import from preset page on an empty database Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" Then I should not see "Field mappings" And I should see "Image" in the "image" "table_row" Scenario: Teacher can import from preset page on a database with fields Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" Then I should see "Field mappings" And I should see "image" And I should see "Create a new field" in the "image" "table_row" Scenario: Teacher can import from preset page on a database with entries And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | field1 | | data1 | Student entry 1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" Then I should see "Field mappings" And I should see "image" And I should see "Create a new field" in the "image" "table_row" Scenario: Teacher can import from field page on a database with entries And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | field1 | | data1 | Student entry 1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" Then I should see "Field mappings" And I should see "title" And I should see "Create a new field" in the "title" "table_row" # We map existing field to keep the entry data And I set the field "id_title" to "Map to field1" And I click on "Continue" "button" And I follow "Database" And I should see "Student entry" Scenario: Teacher can import from zero state page on an empty database Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I click on "Import a preset" "button" And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" Then I should not see "Field mappings" And I should see "Image" in the "image" "table_row" Scenario: Importing a preset could create new fields Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | title | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "title" And I should not see "Description" And I should not see "image" And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" And I click on "Continue" "button" And I should see "Preset applied" Then I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" Scenario: Importing a preset could create map fields Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | oldtitle | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Fields" And I should see "oldtitle" And I should not see "Description" And I should not see "image" And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" # Let's map a field that is not mapped by default And I should see "Create a new field" in the "oldtitle" "table_row" And I set the field "id_title" to "Map to oldtitle" And I click on "Continue" "button" And I should see "Preset applied" Then I should not see "oldtitle" And I should see "title" And I should see "description" in the "description" "table_row" And I should see "image" in the "image" "table_row" Scenario: Importing same preset twice doesn't show mapping dialogue # Importing a preset on an empty database doesn't show the mapping dialogue, so we add a field for the database # not to be empty. Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | title | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" And I should see "Field mappings" And I click on "Continue" "button" And I should see "Preset applied" And I follow "Presets" And I choose the "Import preset" item in the "Action" action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager And I click on "Import preset and apply" "button" Then I should not see "Field mappings" And I should see "Preset applied" Scenario: Teacher can import from field page on a non-empty database and previous fields will be removed Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I click on "Actions" "button" And I choose "Import preset" in the open action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" And I click on "Continue" "button" Then I should see "Preset applied." And I follow "Fields" And I should see "image" And I should see "title" And I should not see "Test field name" tests/behat/tertiary_navigation.feature000064400000016013152265517740014447 0ustar00@mod @mod_data Feature: Users can navigate through the database activity using the tertiary navigation In order to use the database module As a user I need to navigate using the tertiary navigation. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | | data | Database without fields | Database intro | C1 | data2 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | | data1 | text | field2 | Test field 2 description | And the following "mod_data > entries" exist: | database | user | field1 | field2 | | data1 | teacher1 | Teacher entry 1 | Some content 1 | | data1 | teacher1 | Teacher entry 2 | Some content 2 | And the following "mod_data > presets" exist: | database | name | description | user | | data1 | Saved preset by teacher1 | This preset has also a description | teacher1 | And I log in as "admin" And the following config values are set as admin: | enableportfolios | 1 | And I navigate to "Plugins > Portfolios > Manage portfolios" in site administration And I set portfolio instance "File download" to "Enabled and visible" And I click on "Save" "button" And I log out And I am on the "Test database name" "data activity" page logged in as teacher1 @javascript Scenario: The tertiary navigation in the Database page. Given I navigate to "Database" in current page administration # Teacher: List view. Then I should not see "List view" in the "data-listview-content" "region" And the "Import entries" item should exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" And the "Export entries" item should exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" And the "Export to portfolio" item should exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" # Teacher: Single view. And I set the field "View mode tertiary navigation" to "Single view" And I should not see "Single view" in the "data-singleview-content" "region" And the "Import entries" item should exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" And the "Export entries" item should exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" And the "Export to portfolio" item should not exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" # Teacher: Database without fields. And I am on the "Database without fields" "data activity" page And "Actions" "actionmenu" should not exist # Student without entries: List view. And I am on the "Test database name" "data activity" page logged in as student1 And "Actions" "actionmenu" should not exist in the ".tertiary-navigation" "css_element" # Student without entries: Single view. And I set the field "View mode tertiary navigation" to "Single view" And I should not see "Actions" in the ".tertiary-navigation" "css_element" # Student with entries: Single view. But the following "mod_data > entries" exist: | database | user | field1 | field2 | | data1 | student1 | Student entry 3 | Some content 3 | And I should not see "Actions" in the ".tertiary-navigation" "css_element" # Student with entries: List view. And I set the field "View mode tertiary navigation" to "List view" And the "Import entries" item should not exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" And the "Export entries" item should not exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" And the "Export to portfolio" item should exist in the "Actions" action menu of the ".tertiary-navigation" "css_element" @javascript Scenario: The tertiary navigation in the Presets page. Given I navigate to "Presets" in current page administration When I click on "Actions" "button" Then I should see "Import preset" in the ".presetsactions" "css_element" And I should see "Export preset" in the ".presetsactions" "css_element" And I should see "Publish preset on this site" in the ".presetsactions" "css_element" And I press the escape key # Database without fields. But I am on the "Database without fields" "data activity" page And I navigate to "Presets" in current page administration And I click on "Actions" "button" And I should see "Import preset" in the ".presetsactions" "css_element" And I should not see "Export preset" in the ".presetsactions" "css_element" And I should not see "Publish preset on this site" in the ".presetsactions" "css_element" Scenario: The tertiary navigation in the Presets preview page. Given I navigate to "Presets" in current page administration When I follow "Saved preset by teacher1" Then I should see "Preview of Saved preset by teacher1" And "Use this preset" "button" should exist # Single view And I set the field "Templates tertiary navigation" to "Single view template" And I should see "Preview of Saved preset by teacher1" And "Use this preset" "button" should exist Scenario: The tertiary navigation in the Fields page. Given I navigate to "Fields" in current page administration Then the "Edit" item should exist in the "Actions" action menu of the "field1" "table_row" And the "Delete" item should exist in the "Actions" action menu of the "field1" "table_row" And I should not see "Actions" in the ".tertiary-navigation" "css_element" @javascript Scenario: The tertiary navigation in the Templates page. Given I navigate to "Templates" in current page administration When I click on "Actions" "button" Then I should see "Export preset" in the ".presetsactions" "css_element" And I should see "Publish preset on this site" in the ".presetsactions" "css_element" And I press the escape key And I should see "Add entry template" # List template. And I set the field "Templates tertiary navigation" to "List view template" And I should not see "Add entry template" And I should see "Header" And I should see "Repeated entry" And I should see "Footer" tests/behat/advanced_search.feature000064400000010115152265517740013454 0ustar00@mod @mod_data Feature: Database entries can be searched using an advanced search form. In order to find an entry As a user I need to have an advanced search form Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | n | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | My Field | Field 1 description | | data1 | text | Their field | Field 2 description | And the following "mod_data > entries" exist: | database | user | My Field | Their field | | data1 | teacher1 | First content | Owned content | | data1 | teacher1 | Second content | Authored content | And I am on the "Test database name" "data activity" page logged in as teacher1 And I should see "First content" And I should see "Second content" @javascript Scenario: Content can be searched using advanced search Given I click on "Advanced search" "checkbox" And I should see "My Field" in the "data_adv_form" "region" And I should see "Their field" in the "data_adv_form" "region" When I set the field "My Field" to "First" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "First content" And I should not see "Second content" @javascript Scenario: Advanced search template can use field information tags Given I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "Advanced search template" And I set the following fields to these values: | Advanced search template | The test is on [[My Field#name]], [[My Field#description]], and the input [[My Field]] | And I click on "Save" "button" in the "sticky-footer" "region" And I navigate to "Database" in current page administration And I should see "First content" And I should see "Second content" And I click on "Advanced search" "checkbox" And I should see "The test is on My Field, Field 1 description, and the input" in the "data_adv_form" "region" And I should not see "Their field" in the "data_adv_form" "region" When I set the field "My Field" to "First" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "First content" And I should not see "Second content" @javascript Scenario: Advanced search can use otherfields tag Given I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "Advanced search template" And I set the following fields to these values: | Advanced search template | Main search [[My Field]], Other fields ##otherfields## | And I click on "Save" "button" in the "sticky-footer" "region" And I navigate to "Database" in current page administration And I should see "First content" And I should see "Second content" And I click on "Advanced search" "checkbox" And I should see "Main search" in the "data_adv_form" "region" And I should see "Other fields" in the "data_adv_form" "region" And I should see "Their field" in the "data_adv_form" "region" When I set the field "My Field" to "First" And I click on "Save settings" "button" in the "data_adv_form" "region" Then I should see "First content" And I should not see "Second content" And I set the field "My Field" to "" And I set the field "Their field" to "Authored content" And I click on "Save settings" "button" in the "data_adv_form" "region" And I should not see "First content" And I should see "Second content" tests/behat/behat_mod_data.php000064400000004640152265517740012437 0ustar00. /** * Steps definitions related with the database activity. * * @package mod_data * @category test * @copyright 2014 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); use Behat\Gherkin\Node\TableNode as TableNode; /** * Database-related steps definitions. * * @package mod_data * @category test * @copyright 2014 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_data extends behat_base { /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | pagetype | name meaning | description | * | Add entry | Database name | Add an entry page (view.php) | * * @param string $type identifies which type of page this is, e.g. 'Add entry'. * @param string $identifier identifies the particular page, e.g. 'My database name'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { global $DB; switch (strtolower($type)) { case 'add entry': return new moodle_url('/mod/data/edit.php', [ 'd' => $this->get_cm_by_activity_name('data', $identifier)->instance, ]); default: throw new Exception("Unrecognised page type '{$type}'"); } } } tests/behat/actionsmenu_replacement.feature000064400000041357152265517740015302 0ustar00@mod @mod_data Feature: Users can add the ##actionsmenu## replacement to the database templates In order to display all the actions for entries in templates As a teacher I need to edit the templates and add the actionsmenu replacement Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | | data1 | text | field2 | Test field 2 description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | user | field1 | field2 | | data1 | student1 | Student entry 1 | Some student content 1 | | data1 | teacher1 | Teacher entry 1 | Some teacher content 1 | And I am on the "Test database name" "data activity" page logged in as teacher1 And I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "List view template" And I set the field "Enable code editor" to "0" And I set the following fields to these values: | Header | | | Repeated entry | | | Footer |
[[field1]]##actionsmenu##
| And I click on "Save" "button" in the "sticky-footer" "region" And I set the field "Templates tertiary navigation" to "Single view template" And I set the following fields to these values: | Single view template |
[[field1]][[field2]]##actionsmenu##
| And I click on "Save" "button" in the "sticky-footer" "region" @javascript Scenario: The ##actionsmenu## replacement displays the expected actions with default settings depending on the user permissions Given I navigate to "Database" in current page administration # Teachers should be able to edit/delete all the entries. When I open the action menu in "Student entry 1" "table_row" Then I should see "Show more" And I should see "Edit" And I should see "Delete" But I should not see "Approve" And I should not see "Undo approval" And I should not see "Export to portfolio" And I press the escape key And I open the action menu in "Teacher entry 1" "table_row" And I should see "Show more" And I should see "Edit" And I should see "Delete" # Single view (for teacher). And I choose "Show more" in the open action menu And I should see "Teacher entry 1" And I should see "Some teacher content 1" And I should not see "Student entry 1" And I open the action menu in "Teacher entry 1" "table_row" And I should not see "Show more" And I should see "Edit" And I should see "Delete" And I should not see "Approve" And I should not see "Undo approval" And I should not see "Export to portfolio" And I press the escape key And I follow "Previous page" And I should see "Student entry 1" And I should see "Some student content 1" And I should not see "Teacher entry 1" And I open the action menu in "Student entry 1" "table_row" And I should not see "Show more" And I should see "Edit" And I should see "Delete" And I log out # Students only should edit/delete their entries. But I am on the "Test database name" "data activity" page logged in as student1 And I open the action menu in "Student entry 1" "table_row" And I should see "Show more" And I should see "Edit" And I should see "Delete" And I should not see "Approve" And I should not see "Undo approval" And I should not see "Export to portfolio" And I press the escape key And I open the action menu in "Teacher entry 1" "table_row" And I should see "Show more" And I should not see "Edit" And I should not see "Delete" # Single view (for student). And I choose "Show more" in the open action menu And I should see "Teacher entry 1" And I should see "Some teacher content 1" And I should not see "Student entry 1" And I should not see "Actions" in the "Teacher entry 1" "table_row" And I follow "Previous page" And I should see "Student entry 1" And I should see "Some student content 1" And I should not see "Teacher entry 1" And I open the action menu in "Student entry 1" "table_row" And I should not see "Show more" And I should see "Edit" And I should see "Delete" And I should not see "Approve" And I should not see "Undo approval" And I should not see "Export to portfolio" @javascript Scenario: The ##actionsmenu## replacement displays the Approval/Undo approval options Given I navigate to "Settings" in current page administration And I follow "Entries" And I set the field "Approval required" to "Yes" And I press "Save and display" When I navigate to "Database" in current page administration # Teachers should be able to approve/unapprove all the entries from list view. And I open the action menu in "Student entry 1" "table_row" Then I should see "Approve" And I should not see "Undo approval" And I choose "Approve" in the open action menu And I should see "Entry approved" And I press "Dismiss this notification" And I open the action menu in "Student entry 1" "table_row" And I should see "Undo approval" And I should not see "Approve" in the ".menu-action-text" "css_element" And I press the escape key And I open the action menu in "Teacher entry 1" "table_row" And I should see "Undo approval" And I should not see "Approve" in the ".menu-action-text" "css_element" # Single view (for teacher). And I choose "Show more" in the open action menu And I should see "Teacher entry 1" And I open the action menu in "Teacher entry 1" "table_row" And I should see "Undo approval" And I should not see "Approve" And I press the escape key And I follow "Previous page" And I should see "Student entry 1" And I open the action menu in "Student entry 1" "table_row" And I should not see "Approve" And I should see "Undo approval" # Check entries can be approved/unapproved from single view too. And I choose "Undo approval" in the open action menu And I should see "Entry unapproved" And I press "Dismiss this notification" And I open the action menu in "Student entry 1" "table_row" And I should see "Approve" And I should not see "Undo approval" And I log out # Students should not see the Approve/Undo approval options. But I am on the "Test database name" "data activity" page logged in as student1 And I open the action menu in "Teacher entry 1" "table_row" And I should not see "Approve" And I should not see "Undo approval" And I press the escape key And I open the action menu in "Student entry 1" "table_row" And I should not see "Approve" And I should not see "Undo approval" # Single view (for student). And I choose "Show more" in the open action menu And I should see "Student entry 1" And I open the action menu in "Student entry 1" "table_row" And I should not see "Approve" And I should not see "Undo approval" And I follow "Next page" And I should see "Teacher entry 1" And I should not see "Actions" in the "Teacher entry 1" "table_row" @javascript Scenario: The ##actionsmenu## replacement displays the Export to portfolio options Given I log in as "admin" And the following config values are set as admin: | enableportfolios | 1 | And I navigate to "Plugins > Portfolios > Manage portfolios" in site administration And I set portfolio instance "File download" to "Enabled and visible" And I click on "Save" "button" And I log out And I am on the "Test database name" "data activity" page logged in as teacher1 # Teachers should be able to export to portfolio all the entries from list view. When I open the action menu in "Student entry 1" "table_row" Then I should see "Export to portfolio" And I choose "Export to portfolio" in the open action menu And I should see "Configure exported data" And I press "Cancel" And I click on "Yes" "button" in the "Confirm" "dialogue" And I open the action menu in "Teacher entry 1" "table_row" And I should see "Export to portfolio" # Single view (for teacher). And I choose "Show more" in the open action menu And I should see "Teacher entry 1" And I open the action menu in "Teacher entry 1" "table_row" And I should see "Export to portfolio" And I press the escape key And I follow "Previous page" And I should see "Student entry 1" And I open the action menu in "Student entry 1" "table_row" And I should see "Export to portfolio" # Check entries can be exported from single view too. And I choose "Export to portfolio" in the open action menu And I should see "Configure exported data" And I log out # Students should only export their entries. But I am on the "Test database name" "data activity" page logged in as student1 And I open the action menu in "Teacher entry 1" "table_row" And I should not see "Export to portfolio" And I press the escape key And I open the action menu in "Student entry 1" "table_row" And I should see "Export to portfolio" And I choose "Export to portfolio" in the open action menu And I should see "Configure exported data" And I press "Cancel" And I click on "Yes" "button" in the "Confirm" "dialogue" And I open the action menu in "Teacher entry 1" "table_row" # Single view (for student). And I choose "Show more" in the open action menu And I should see "Teacher entry 1" And I should not see "Actions" in the "Teacher entry 1" "table_row" And I follow "Previous page" And I should see "Student entry 1" And I open the action menu in "Student entry 1" "table_row" And I should see "Export to portfolio" And I choose "Export to portfolio" in the open action menu And I should see "Configure exported data" @javascript Scenario: The ##actionsmenu## replacement does not display the Export to portfolio option when there are no portfolios enabled Given I log in as "admin" And the following config values are set as admin: | enableportfolios | 1 | And I log out And I am on the "Test database name" "data activity" page logged in as teacher1 When I open the action menu in "Student entry 1" "table_row" Then I should not see "Export to portfolio" And I log out # If we enable, at least, one portfolio, the Export to portfolio option should be displayed. But I log in as "admin" And I navigate to "Plugins > Portfolios > Manage portfolios" in site administration And I set portfolio instance "File download" to "Enabled and visible" And I click on "Save" "button" And I log out And I am on the "Test database name" "data activity" page logged in as teacher1 And I open the action menu in "Student entry 1" "table_row" And I should see "Export to portfolio" @javascript Scenario: The Edit option in the ##actionsmenu## replacement is working Given I navigate to "Database" in current page administration # Teachers should be able to edit any entry. And I open the action menu in "Student entry 1" "table_row" When I choose "Edit" in the open action menu And I set the field "field2" to "Some MODIFIED BY THE TEACHER student content 1" And I click on "Save" "button" # Single view (for teacher). Then I should see "Some MODIFIED BY THE TEACHER student content 1" And I should not see "Some student content 1" And I open the action menu in "Student entry 1" "table_row" And I choose "Edit" in the open action menu And I set the field "field2" to "Some MORE TEACHER MODIFICATIONS FOR student content 1" And I click on "Save" "button" And I should see "Some MORE TEACHER MODIFICATIONS FOR student content 1" And I should not see "Some MODIFIED BY THE TEACHER student content 1" And I log out # Students only should edit their entries. But I am on the "Test database name" "data activity" page logged in as student1 And I open the action menu in "Student entry 1" "table_row" And I choose "Edit" in the open action menu And I set the field "field2" to "Some MODIFIED student content 1" And I click on "Save" "button" # Single view (for student). And I should see "Some MODIFIED student content 1" And I should not see "Some MORE TEACHER MODIFICATIONS FOR student content 1" And I open the action menu in "Student entry 1" "table_row" And I choose "Edit" in the open action menu And I set the field "field2" to "Some MORE MODIFICATIONS FOR student content 1" And I click on "Save" "button" And I should see "Some MORE MODIFICATIONS FOR student content 1" And I should not see "Some MODIFIED student content 1" @javascript Scenario: The Delete option in the ##actionsmenu## replacement is working Given the following "mod_data > entries" exist: | database | user | field1 | field2 | | data1 | student1 | Student entry 2 | Some student content 2 | | data1 | teacher1 | Teacher entry 2 | Some teacher content 2 | And I navigate to "Database" in current page administration # Teachers should be able to delete any entry. And I open the action menu in "Student entry 1" "table_row" When I choose "Delete" in the open action menu Then I should see "Delete entry" # Cancel doesn't delete the entry. And I click on "Cancel" "button" in the "Confirm" "dialogue" And I open the action menu in "Teacher entry 1" "table_row" # But Delete removes the entry. And I choose "Delete" in the open action menu And I should see "Delete entry" And I click on "Delete" "button" in the "Confirm" "dialogue" And I should see "Entry deleted" And I should not see "Teacher entry 1" And I should see "Teacher entry 2" And I should see "Student entry 1" And I should see "Student entry 2" # Single view (for teacher). And I open the action menu in "Teacher entry 2" "table_row" And I choose "Delete" in the open action menu And I should see "Delete entry" And I click on "Delete" "button" in the "Confirm" "dialogue" And I should see "Entry deleted" And I should not see "Teacher entry 1" And I should not see "Teacher entry 2" And I should see "Student entry 1" And I should see "Student entry 2" And I log out # Students only should edit their entries. But I am on the "Test database name" "data activity" page logged in as student1 And I open the action menu in "Student entry 1" "table_row" When I choose "Delete" in the open action menu Then I should see "Delete entry" # Cancel doesn't delete the entry. And I click on "Cancel" "button" in the "Confirm" "dialogue" And I open the action menu in "Student entry 1" "table_row" # But Delete removes the entry. And I choose "Delete" in the open action menu And I should see "Delete entry" And I click on "Delete" "button" in the "Confirm" "dialogue" And I should see "Entry deleted" And I should not see "Teacher entry 1" And I should not see "Teacher entry 2" And I should not see "Student entry 1" And I should see "Student entry 2" # Single view (for student). And I open the action menu in "Student entry 2" "table_row" And I choose "Delete" in the open action menu And I should see "Delete entry" And I click on "Delete" "button" in the "Confirm" "dialogue" And I should see "Entry deleted" And I should not see "Teacher entry 1" And I should not see "Teacher entry 2" And I should not see "Student entry 1" And I should not see "Student entry 2" tests/behat/add_entries.feature000064400000021637152265517740012656 0ustar00@mod @mod_data Feature: Users can add entries to database activities In order to populate databases As a user I need to add entries to databases Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | n | C1 | data1 | @javascript Scenario: Students can add entries to a database Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | | data1 | text | Test field 2 name | Test field 2 description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | | data1 | student1 | Student original entry | Student original entry 2 | And I am on the "data1" Activity page logged in as student1 And I open the action menu in "#data-listview-content" "css_element" And I choose "Edit" in the open action menu And I set the following fields to these values: | Test field name | Student original entry | | Test field 2 name | | And I press "Save" Then I should not see "Student original entry 2" And I open the action menu in "#data-singleview-content" "css_element" And I choose "Edit" in the open action menu And I set the following fields to these values: | Test field name | Student edited entry | And I press "Save" And I should see "Student edited entry" And the following "mod_data > entries" exist: | database | user | Test field name | Test field 2 name | | data1 | student1 | Student second entry | | | data1 | student1 | Student third entry | | And I am on the "data1" Activity page logged in as student1 And I should see "Student edited entry" And I should see "Student second entry" And I should see "Student third entry" # Will delete the first one. And I open the action menu in ".defaulttemplate-listentry" "css_element" And I choose "Delete" in the open action menu And I press "Delete" And I should not see "Student edited entry" And I should see "Student second entry" And I should see "Student third entry" @javascript Scenario: If a new text area entry is added, the filepicker is displayed in the H5P editor dialogue Given the following "mod_data > fields" exist: | database | type | name | | data1 | textarea | Textarea field name | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And I am on the "Course 1" course page logged in as teacher1 When I click on "Test database name" "link" And I click on "Add entry" "button" And I click on "Insert H5P content" "button" Then I should see "Browse repositories..." in the "Insert H5P content" "dialogue" @javascript Scenario: If maximum number of entries is set other than None then add entries should be seen only if number of entries is less than it Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | Test1 | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | user | Test1 | | data1 | student1 | foo | | data1 | student1 | bar | And I am on the "Test database name" "data activity" page logged in as teacher1 And I navigate to "Settings" in current page administration And I expand all fieldsets And I set the following fields to these values: | Maximum number of entries | 2 | And I press "Save and display" And I log out When I am on the "Test database name" "data activity" page logged in as student1 Then I should not see "Add entry" And I log out And I am on the "Test database name" "data activity" page logged in as teacher1 And I navigate to "Settings" in current page administration And I expand all fieldsets And I set the following fields to these values: | Maximum number of entries | 3 | And I press "Save and display" And I log out And I am on the "Test database name" "data activity" page logged in as student1 And I should see "Add entry" @javascript Scenario: Guest user cannot add entries to a database Given the following "mod_data > fields" exist: | database | type | name | | data1 | text | Textarea field name | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And I am on the "Course 1" "enrolment methods" page logged in as teacher1 And I click on "Enable" "link" in the "Guest access" "table_row" And I log out When I am on the "Test database name" "data activity" page logged in as "guest" Then I should not see "Add entry" @javascript Scenario Outline: Users see the Add entry button in the view page when some field has been created only. Given I am on the "Test database name" "data activity" page logged in as And I should not see "Add entry" And I log out When the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | Then I am on the "Test database name" "data activity" page logged in as And I should see "Add entry" Examples: | user | | teacher1 | | student1 | @javascript @_file_upload Scenario Outline: Users can't upload non-picture files into a picture field Given the following "mod_data > fields" exist: | database | type | name | | data1 | picture | Test field 1 | And I am on the "Test database name" "data activity" page logged in as student1 And I press "Add entry" And I follow "Add..." And I follow "Upload a file" When I click on "Upload a file" "link" in the ".fp-repo-area" "css_element" # Upload non-image files. And I set the field "Attachment" to "" And I press "Upload this file" # Confirm non-image files cannot be uploaded. Then I should see "" Examples: | filepath | errormessage | | #dirroot#/lib/tests/fixtures/empty.txt | Text file filetype cannot be accepted. | | #dirroot#/lib/tests/fixtures/timezonewindows.xml | application/xml filetype cannot be accepted. | | #dirroot#/mod/data/tests/fixtures/behat_preset.zip | Archive (ZIP) filetype cannot be accepted. | | #dirroot#/mod/data/tests/fixtures/test_data_content.csv | Comma-separated values filetype cannot be accepted. | @javascript @_file_upload Scenario: Users can upload picture files into a picture field Given the following "mod_data > fields" exist: | database | type | name | | data1 | picture | Test field 1 | And I am on the "Test database name" "data activity" page logged in as student1 And I press "Add entry" And I follow "Add..." And I follow "Upload a file" When I click on "Upload a file" "link" in the ".fp-repo-area" "css_element" # Confirm image files can be uploaded. And I set the field "Attachment" to "#dirroot#/lib/tests/fixtures/gd-logo.png" Then "Error" "dialogue" should not be visible And I press "Upload this file" And I should see "gd-logo.png" tests/behat/edit_entries.feature000064400000007572152265517740013055 0ustar00@mod @mod_data @javascript @editor_tiny Feature: Edit existing database entries In order to modify a database entry As a teacher I need to successfully have changes recorded for each entry Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Intro text | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | required | description | | data1 | text | headline | 1 | Headline | | data1 | textarea | description | 0 | Description | And the following "mod_data > entries" exist: | database | user | headline | description | | data1 | teacher1 | Headline 1 | Original text | | data1 | teacher1 | Headline 2 | | Scenario: Text areas are filled correctly when editing datasets Given I am on the "Test database name" "data activity" page logged in as "teacher1" And I select "Single view" from the "jump" singleselect And I should see "Headline 1" in the "region-main" "region" And I should see "Original text" in the "region-main" "region" And I click on ".defaulttemplate-single-body .action-menu" "css_element" # Edit fields and observe changes. And I click on "Edit" "link" in the ".defaulttemplate-single-body .dropdown-menu" "css_element" And I wait until the page is ready And I set the field "headline" to "New headline" And I set the field "description" to "New text" When I click on "Save" "button" in the "sticky-footer" "region" Then I should not see "Headline 1" in the "region-main" "region" And I should not see "Original text" in the "region-main" "region" And I should see "New headline" in the "region-main" "region" And I should see "New text" in the "region-main" "region" Scenario: Text areas are filled correctly when triggering autosave without making changes Given I am on the "Test database name" "data activity" page logged in as "teacher1" And I select "Single view" from the "jump" singleselect # Edit the the first DB entry textfield, but don't save the changes. And I click on ".defaulttemplate-single-body .action-menu" "css_element" And I click on "Edit" "link" in the ".defaulttemplate-single-body .dropdown-menu" "css_element" And I wait until the page is ready And I set the field "description" to "Unsaved changes" # Trigger autosave. And I press tab And I wait "1" seconds And I click on "Cancel" "button" in the "sticky-footer" "region" # Edit the second DB entry headline only. And I select "Single view" from the "jump" singleselect And I click on "2" "link" in the "sticky-footer" "region" And I click on ".defaulttemplate-single-body .action-menu" "css_element" And I click on "Edit" "link" in the ".defaulttemplate-single-body .dropdown-menu" "css_element" And I wait until the page is ready And I set the field "headline" to "New headline" And I click on "Save" "button" in the "sticky-footer" "region" And I select "Single view" from the "jump" singleselect And I click on "2" "link" in the "sticky-footer" "region" # Only the new heading is updated and the description field autosave did not carry over from the first entry. And I should see "New headline" in the "region-main" "region" And I should not see "Unsaved changes" in the "region-main" "region" tests/behat/edit_templates.feature000064400000024173152265517740013376 0ustar00@mod @mod_data Feature: Users can edit the database templates In order to use custom templates for entries As a teacher I need to edit the templates html Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | field1 | Test field description | | data1 | text | field2 | Test field 2 description | And the following "mod_data > entries" exist: | database | field1 | field2 | | data1 | Student entry 1 | Some content 1 | And I am on the "Test database name" "data activity" page logged in as teacher1 And I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "List view template" @javascript Scenario: Edit list template Given I set the following fields to these values: | Header | New header! | | Repeated entry | [[field1]] and [[field2]]! | | Footer | New footer! | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration Then I should see "New header!" And I should see "Student entry 1 and Some content 1!" And I should see "New footer!" @javascript Scenario: Edit single template Given I set the field "Templates tertiary navigation" to "Single view template" And I set the following fields to these values: | Single view template | [[field1]] and [[field2]] details! | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration And I set the field "View mode tertiary navigation" to "Single view" Then I should see "Student entry 1 and Some content 1 details!" @javascript Scenario: Edit add entry template Given I set the field "Templates tertiary navigation" to "Add entry template" And I set the following fields to these values: | Add entry template | [[field1]] [[field2]] Form extra! | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration And I click on "Add entry" "button" Then I should see "Form extra!" @javascript Scenario: Edit advanced search template Given I set the field "Templates tertiary navigation" to "Advanced search template" And I set the following fields to these values: | Advanced search template | New advanced search template! | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration And I click on "Advanced search" "checkbox" Then I should see "New advanced search template!" @javascript Scenario: Edit without the wysiwyg editor Given I click on "Enable code editor" "checkbox" And I set the following fields to these values: | Repeated entry | NopeYep! | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration Then I should not see "Nope" And I should see "Yep!" @javascript Scenario: Edit CSS teamplate Given I click on "Enable code editor" "checkbox" And I set the following fields to these values: | Repeated entry | NopeYep! | And I click on "Save" "button" in the "sticky-footer" "region" And I set the field "Templates tertiary navigation" to "Custom CSS" And I set the following fields to these values: | Custom CSS | .hideme {display: none;} | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration Then I should not see "Nope" And I should see "Yep!" @javascript Scenario: Edit Custom JavaScript Given I click on "Enable code editor" "checkbox" And I set the following fields to these values: | Repeated entry | NopeYep! | And I click on "Save" "button" in the "sticky-footer" "region" And I set the field "Templates tertiary navigation" to "Custom JavaScript" And I set the following fields to these values: | Custom JavaScript | window.onload = () => document.querySelector('#hideme').style.display = 'none'; | And I click on "Save" "button" in the "sticky-footer" "region" When I navigate to "Database" in current page administration Then I should not see "Nope" And I should see "Yep!" @javascript Scenario: Reset database activity template Given I set the following fields to these values: | Header | New header! | | Repeated entry | This is the template content | | Footer | New footer! | And I click on "Save" "button" in the "sticky-footer" "region" And I navigate to "Database" in current page administration And I should see "New header!" And I should see "This is the template content" And I should see "New footer!" And I should not see "Student entry 1" And I should not see "Some content 1" When I navigate to "Templates" in current page administration And I set the field "Templates tertiary navigation" to "List view template" And I click on "Actions" "button" And I choose "Reset current template" in the open action menu And I should see "This will permanently remove the List view template for your current preset." And I click on "Reset" "button" in the "Reset template?" "dialogue" Then I should see "Template reset" And I navigate to "Database" in current page administration And I should not see "New header!" And I should not see "This is the template content" And I should not see "New footer!" And I should see "Student entry 1" And I should see "Some content 1" @javascript Scenario: Reset all database templates using the action menu Given the following "mod_data > templates" exist: | database | name | content | | data1 | singletemplate | Initial single | | data1 | listtemplate | Initial list | | data1 | addtemplate | Initial add | | data1 | asearchtemplate | Initial search | And I navigate to "Database" in current page administration And I should see "Initial list" And I should not see "Student entry 1" And I should not see "Some content 1" And I click on "Advanced search" "checkbox" And I should see "Initial search" And I set the field "View mode tertiary navigation" to "Single view" And I should see "Initial single" And I should not see "Student entry 1" And I should not see "Some content 1" And I click on "Add entry" "button" And I should see "Initial add" When I navigate to "Templates" in current page administration And I click on "Actions" "button" And I choose "Reset all templates" in the open action menu And I should see "You're about to remove all templates for your current preset." And I click on "Reset" "button" in the "Reset all templates?" "dialogue" Then I should see "All templates reset" And I navigate to "Database" in current page administration And I should not see "Initial list" And I should see "Student entry 1" And I should see "Some content 1" And I click on "Advanced search" "checkbox" And I should not see "Initial search" And I set the field "View mode tertiary navigation" to "Single view" And I should not see "Initial single" And I should see "Student entry 1" And I should see "Some content 1" And I click on "Add entry" "button" And I should not see "Initial add" @javascript Scenario: Reset all database templates using the reset template button Given the following "mod_data > templates" exist: | database | name | content | | data1 | singletemplate | Initial single | | data1 | listtemplate | Initial list | | data1 | addtemplate | Initial add | | data1 | asearchtemplate | Initial search | And I navigate to "Database" in current page administration And I should see "Initial list" And I should not see "Student entry 1" And I should not see "Some content 1" And I click on "Advanced search" "checkbox" And I should see "Initial search" And I set the field "View mode tertiary navigation" to "Single view" And I should see "Initial single" And I should not see "Student entry 1" And I should not see "Some content 1" And I click on "Add entry" "button" And I should see "Initial add" When I navigate to "Templates" in current page administration And I click on "Actions" "button" And I choose "Reset current template" in the open action menu And I should see "This will permanently remove the Add entry template for your current preset." And I click on "Reset all templates" "checkbox" And I click on "Reset" "button" in the "Reset template?" "dialogue" Then I should see "All templates reset" And I navigate to "Database" in current page administration And I should not see "Initial list" And I should see "Student entry 1" And I should see "Some content 1" And I click on "Advanced search" "checkbox" And I should not see "Initial search" And I set the field "View mode tertiary navigation" to "Single view" And I should not see "Initial single" And I should see "Student entry 1" And I should see "Some content 1" And I click on "Add entry" "button" And I should not see "Initial add" tests/behat/data_activity_completion.feature000064400000013674152265517740015455 0ustar00@mod @mod_data @core_completion @javascript Feature: View activity completion in the database activity In order to have visibility of database completion requirements As a student I need to be able to view my database completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | enablecompletion | showcompletionconditions | | Course 1 | C1 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | data | | course | C1 | | idnumber | mh1 | | name | Music history | | section | 1 | | completionentriesenabled | 1 | | completionentries | 2 | And the following "mod_data > fields" exist: | database | type | name | | mh1 | text | Instrument types | And the following "mod_data > templates" exist: | database | name | | mh1 | singletemplate | | mh1 | listtemplate | | mh1 | addtemplate | | mh1 | asearchtemplate | | mh1 | rsstemplate | Given I am on the "Music history" "data activity editing" page logged in as teacher1 And I expand all fieldsets And I set the following fields to these values: | Aggregate type | Average of ratings | | scale[modgrade_type] | Point | | scale[modgrade_point] | 100 | | Add requirements | 1 | | View the activity | 1 | | Receive a grade | 1 | | Any grade | 1 | And I press "Save and display" And I log out Scenario: Database activity displays completion conditions to teachers # We add an entry to let the user change to a different view. Given the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | teacher1 | Drums | When I am on the "Music history" "data activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Make entries: 2" completion condition And "Music history" should have the "Receive a grade" completion condition And I select "Single view" from the "jump" singleselect And "Music history" should have the "View" completion condition And "Music history" should have the "Make entries: 2" completion condition And "Music history" should have the "Receive a grade" completion condition Scenario: A student can complete an database activity by achieving a passing grade Given I am on the "Music history" "data activity" page logged in as student1 And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | student1 | Drums | And I am on "Course 1" course homepage # One entry is not enough to mark as complete. And the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "todo" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And the following "mod_data > entries" exist: | database | user | Instrument types | | mh1 | student1 | Hurdygurdy | And I am on "Course 1" course homepage Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "todo" And I log out And I am on the "Music history" "data activity" page logged in as teacher1 And I select "Single view" from the "jump" singleselect And I set the field "rating" to "3" And I log out When I am on the "Music history" "data activity" page logged in as student1 Then the "View" completion condition of "Music history" is displayed as "done" And the "Make entries: 2" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And I log out When I am on the "Course 1" course page logged in as teacher1 And "Vinnie Student1" user has completed "Music history" activity @javascript Scenario: A student can manually mark the data activity as done but a teacher cannot Given I am on the "Music history" "data activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and display" # Teacher view. And the manual completion button for "Music history" should be disabled And I log out # Student view. When I am on the "Music history" "data activity" page logged in as student1 Then the manual completion button of "Music history" is displayed as "Mark as done" And I toggle the manual completion state of "Music history" And the manual completion button of "Music history" is displayed as "Done" tests/behat/create_activity.feature000064400000003557152265517740013555 0ustar00@mod @mod_data Feature: Teachers can enable comments only if comments are enabled at site level In order to enable comments on entries As an admin I need to enable comments at site level Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | When I log in as "teacher1" And I am on "Course 1" course homepage And I turn editing mode on @javascript Scenario: Teacher can enable comments if they are enabled at site level Given I open the activity chooser And I click on "Add a new Database" "link" in the "Add an activity or resource" "dialogue" When I expand all fieldsets And "Allow comments on entries" "field" should exist And I set the field "Name" to "Test Database name" And I set the field "Allow comments on entries" to "Yes" And I press "Save and return to course" And I should see "Test Database name" @javascript Scenario: Teacher cannot enable comments if they are disabled at site level # Disable comments in site config. Given the following config values are set as admin: | usecomments | 0 | And I open the activity chooser And I click on "Add a new Database" "link" in the "Add an activity or resource" "dialogue" When I expand all fieldsets And I set the field "Name" to "Test Database name 2" And "Allow comments on entries" "field" should not exist Then I should see "No" in the "//*[@id=\"fitem_id_comments\"]/*[@data-fieldtype=\"selectyesno\"]" "xpath_element" And I press "Save and return to course" And I should see "Test Database name 2" tests/behat/preview_preset.feature000064400000021702152265517740013431 0ustar00@mod @mod_data Feature: Users can preview presets In order to find the preset I am looking for As a teacher I need to preview the database activity presets Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "tags" exist: | name | isstandard | | Tag1 | 1 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | And I am on the "Test database name" "data activity" page logged in as teacher1 @javascript @_file_upload Scenario: Preview a user preset as list view template in database Given I follow "Presets" And I click on "Actions" "button" And I choose "Import preset" in the open action menu And I upload "mod/data/tests/fixtures/behat_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" And I follow "Templates" And I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu And I set the field "Name" to "Saved preset by teacher1" And I set the field "Description" to "Behat test preset" And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" When I follow "Presets" And I click on "Saved preset by teacher1" "link" # Check list template preview fields. Then I should see "Saved preset by teacher1" And I should see "List header" in the "template-preview" "region" And I should see "List footer" in the "template-preview" "region" And I should see "List template content" in the "template-preview" "region" And I should see "My text field" in the "template-preview" "region" And I should see "This is a short text" in the "template-preview" "region" And I should see "My multiple selection" in the "template-preview" "region" And I should see "Multi 1" in the "template-preview" "region" And I should see "My date field" in the "template-preview" "region" And I should see "My checkbox field" in the "template-preview" "region" And I should see "Check 2" in the "template-preview" "region" And I should see "My geo field" in the "template-preview" "region" And I should see "41.3912°N 2.1639°E" in the "template-preview" "region" And I should see "My menu field" in the "template-preview" "region" And I should see "Menu 2" in the "template-preview" "region" And I should see "My number field" in the "template-preview" "region" And I should see "1234" in the "template-preview" "region" And I should see "My radio field" in the "template-preview" "region" And I should see "Radio 2" in the "template-preview" "region" And I should see "My text area field" in the "template-preview" "region" And I should see "This is a text area" in the "template-preview" "region" And I should see "My URL field" in the "template-preview" "region" And I should see "https://example.com" in the "template-preview" "region" And I should see "My file field" in the "template-preview" "region" And "Comma-separated values" "icon" should exist in the "template-preview" "region" And I should see "samplefile.csv" in the "template-preview" "region" And I should see "My picture field" in the "template-preview" "region" # Test CSS and JS templates. And I should not see "This content should not be displayed" in the "template-preview" "region" And I should not see "This text should change" in the "template-preview" "region" And I should see "New value" in the "template-preview" "region" @javascript @_file_upload Scenario: Preview a user preset as single view template in database Given I follow "Presets" And I click on "Actions" "button" And I choose "Import preset" in the open action menu And I upload "mod/data/tests/fixtures/behat_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" And I follow "Templates" And I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu And I set the field "Name" to "Saved preset by teacher1" And I set the field "Description" to "Behat test preset" And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" When I follow "Presets" And I click on "Saved preset by teacher1" "link" And I set the field "Templates tertiary navigation" to "Single view template" # Check single view template preview fields. Then I should see "Saved preset by teacher1" And I should see "Single template content" in the "template-preview" "region" And I should see "My text field" in the "template-preview" "region" And I should see "This is a short text" in the "template-preview" "region" And I should see "My multiple selection" in the "template-preview" "region" And I should see "Multi 2" in the "template-preview" "region" And I should see "My date field" in the "template-preview" "region" And I should see "My checkbox field" in the "template-preview" "region" And I should see "Check 2" in the "template-preview" "region" And I should see "My geo field" in the "template-preview" "region" And I should see "41.3912°N 2.1639°E" in the "template-preview" "region" And I should see "My menu field" in the "template-preview" "region" And I should see "Menu 2" in the "template-preview" "region" And I should see "My number field" in the "template-preview" "region" And I should see "1234" in the "template-preview" "region" And I should see "My radio field" in the "template-preview" "region" And I should see "Radio 2" in the "template-preview" "region" And I should see "My text area field" in the "template-preview" "region" And I should see "This is a text area" in the "template-preview" "region" And I should see "My URL field" in the "template-preview" "region" And I should see "https://example.com" in the "template-preview" "region" And I should see "My file field" in the "template-preview" "region" And "Comma-separated values" "icon" should exist in the "template-preview" "region" And I should see "samplefile.csv" in the "template-preview" "region" And I should see "My picture field" in the "template-preview" "region" # Test CSS and JS templates. And I should not see "This content should not be displayed" in the "template-preview" "region" And I should not see "This text should change" in the "template-preview" "region" And I should see "New value" in the "template-preview" "region" @javascript Scenario: Preview a plugin preset in database Given I follow "Presets" When I click on "Journal" "link" Then I should see "Journal" And I should see "This is a short text" And I should see "This is a text area" And I select "Single view template" from the "Templates tertiary navigation" singleselect And I should see "This is a short text" And I should see "This is a text area" And I should see "This is a short text" in the "template-preview" "region" @javascript Scenario: Use back button to return to the presets page in database Given I follow "Presets" And I click on "Image gallery" "link" And I should see "Image gallery" When I click on "Back" "button" Then I should see "Choose a preset to use as a starting point." @javascript Scenario: Apply plugin preset from preview in database Given I follow "Presets" And I click on "Image gallery" "link" When I click on "Use this preset" "button" Then I should see "image" And I should see "title" @javascript @_file_upload Scenario: Apply user preset from preview in database Given I follow "Presets" And I click on "Actions" "button" And I choose "Import preset" in the open action menu And I upload "mod/data/tests/fixtures/behat_preset.zip" file to "Preset file" filemanager When I click on "Import preset and apply" "button" And I follow "Templates" And I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu And I set the field "Name" to "Saved preset by teacher1" And I set the field "Description" to "Behat test preset" And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" When I follow "Presets" And I click on "Saved preset by teacher1" "link" And I click on "Use this preset" "button" Then I should see "Preset applied" And I should see "My URL field" tests/behat/data_presets.feature000064400000047415152265517740013055 0ustar00@mod @mod_data Feature: Users can view and manage data presets In order to use presets As a user I need to view, manage and use presets Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Mountain landscapes | n | C1 | data1 | And the following "mod_data > presets" exist: | database | name | description | user | | data1 | Saved preset 1 | The preset1 has description | admin | | data1 | Saved preset 2 | | admin | | data1 | Saved preset by teacher1 | This preset has also a description | teacher1 | @javascript Scenario: Admins can delete saved presets Given I am on the "Mountain landscapes" "data activity" page logged in as admin When I follow "Presets" Then I should see "Choose a preset to use as a starting point." And I should see "Image gallery" And I should see "Saved preset 1" And I should see "Saved preset 2" And I should see "Saved preset by teacher1" # Plugin presets can't be removed. And the "Delete" item should not exist in the "Actions" action menu of the "Image gallery" "table_row" # The admin should be able to delete saved presets. But the "Delete" item should exist in the "Actions" action menu of the "Saved preset 1" "table_row" And the "Delete" item should exist in the "Actions" action menu of the "Saved preset 2" "table_row" And the "Delete" item should exist in the "Actions" action menu of the "Saved preset by teacher1" "table_row" @javascript Scenario: Teachers can see and use presets Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" Then I should see "Choose a preset to use as a starting point." And I should see "Image gallery" And I should see "Use this preset to collect images." in the "Image gallery" "table_row" And I should see "Saved preset 1" And I should see "The preset1 has description" in the "Saved preset 1" "table_row" And I should see "Saved preset 2" And I should see "Saved preset by teacher1" And I should see "This preset has also a description" in the "Saved preset by teacher1" "table_row" # Plugin presets can't be removed. And the "Delete" item should not exist in the "Actions" action menu of the "Image gallery" "table_row" # Teachers should be able to delete their saved presets. And the "Delete" item should exist in the "Actions" action menu of the "Saved preset by teacher1" "table_row" # Teachers can't delete the presets they haven't created. And the "Delete" item should not exist in the "Actions" action menu of the "Saved preset 1" "table_row" # The "Use this preset" button should be enabled only when a preset is selected. And the "Use this preset" "button" should be disabled And I click on "fullname" "radio" in the "Image gallery" "table_row" And the "Use this preset" "button" should be enabled @javascript Scenario: Only users with the viewalluserpresets capability can see presets created by other users Given the following "permission override" exists: | role | editingteacher | | capability | mod/data:viewalluserpresets | | permission | Prohibit | | contextlevel | System | | reference | | When I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" Then I should see "Image gallery" And I should not see "Saved preset 1" And I should not see "Saved preset 2" But I should see "Saved preset by teacher1" @javascript Scenario: Teachers can save presets Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Templates" When I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu Then I should see "Name" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "Description" in the "Save all fields and templates and publish as preset on this site" "dialogue" And "Replace existing preset with this name and overwrite its contents" "checkbox" should not be visible # Teacher should be able to save preset. And I set the field "Name" to "New saved preset" And I set the field "Description" to "My funny description goes here." And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "Preset saved." And I follow "Presets" And I should see "New saved preset" And I should see "My funny description goes here." in the "New saved preset" "table_row" # Teacher can't overwrite an existing preset that they haven't created. And I follow "Templates" And I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu And I set the field "Name" to "Saved preset 1" And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "A preset with this name already exists. Choose a different name." And "Replace existing preset with this name and overwrite its contents" "checkbox" should not be visible # Teacher can overwrite existing presets created by them, but they are not overwritten if the checkbox is not marked. And I set the field "Name" to "New saved preset" And I set the field "Description" to "This is a new description that shouldn't be saved." And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "A preset with this name already exists." And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible # Confirm the checkbox is still displayed and nothing happens if it's not checked and no change is done in the name. And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "A preset with this name already exists." And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible And I click on "Cancel" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I follow "Presets" And I should see "New saved preset" And I should see "My funny description goes here." in the "New saved preset" "table_row" And I should not see "This is a new description that shouldn't be saved." # But teacher can overwrite existing presets created by them. But I follow "Templates" And I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu And I set the field "Name" to "New saved preset" And I set the field "Description" to "This is a new description that will be overwritten." And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "A preset with this name already exists." And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible And I click on "Replace existing preset with this name and overwrite its contents" "checkbox" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "Preset saved." And I follow "Presets" And I should see "New saved preset" And I should see "This is a new description that will be overwritten." in the "New saved preset" "table_row" And I should not see "My funny description goes here." in the "New saved preset" "table_row" @javascript Scenario: Teachers can edit presets Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" # Plugin presets can't be edited. Then the "Edit" item should not exist in the "Actions" action menu of the "Image gallery" "table_row" # Teachers can't edit the presets they haven't created. And the "Edit" item should not exist in the "Actions" action menu of the "Saved preset 1" "table_row" # Teachers should be able to edit their saved presets. And I open the action menu in "Saved preset by teacher1" "table_row" And I choose "Edit" in the open action menu And I set the field "Name" to "RENAMED preset by teacher1" And I set the field "Description" to "My funny description goes here." And I click on "Save" "button" in the "Edit preset" "dialogue" And I should see "Preset saved." And I should see "RENAMED preset by teacher1" And I should see "My funny description goes here." in the "RENAMED preset by teacher1" "table_row" And I should not see "Saved preset by teacher1" And I should not see "This preset has also a description" @javascript Scenario: Nothing happens when teachers edit a preset and do not change anything Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" And I open the action menu in "Saved preset by teacher1" "table_row" And I choose "Edit" in the open action menu And I click on "Save" "button" in the "Edit preset" "dialogue" Then I should not see "Preset saved." And I should see "Saved preset by teacher1" @javascript Scenario: Teachers can edit presets and overwrite them if they are the authors Given the following "mod_data > preset" exists: | database | data1 | | name | Another preset created by teacher1 | | description | This description will be overwritten | | user | teacher1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" And I open the action menu in "Saved preset by teacher1" "table_row" And I choose "Edit" in the open action menu And I set the field "Name" to "Another preset created by teacher1" And I click on "Save" "button" in the "Edit preset" "dialogue" Then I should see "A preset with this name already exists." And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible # If the checkbox is not selected, the preset shoudn't be saved. And I click on "Save" "button" in the "Edit preset" "dialogue" And I should see "A preset with this name already exists." # But when I select the overwrite checkbox, the preset should be overwritten. But I click on "Replace existing preset with this name and overwrite its contents" "checkbox" in the "Edit preset" "dialogue" And I click on "Save" "button" in the "Edit preset" "dialogue" And I should see "Preset saved." And I should see "Another preset created by teacher1" And I should see "This preset has also a description" in the "Another preset created by teacher1" "table_row" And I should not see "Saved preset by teacher1" And I should not see "This description will be overwritten" @javascript Scenario: Teachers cannot overwrite presets if they are not the authors Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" And I open the action menu in "Saved preset by teacher1" "table_row" And I choose "Edit" in the open action menu And I set the field "Name" to "Saved preset 1" And I click on "Save" "button" in the "Edit preset" "dialogue" Then I should see " A preset with this name already exists. Choose a different name." And "Replace existing preset with this name and overwrite its contents" "checkbox" should not be visible # If the teacher clicks again the Save button, the preset shoudn't be saved. And I click on "Save" "button" in the "Edit preset" "dialogue" And I should see " A preset with this name already exists. Choose a different name." # But if they set a different name (which doesn't exist), the preset should be saved. And I set the field "Name" to "New saved preset" And I click on "Save" "button" in the "Edit preset" "dialogue" And I should see "Preset saved." And I should see "New saved preset" And I should see "This preset has also a description" in the "New saved preset" "table_row" And I should not see "Saved preset by teacher1" @javascript Scenario: Admins can overwrite presets even if they are not the authors Given I am on the "Mountain landscapes" "data activity" page logged in as admin When I follow "Presets" And I open the action menu in "Saved preset by teacher1" "table_row" And I choose "Edit" in the open action menu And I set the field "Name" to "Saved preset 1" And I click on "Save" "button" in the "Edit preset" "dialogue" Then I should see " A preset with this name already exists." And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible # But when admin selects the overwrite checkbox, the preset should be overwritten. But I click on "Replace existing preset with this name and overwrite its contents" "checkbox" in the "Edit preset" "dialogue" And I click on "Save" "button" in the "Edit preset" "dialogue" And I should see "Preset saved." And I should see "Saved preset 1" And I should see "This preset has also a description" in the "Saved preset 1" "table_row" And I should not see "Saved preset by teacher1" And I should not see "The preset1 has description" @javascript Scenario: Teachers can delete their own presets Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And the following "mod_data > presets" exist: | database | name | description | user | | data1 | Saved preset by teacher1 | My funny description goes here. | teacher1 | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" And I should see "Image gallery" And I should see "Saved preset 1" And I should see "Saved preset by teacher1" # Plugin presets can't be removed. And the "Delete" item should not exist in the "Actions" action menu of the "Image gallery" "table_row" # The teacher should not be able to delete presets saved by others. And the "Delete" item should not exist in the "Actions" action menu of the "Saved preset 1" "table_row" # The teacher should be able to delete their own preset. And I open the action menu in "Saved preset by teacher" "table_row" And I choose "Delete" in the open action menu And I click on "Delete" "button" in the "Delete preset Saved preset by teacher1?" "dialogue" And I should see "Preset deleted" And I should not see "Saved preset by teacher1" @javascript Scenario: Teachers can preview a saved preset from the notification Given the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Templates" And I click on "Actions" "button" And I choose "Publish preset on this site" in the open action menu And I set the field "Name" to "New saved preset" And I set the field "Description" to "My funny description goes here." And I click on "Save" "button" in the "Save all fields and templates and publish as preset on this site" "dialogue" And I should see "Preset saved" When I click on "Preview preset" "link" Then I should see "Preview" And I should see "New saved preset" And I should see "Test field name" And I should see "This is a short text" Then "Use this preset" "button" should exist Scenario: Teachers can export any saved preset Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 When I follow "Presets" # Plugin presets can't be exported. And the "Export" item should not exist in the "Actions" action menu of the "Image gallery" "table_row" # The teacher should be able to export any saved preset. And the "Export" item should exist in the "Actions" action menu of the "Saved preset by teacher1" "table_row" And following "Export" in the "Saved preset by teacher1" "table_row" should download a file that: | Contains file in zip | preset.xml | And the "Export" item should exist in the "Actions" action menu of the "Saved preset 1" "table_row" And following "Export" in the "Saved preset 1" "table_row" should download a file that: | Contains file in zip | preset.xml | @javascript @_file_upload Scenario Outline: Admins and Teachers can load a preset from a file Given I am on the "Mountain landscapes" "data activity" page logged in as When I follow "Presets" Then I click on "Actions" "button" And I choose "Import preset" in the open action menu And I upload "mod/data/tests/fixtures/image_gallery_preset.zip" file to "Preset file" filemanager Then I click on "Import preset and apply" "button" in the ".modal-dialog" "css_element" Then I should see "Preset applied" # I am on the field page. And I should see "Manage fields" Then I should see "Preset applied" Examples: | user | | admin | | teacher1 | @javascript Scenario Outline: Teachers can use "Use this preset" actions menu next to each preset. Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I open the action menu in "" "table_row" When I click on "Use this preset" "link" in the "" "table_row" Then I should see "Preset applied" Examples: | Preset Name | | Image gallery | | Saved preset 1 (Admin User) | | Saved preset by teacher1 (Teacher 1) | @javascript Scenario Outline: Teachers can use "Preview" actions menu next to each preset. Given I am on the "Mountain landscapes" "data activity" page logged in as teacher1 And I follow "Presets" And I open the action menu in "" "table_row" When I choose "Preview" in the open action menu Then I should see "Preview of " Examples: | Preset Name | Preset preview name | | Image gallery | Image gallery | | Saved preset 1 (Admin User) | Saved preset 1 | | Saved preset by teacher1 (Teacher 1) | Saved preset by teacher1 | tests/behat/required_entries.feature000064400000031530152265517740013737 0ustar00@mod @mod_data Feature: Users can be required to specify certain fields when adding entries to database activities In order to constrain user input As a teacher I need to specify certain fields as required when I add entries to databases Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | n | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | required | description | param1 | | data1 | text | Base Text input | 1 | Base Text input | | | data1 | checkbox | Required Checkbox | 1 | Base Text input | Required Checkbox Option 1 | | data1 | checkbox | Required Two-Option Checkbox | 1 | Required Two-Option Checkbox | RTOC Option 1\nRTOC Option 2 | | data1 | latlong | Required Coordinates | 1 | Required Coordinates | | | data1 | menu | Required Menu | 1 | Required Menu | Option 1 | | data1 | number | Required Number | 1 | Required Number | | | data1 | radiobutton | Required Radio | 1 | Required Radio | Required Radio Option 1 | | data1 | text | Required Text input | 1 | Required Text input | | | data1 | textarea | Required Text area | 1 | Required Text area | | | data1 | url | Required URL | 1 | Required URL | | | data1 | multimenu | Required Multimenu | 1 | Required Multimenu | Option 1 | | data1 | multimenu | Required Two-Option Multimenu | 1 | Required Two-Option Multimenu | Option 1\nOption 2 | | data1 | checkbox | Not required Checkbox | 0 | Not required Checkbox | Not required Checkbox Option 1 | | data1 | latlong | Not required Coordinates | 0 | Not required Coordinates | | | data1 | menu | Not required Menu | 0 | Not required Menu | Option 1 | | data1 | number | Not required Number | 0 | Not required Number | | | data1 | radiobutton | Not required Radio | 0 | Not required Radio | Not required Radio Option 1 | | data1 | text | Not required Text input | 0 | Not required Text input | | | data1 | textarea | Not required Text area | 0 | Not required Text area | | | data1 | url | Not required URL | 0 | Not required URL | | | data1 | multimenu | Not required Multimenu | 0 | Not required Multimenu | Option 1 | Scenario: Students receive errors for empty required fields but not for optional fields Given I am on the "Test database name" "data activity" page logged in as student1 And I click on "Add entry" "button" And I set the field "Base Text input" to "Some input to allow us to submit the otherwise empty form" When I press "Save" # Then ".alert" "css_element" should exist in the "//div[contains(@id,'defaulttemplate-addentry']//div[position()=1]]" "xpath_element" Then ".alert" "css_element" should appear after "Checkbox" "text" And ".alert" "css_element" should appear before "Required Checkbox" "text" And ".alert" "css_element" should appear after "Two-Option Checkbox" "text" And ".alert" "css_element" should appear before "Required Two-Option Checkbox" "text" And ".alert" "css_element" should appear after "Coordinates" "text" And ".alert" "css_element" should appear before "Required Coordinates" "text" And ".alert" "css_element" should appear after "Menu" "text" And ".alert" "css_element" should appear before "Required Menu" "text" And ".alert" "css_element" should appear after "Radio" "text" And ".alert" "css_element" should appear before "Required Radio" "text" And ".alert" "css_element" should appear after "Text input" "text" And ".alert" "css_element" should appear before "Required Text input" "text" And ".alert" "css_element" should appear after "Text area" "text" And ".alert" "css_element" should appear before "Required Text area" "text" And ".alert" "css_element" should appear after "URL" "text" And ".alert" "css_element" should appear before "Required URL" "text" And ".alert" "css_element" should appear after "Multimenu" "text" And ".alert" "css_element" should appear before "Required Multimenu" "text" And ".alert" "css_element" should appear after "Two-Option Multimenu" "text" And ".alert" "css_element" should appear before "Required Two-Option Multimenu" "text" And I am on "Course 1" course homepage And I follow "Test database name" And I should see "No entries yet" Scenario: Students receive no error for filled in required fields Given I am on the "Test database name" "data activity" page logged in as student1 And I click on "Add entry" "button" And I set the following fields to these values: | Base Text input | Some input to allow us to submit the otherwise empty form | | Required Checkbox Option 1 | 1 | | RTOC Option 1 | 1 | | Latitude | 0 | | Longitude | 0 | | Required Menu | 1 | | Required Number | 1 | | Required Radio Option 1 | 1 | | Required Text input | New entry text | | Required Text area | More text | | Required URL | http://example.com/ | | Required Multimenu | 1 | | Required Two-Option Multimenu | 1 | When I press "Save" And I select "List view" from the "jump" singleselect Then I should not see "No entries in database" And I should see "New entry text" Scenario: Fields refill with data after having an error Given I am on the "Test database name" "data activity" page logged in as student1 And I click on "Add entry" "button" And I set the following fields to these values: | RTOC Option 1 | 1 | | Latitude | 0 | | Longitude | 0 | | Required Menu | 1 | | Required Number | 1 | | Required Radio Option 1 | 1 | | Required Text input | New entry text | | Required Text area | More text | | Required URL | http://example.com/ | | Required Multimenu | 1 | | Required Two-Option Multimenu | 1 | When I press "Save" Then the following fields match these values: | Base Text input | | | Latitude | 0 | | Longitude | 0 | | Required Menu | Option 1 | | Required Number | 1 | | Required Radio Option 1 | 1 | | Required Text input | New entry text | | Required Text area | More text | | Required URL | http://example.com/ | | Required Multimenu | Option 1 | | Required Two-Option Multimenu | Option 1 | @javascript Scenario: A student fills in Latitude but not Longitude will see an error Given I am on the "Test database name" "data activity" page logged in as student1 And I click on "Add entry" "button" And I set the following fields to these values: | Base Text input | Some input to allow us to submit the otherwise empty form | | Required Checkbox Option 1 | 1 | | RTOC Option 1 | 1 | | Latitude | 24 | | Required Menu | 1 | | Required Number | 1 | | Required Radio Option 1 | 1 | | Required Text input | New entry text | | Required Text area | More text | | Required URL | http://example.com/ | | Required Multimenu | 1 | | Required Two-Option Multimenu | 1 | | Latitude | 20 | When I press "Save" Then I should see "Both latitude and longitude are required." Scenario: A student filling in number and text fields with zero will not see an error. Scenario: A student fills in Latitude but not Longitude will see an error Given I am on the "Test database name" "data activity" page logged in as student1 And I click on "Add entry" "button" And I set the following fields to these values: | Base Text input | Some input to allow us to submit the otherwise empty form | | Required Checkbox Option 1 | 1 | | RTOC Option 1 | 1 | | Latitude | 0 | | Longitude | 0 | | Required Menu | 1 | | Required Number | 0 | | Required Radio Option 1 | 1 | | Required Text input | 0 | | Required Text area | 0 | | Required URL | http://example.com/ | | Required Multimenu | 1 | | Required Two-Option Multimenu | 1 | When I press "Save" And I select "List view" from the "jump" singleselect Then I should not see "No entries in database" And I should see "Some input to allow us to submit the otherwise empty form" tests/manager_test.php000064400000073466152265517740011126 0ustar00. namespace mod_data; use context_module; use moodle_url; use core_component; use stdClass; /** * Manager tests class for mod_data. * * @package mod_data * @category test * @copyright 2022 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \mod_data\manager */ final class manager_test extends \advanced_testcase { /** * Test for static create methods. * * @covers ::create_from_instance * @covers ::create_from_coursemodule * @covers ::create_from_data_record */ public function test_create(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $manager = manager::create_from_instance($activity); $manageractivity = $manager->get_instance(); $this->assertEquals($activity->id, $manageractivity->id); $managercm = $manager->get_coursemodule(); $this->assertEquals($cm->id, $managercm->id); $managercontext = $manager->get_context(); $this->assertEquals($context->id, $managercontext->id); $manager = manager::create_from_coursemodule($cm); $manageractivity = $manager->get_instance(); $this->assertEquals($activity->id, $manageractivity->id); $managercm = $manager->get_coursemodule(); $this->assertEquals($cm->id, $managercm->id); $managercontext = $manager->get_context(); $this->assertEquals($context->id, $managercontext->id); $datarecord = (object)[ 'dataid' => $activity->id, 'id' => 0, 'userid' => 0, 'groupid' => 0, 'timecreated' => 0, 'timemodified' => 0, 'approved' => 0, ]; $manager = manager::create_from_data_record($datarecord); $manageractivity = $manager->get_instance(); $this->assertEquals($activity->id, $manageractivity->id); $managercm = $manager->get_coursemodule(); $this->assertEquals($cm->id, $managercm->id); $managercontext = $manager->get_context(); $this->assertEquals($context->id, $managercontext->id); } /** * Test set_module_viewed * @covers ::set_module_viewed */ public function test_set_module_viewed(): void { global $CFG; $CFG->enablecompletion = 1; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $instance = $this->getDataGenerator()->create_module( 'data', ['course' => $course->id], ['completion' => 2, 'completionview' => 1] ); $manager = manager::create_from_instance($instance); $context = $manager->get_context(); $cm = $manager->get_coursemodule(); // Trigger and capture the event. $sink = $this->redirectEvents(); $manager->set_module_viewed($course); $events = $sink->get_events(); // 2 additional events thanks to completion. $this->assertCount(3, $events); $event = array_shift($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_data\event\course_module_viewed', $event); $this->assertEquals($context, $event->get_context()); $moodleurl = new moodle_url('/mod/data/view.php', ['id' => $cm->id]); $this->assertEquals($moodleurl, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); // Check completion status. $completion = new \completion_info($course); $completiondata = $completion->get_data($cm); $this->assertEquals(1, $completiondata->completionstate); } /** * Test set_template_viewed * @covers ::set_template_viewed */ public function test_set_template_viewed(): void { $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module( 'data', ['course' => $course->id] ); $manager = manager::create_from_instance($instance); $context = $manager->get_context(); // Trigger and capture the event. $sink = $this->redirectEvents(); $manager->set_template_viewed(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('mod_data\event\template_viewed', $event); $this->assertEquals($context, $event->get_context()); $moodleurl = new moodle_url('/mod/data/templates.php', ['d' => $instance->id]); $this->assertEquals($moodleurl, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test for has_records(). * * @covers ::has_records */ public function test_has_records(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $data = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($data); // Empty database should return false. $this->assertFalse($manager->has_records()); // Create data record. $datarecords = new \stdClass(); $datarecords->userid = '2'; $datarecords->dataid = $data->id; $datarecords->id = $DB->insert_record('data_records', $datarecords); // Database with records should return true. $this->assertTrue($manager->has_records()); } /** * Test for has_fields(). * * @covers ::has_fields */ public function test_has_fields(): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $manager = manager::create_from_instance($activity); // Empty database should return false. $this->assertFalse($manager->has_fields()); // Add a field to the activity. $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $fieldrecord = new \stdClass(); $fieldrecord->name = 'field1'; $fieldrecord->type = 'text'; $datagenerator->create_field($fieldrecord, $activity); // Database with fields should return true. $this->assertTrue($manager->has_fields()); } /** * Test for get_available_presets(). * * @covers ::get_available_presets */ public function test_get_available_presets(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->setUser($user); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); // Check available presets meet the datapreset plugins when there are no any preset saved by users. $datapresetplugins = core_component::get_plugin_list('datapreset'); $manager = manager::create_from_coursemodule($cm); $presets = $manager->get_available_presets(); $this->assertCount(count($datapresetplugins), $presets); // Confirm that, at least, the "Image gallery" is one of them. $namepresets = array_map(function($preset) { return $preset->name; }, $presets); $this->assertContains('Image gallery', $namepresets); // Login as admin and create some presets saved manually by users. $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $savedpresets = []; for ($i = 1; $i <= 3; $i++) { $preset = (object) [ 'name' => 'Preset name ' . $i, ]; $plugingenerator->create_preset($activity, $preset); $savedpresets[] = $preset; } $savedpresetsnames = array_map(function($preset) { return $preset->name; }, $savedpresets); $this->setUser($user); // Check available presets meet the datapreset plugins + presets saved manually by users. $presets = $manager->get_available_presets(); $this->assertCount(count($datapresetplugins) + count($savedpresets), $presets); // Confirm that, apart from the "Image gallery" preset, the ones created manually have been also returned. $namepresets = array_map(function($preset) { return $preset->name; }, $presets); $this->assertContains('Image gallery', $namepresets); foreach ($savedpresets as $savedpreset) { $this->assertContains($savedpreset->name, $namepresets); } // Check all the presets have the proper value for the isplugin attribute. foreach ($presets as $preset) { if (in_array($preset->name, $savedpresetsnames)) { $this->assertFalse($preset->isplugin); } else { $this->assertTrue($preset->isplugin); } } // Unassign the capability to the teacher role and check that only plugin presets are returned (because the saved presets // have been created by admin). $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); unassign_capability('mod/data:viewalluserpresets', $teacherrole->id); $presets = $manager->get_available_presets(); $this->assertCount(count($datapresetplugins), $presets); // Confirm that, at least, the "Image gallery" is one of them. $namepresets = array_map(function($preset) { return $preset->name; }, $presets); $this->assertContains('Image gallery', $namepresets); foreach ($savedpresets as $savedpreset) { $this->assertNotContains($savedpreset->name, $namepresets); } // Create a preset with the current user and check that, although the viewalluserpresets is not assigned to the teacher // role, the preset is returned because the teacher is the owner. $savedpreset = (object) [ 'name' => 'Preset created by teacher', ]; $plugingenerator->create_preset($activity, $savedpreset); $presets = $manager->get_available_presets(); // The presets total is all the plugin presets plus the preset created by the teacher. $this->assertCount(count($datapresetplugins) + 1, $presets); // Confirm that, at least, the "Image gallery" is one of them. $namepresets = array_map(function($preset) { return $preset->name; }, $presets); $this->assertContains('Image gallery', $namepresets); // Confirm that savedpresets are still not returned. foreach ($savedpresets as $savedpreset) { $this->assertNotContains($savedpreset->name, $namepresets); } // Confirm the new preset created by the teacher is returned too. $this->assertContains('Preset created by teacher', $namepresets); } /** * Test for get_available_plugin_presets(). * * @covers ::get_available_plugin_presets */ public function test_get_available_plugin_presets(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); // Check available plugin presets meet the datapreset plugins. $datapresetplugins = core_component::get_plugin_list('datapreset'); $manager = manager::create_from_instance($activity); $presets = $manager->get_available_plugin_presets(); $this->assertCount(count($datapresetplugins), $presets); // Confirm that, at least, the "Image gallery" is one of them. $namepresets = array_map(function($preset) { return $preset->name; }, $presets); $this->assertContains('Image gallery', $namepresets); // Create a preset saved manually by users. $savedpreset = (object) [ 'name' => 'Preset name 1', ]; $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $plugingenerator->create_preset($activity, $savedpreset); // Check available plugin presets don't contain the preset saved manually. $presets = $manager->get_available_plugin_presets(); $this->assertCount(count($datapresetplugins), $presets); // Confirm that, at least, the "Image gallery" is one of them. $namepresets = array_map(function($preset) { return $preset->name; }, $presets); $this->assertContains('Image gallery', $namepresets); // Confirm that the preset saved manually hasn't been returned. $this->assertNotContains($savedpreset->name, $namepresets); // Check all the presets have the proper value for the isplugin attribute. foreach ($presets as $preset) { $this->assertTrue($preset->isplugin); } } /** * Test for get_available_saved_presets(). * * @covers ::get_available_saved_presets */ public function test_get_available_saved_presets(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $this->setUser($user); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); // Check available saved presets is empty (because, for now, no user preset has been created). $manager = manager::create_from_coursemodule($cm); $presets = $manager->get_available_saved_presets(); $this->assertCount(0, $presets); // Create some presets saved manually by the admin user. $this->setAdminUser(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $savedpresets = []; for ($i = 1; $i <= 3; $i++) { $preset = (object) [ 'name' => 'Preset name ' . $i, ]; $plugingenerator->create_preset($activity, $preset); $savedpresets[] = $preset; } // Create one more preset saved manually by the teacher user. $this->setUser($user); $teacherpreset = (object) [ 'name' => 'Preset created by teacher', ]; $plugingenerator->create_preset($activity, $teacherpreset); $savedpresets[] = $teacherpreset; $savedpresetsnames = array_map(function($preset) { return $preset->name; }, $savedpresets); // Check available saved presets only contain presets saved manually by users. $presets = $manager->get_available_saved_presets(); $this->assertCount(count($savedpresets), $presets); // Confirm that it contains only the presets created manually. foreach ($presets as $preset) { $this->assertContains($preset->name, $savedpresetsnames); $this->assertFalse($preset->isplugin); } // Unassign the mod/data:viewalluserpresets capability to the teacher role and check that saved presets are not returned. $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); unassign_capability('mod/data:viewalluserpresets', $teacherrole->id); $presets = $manager->get_available_saved_presets(); $this->assertCount(1, $presets); $preset = reset($presets); $this->assertEquals($teacherpreset->name, $preset->name); } /** * Test for can_view_preset(). * * @covers ::can_view_preset * @dataProvider can_view_preset_provider * @param string $rolename the user role name * @param bool $ownpreset if the preset belongs to the user * @param bool|null $useridparam if the method should be called with a user id param * @param bool $plugin if the preset is a plugin or not * @param bool $expected the expected result */ public function test_can_view_preset(string $rolename, bool $ownpreset, ?bool $useridparam, bool $plugin, bool $expected): void { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, $rolename); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); $manager = manager::create_from_coursemodule($cm); // Create preset. if ($ownpreset) { $this->setUser($user); } else { $this->setAdminUser(); } if ($plugin) { $preset = preset::create_from_plugin($manager, 'imagegallery'); } else { $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $preset = $plugingenerator->create_preset($activity, (object)['name' => 'Preset name']); } // Setup user param. if ($useridparam) { // Login as a different user to validate the userid param is working. $otheruser = $this->getDataGenerator()->create_user(); $this->setUser($otheruser); $useridparam = $user->id; } else { $this->setUser($user); } $result = $manager->can_view_preset($preset, $useridparam); $this->assertEquals($expected, $result); } /** * Data provider for test_can_view_preset. * * @return array */ public static function can_view_preset_provider(): array { return [ // User presets. 'Teacher owned preset without user id param' => [ 'rolename' => 'editingteacher', 'ownpreset' => true, 'useridparam' => null, 'plugin' => false, 'expected' => true, ], 'Teacher owned preset with user id param' => [ 'rolename' => 'editingteacher', 'ownpreset' => true, 'useridparam' => true, 'plugin' => false, 'expected' => true, ], 'Teacher not owned preset without user id param' => [ 'rolename' => 'editingteacher', 'ownpreset' => false, 'useridparam' => null, 'plugin' => false, 'expected' => true, ], 'Teacher not owned preset with user id param' => [ 'rolename' => 'editingteacher', 'ownpreset' => false, 'useridparam' => true, 'plugin' => false, 'expected' => true, ], 'Student owned preset without user id param' => [ 'rolename' => 'student', 'ownpreset' => true, 'useridparam' => null, 'plugin' => false, 'expected' => true, ], 'Student owned preset with user id param' => [ 'rolename' => 'student', 'ownpreset' => true, 'useridparam' => true, 'plugin' => false, 'expected' => true, ], 'Student not owned preset without user id param' => [ 'rolename' => 'student', 'ownpreset' => false, 'useridparam' => null, 'plugin' => false, 'expected' => false, ], 'Student not owned preset with user id param' => [ 'rolename' => 'student', 'ownpreset' => false, 'useridparam' => true, 'plugin' => false, 'expected' => false, ], // Plugin presets. 'Teacher plugin preset without user id param' => [ 'rolename' => 'editingteacher', 'ownpreset' => false, 'useridparam' => null, 'plugin' => true, 'expected' => true, ], 'Teacher plugin preset with user id param' => [ 'rolename' => 'editingteacher', 'ownpreset' => false, 'useridparam' => true, 'plugin' => true, 'expected' => true, ], 'Student plugin preset without user id param' => [ 'rolename' => 'student', 'ownpreset' => false, 'useridparam' => null, 'plugin' => true, 'expected' => true, ], 'Student plugin preset with user id param' => [ 'rolename' => 'student', 'ownpreset' => false, 'useridparam' => true, 'plugin' => true, 'expected' => true, ], ]; } /** * Test for can_export_entries(). * * @covers ::can_export_entries */ public function test_can_export_entries(): void { global $DB; $this->resetAfterTest(); // Create course with activity and enrol users. $course = $this->getDataGenerator()->create_course(); $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); $manager = manager::create_from_coursemodule($cm); // Add a field. /** @var \mod_data_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_data'); $fieldrecord = (object)[ 'name' => 'myfield', 'type' => 'text', ]; $field = $generator->create_field($fieldrecord, $activity); // Teacher with default capabilities can export entries. $this->setUser($teacher); $result = $manager->can_export_entries(); $this->assertEquals(true, $result); // Teacher without exportallentries can still export entries. unassign_capability('mod/data:exportallentries', $teacherrole->id); $result = $manager->can_export_entries(); $this->assertEquals(true, $result); // Teacher without exportallentries and exportentry can't export entries (unless they have created some entries). unassign_capability('mod/data:exportentry', $teacherrole->id); $result = $manager->can_export_entries(); $this->assertEquals(false, $result); $generator->create_entry( $activity, [$field->field->id => 'Example entry'], ); $result = $manager->can_export_entries(); $this->assertEquals(true, $result); // Student without entries can't export. $this->setUser($student); $result = $manager->can_export_entries(); $this->assertEquals(false, $result); // However, student who has created any entry, can export. $generator->create_entry( $activity, [$field->field->id => 'Another example entry'], ); $this->setUser($student); $result = $manager->can_export_entries(); $this->assertEquals(true, $result); } /* * Test reset_all_templates. * * @covers ::reset_all_templates */ public function test_reset_all_templates(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module( 'data', ['course' => $course->id] ); $manager = manager::create_from_instance($instance); // Create some initial templates. $initialtemplates = new stdClass(); foreach (manager::TEMPLATES_LIST as $templatename => $unused) { $initialtemplates->$templatename = "Initial $templatename"; } $manager->update_templates($initialtemplates); $instance = $manager->get_instance(); $record = $DB->get_record('data', ['id' => $instance->id]); foreach (manager::TEMPLATES_LIST as $templatename => $unused) { $this->assertEquals($initialtemplates->$templatename, $instance->$templatename); $this->assertEquals($initialtemplates->$templatename, $record->$templatename); } // Reset all templates. $result = $manager->reset_all_templates(); $this->assertTrue($result); $instance = $manager->get_instance(); $record = $DB->get_record('data', ['id' => $instance->id]); foreach (manager::TEMPLATES_LIST as $templatename => $unused) { $this->assertEquals('', $instance->$templatename); $this->assertEquals('', $record->$templatename); } } /** * Test reset_template. * * @covers ::reset_template * @dataProvider reset_template_provider * @param string $templatetoreset the template to reset * @param string[] $expected the expected templates to be reset */ public function test_reset_template(string $templatetoreset, array $expected): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup test data. $course = $this->getDataGenerator()->create_course(); $instance = $this->getDataGenerator()->create_module( 'data', ['course' => $course->id] ); $manager = manager::create_from_instance($instance); // Create some initial templates. $initialtemplates = new stdClass(); foreach (manager::TEMPLATES_LIST as $templatename => $unused) { $initialtemplates->$templatename = "Initial $templatename"; } $manager->update_templates($initialtemplates); $instance = $manager->get_instance(); $record = $DB->get_record('data', ['id' => $instance->id]); foreach (manager::TEMPLATES_LIST as $templatename => $unused) { $this->assertEquals($initialtemplates->$templatename, $instance->$templatename); $this->assertEquals($initialtemplates->$templatename, $record->$templatename); } // Reset template. $result = $manager->reset_template($templatetoreset); $this->assertTrue($result); $instance = $manager->get_instance(); $record = $DB->get_record('data', ['id' => $instance->id]); foreach (manager::TEMPLATES_LIST as $templatename => $unused) { if (in_array($templatename, $expected)) { $this->assertEquals('', $instance->$templatename); $this->assertEquals('', $record->$templatename); } else { $this->assertEquals($initialtemplates->$templatename, $instance->$templatename); $this->assertEquals($initialtemplates->$templatename, $record->$templatename); } } } /** * Data provider for test_reset_templatet. * * @return array */ public static function reset_template_provider(): array { return [ // User presets. 'listtemplate' => [ 'templatetoreset' => 'listtemplate', 'expected' => ['listtemplate', 'listtemplateheader', 'listtemplatefooter'], ], 'singletemplate' => [ 'templatetoreset' => 'singletemplate', 'expected' => ['singletemplate'], ], 'asearchtemplate' => [ 'templatetoreset' => 'asearchtemplate', 'expected' => ['asearchtemplate'], ], 'addtemplate' => [ 'templatetoreset' => 'addtemplate', 'expected' => ['addtemplate'], ], 'rsstemplate' => [ 'templatetoreset' => 'rsstemplate', 'expected' => ['rsstemplate', 'rsstitletemplate'], ], 'csstemplate' => [ 'templatetoreset' => 'csstemplate', 'expected' => ['csstemplate'], ], 'jstemplate' => [ 'templatetoreset' => 'jstemplate', 'expected' => ['jstemplate'], ], ]; } } import_form.php000064400000003073152265517740007633 0ustar00libdir.'/formslib.php'); require_once($CFG->libdir.'/csvlib.class.php'); class mod_data_import_form extends moodleform { function definition() { $mform =& $this->_form; $dataid = $this->_customdata['dataid']; $backtourl = $this->_customdata['backtourl']; $mform->addElement('filepicker', 'recordsfile', get_string('csvfile', 'data'), null, ['accepted_types' => ['application/zip', 'text/csv']]); $delimiters = csv_import_reader::get_delimiter_list(); $mform->addElement('select', 'fielddelimiter', get_string('fielddelimiter', 'data'), $delimiters); $mform->setDefault('fielddelimiter', 'comma'); $mform->addElement('text', 'fieldenclosure', get_string('fieldenclosure', 'data')); $mform->setType('fieldenclosure', PARAM_CLEANHTML); $choices = core_text::get_encodings(); $mform->addElement('select', 'encoding', get_string('fileencoding', 'mod_data'), $choices); $mform->setDefault('encoding', 'UTF-8'); // Database activity ID. $mform->addElement('hidden', 'd'); $mform->setType('d', PARAM_INT); $mform->setDefault('d', $dataid); // Back to URL. $mform->addElement('hidden', 'backto'); $mform->setType('backto', PARAM_LOCALURL); $mform->setDefault('backto', $backtourl); $this->add_action_buttons(true, get_string('submit')); } } deprecatedlib.php000064400000031234152265517740010065 0ustar00. /** * List of deprecated mod_data functions. * * @package mod_data * @copyright 2021 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * @deprecated since Moodle 4.1 MDL-75146 - please do not use this function any more. */ #[\core\attribute\deprecated( 'mod_data\manager::get_template and mod_data\template::parse_entries', since: '4.1', mdl: 'MDL-75146', final: true, )] function data_print_template(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75148 - please, use the preset::get_name_from_plugin() function instead. */ #[\core\attribute\deprecated('mod_data\preset::get_name_from_plugin()', since: '4.1', mdl: 'MDL-75148', final: true)] function data_preset_name(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75148 - please, use the manager::get_available_presets() function instead. */ #[\core\attribute\deprecated('mod_data\manager::get_available_presets()', since: '4.1', mdl: 'MDL-75148', final: true)] function data_get_available_presets(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75148 - please, use the manager::get_available_saved_presets() function instead. */ #[\core\attribute\deprecated('mod_data\manager::get_available_saved_presets()', since: '4.1', mdl: 'MDL-75148', final: true)] function data_get_available_site_presets(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75187 - please, use the preset::delete() function instead. */ #[\core\attribute\deprecated('mod_data\preset::delete()', since: '4.1', mdl: 'MDL-75187', final: true)] function data_delete_site_preset(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75142 - please, use the preset::save() function instead. */ #[\core\attribute\deprecated('mod_data\preset::save()', since: '4.1', mdl: 'MDL-75142', final: true)] function data_presets_save(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75142 - please, use the protected preset::generate_preset_xml() function instead. */ #[\core\attribute\deprecated('mod_data\preset::generate_preset_xml()', since: '4.1', mdl: 'MDL-75142', final: true)] function data_presets_generate_xml(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75142 - please, use the preset::export() function instead. */ #[\core\attribute\deprecated('mod_data\preset::export()', since: '4.1', mdl: 'MDL-75142', final: true)] function data_presets_export(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75187 - please, use the preset::can_manage() function instead. */ #[\core\attribute\deprecated('mod_data\preset::can_manage()', since: '4.1', mdl: 'MDL-75187', final: true)] function data_user_can_delete_preset(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75146 - please do not use this function any more. */ #[\core\attribute\deprecated('mod_data\manager::set_module_viewed', since: '4.1', mdl: 'MDL-75146', final: true)] function data_view(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.1 MDL-75148 - please, use the preset::is_directory_a_preset() function instead. */ #[\core\attribute\deprecated('mod_data\preset::is_directory_a_preset()', since: '4.1', mdl: 'MDL-75148', final: true)] function is_directory_a_preset(): void { \core\deprecation::emit_deprecation(__FUNCTION__); } /** * @deprecated since Moodle 4.3. * @global object * @param array $export * @param string $dataname * @param int $count * @return string */ function data_export_xls($export, $dataname, $count) { global $CFG; debugging('Function data_export_xls() has been deprecated, because xls export has been dropped.', DEBUG_DEVELOPER); require_once("$CFG->libdir/excellib.class.php"); $filename = clean_filename("{$dataname}-{$count}_record"); if ($count > 1) { $filename .= 's'; } $filename .= clean_filename('-' . gmdate("Ymd_Hi")); $filename .= '.xls'; $filearg = '-'; $workbook = new MoodleExcelWorkbook($filearg); $workbook->send($filename); $worksheet = array(); $worksheet[0] = $workbook->add_worksheet(''); $rowno = 0; foreach ($export as $row) { $colno = 0; foreach($row as $col) { $worksheet[0]->write($rowno, $colno, $col); $colno++; } $rowno++; } $workbook->close(); return $filename; } /** * @deprecated since Moodle 4.3, exporting is now being done by \mod_data\local\exporter\csv_entries_exporter * @global object * @param array $export * @param string $delimiter_name * @param object $database * @param int $count * @param bool $return * @return string|void */ function data_export_csv($export, $delimiter_name, $database, $count, $return=false) { global $CFG; debugging('Function data_export_csv has been deprecated. Exporting is now being done by ' . '\mod_data\local\csv_exporter.', DEBUG_DEVELOPER); require_once($CFG->libdir . '/csvlib.class.php'); $filename = $database . '-' . $count . '-record'; if ($count > 1) { $filename .= 's'; } if ($return) { return csv_export_writer::print_array($export, $delimiter_name, '"', true); } else { csv_export_writer::download_array($filename, $export, $delimiter_name); } } /** * @deprecated since Moodle 4.3, exporting is now being done by \mod_data\local\exporter\ods_entries_exporter * @global object * @param array $export * @param string $dataname * @param int $count * @param string */ function data_export_ods($export, $dataname, $count) { global $CFG; debugging('Function data_export_ods has been deprecated. Exporting is now being done by ' . '\mod_data\local\ods_exporter.', DEBUG_DEVELOPER); require_once("$CFG->libdir/odslib.class.php"); $filename = clean_filename("{$dataname}-{$count}_record"); if ($count > 1) { $filename .= 's'; } $filename .= clean_filename('-' . gmdate("Ymd_Hi")); $filename .= '.ods'; $filearg = '-'; $workbook = new MoodleODSWorkbook($filearg); $workbook->send($filename); $worksheet = array(); $worksheet[0] = $workbook->add_worksheet(''); $rowno = 0; foreach ($export as $row) { $colno = 0; foreach($row as $col) { $worksheet[0]->write($rowno, $colno, $col); $colno++; } $rowno++; } $workbook->close(); return $filename; } /** * @deprecated since Moodle 4.3, use \mod_data\local\exporter\utils::data_exportdata with a \mod_data\local\exporter\entries_exporter object * @global object * @param int $dataid * @param array $fields * @param array $selectedfields * @param int $currentgroup group ID of the current group. This is used for * exporting data while maintaining group divisions. * @param object $context the context in which the operation is performed (for capability checks) * @param bool $userdetails whether to include the details of the record author * @param bool $time whether to include time created/modified * @param bool $approval whether to include approval status * @param bool $tags whether to include tags * @return array */ function data_get_exportdata($dataid, $fields, $selectedfields, $currentgroup=0, $context=null, $userdetails=false, $time=false, $approval=false, $tags = false) { global $DB; debugging('Function data_get_exportdata has been deprecated. Use ' . '\mod_data\local\exporter_utils::data_exportdata with a \mod_data\local\exporter object instead', DEBUG_DEVELOPER); if (is_null($context)) { $context = context_system::instance(); } // exporting user data needs special permission $userdetails = $userdetails && has_capability('mod/data:exportuserinfo', $context); $exportdata = array(); // populate the header in first row of export foreach($fields as $key => $field) { if (!in_array($field->field->id, $selectedfields)) { // ignore values we aren't exporting unset($fields[$key]); } else { $exportdata[0][] = $field->field->name; } } if ($tags) { $exportdata[0][] = get_string('tags', 'data'); } if ($userdetails) { $exportdata[0][] = get_string('user'); $exportdata[0][] = get_string('username'); $exportdata[0][] = get_string('email'); } if ($time) { $exportdata[0][] = get_string('timeadded', 'data'); $exportdata[0][] = get_string('timemodified', 'data'); } if ($approval) { $exportdata[0][] = get_string('approved', 'data'); } $datarecords = $DB->get_records('data_records', array('dataid'=>$dataid)); ksort($datarecords); $line = 1; foreach($datarecords as $record) { // get content indexed by fieldid if ($currentgroup) { $select = 'SELECT c.fieldid, c.content, c.content1, c.content2, c.content3, c.content4 FROM {data_content} c, {data_records} r WHERE c.recordid = ? AND r.id = c.recordid AND r.groupid = ?'; $where = array($record->id, $currentgroup); } else { $select = 'SELECT fieldid, content, content1, content2, content3, content4 FROM {data_content} WHERE recordid = ?'; $where = array($record->id); } if( $content = $DB->get_records_sql($select, $where) ) { foreach($fields as $field) { $contents = ''; if(isset($content[$field->field->id])) { $contents = $field->export_text_value($content[$field->field->id]); } $exportdata[$line][] = $contents; } if ($tags) { $itemtags = \core_tag_tag::get_item_tags_array('mod_data', 'data_records', $record->id); $exportdata[$line][] = implode(', ', $itemtags); } if ($userdetails) { // Add user details to the export data $userdata = get_complete_user_data('id', $record->userid); $exportdata[$line][] = fullname($userdata); $exportdata[$line][] = $userdata->username; $exportdata[$line][] = $userdata->email; } if ($time) { // Add time added / modified $exportdata[$line][] = userdate($record->timecreated); $exportdata[$line][] = userdate($record->timemodified); } if ($approval) { // Add approval status $exportdata[$line][] = (int) $record->approved; } } $line++; } $line--; return $exportdata; } /** * @deprecated since Moodle 4.3, importing is now being done by \mod_data\local\importer\csv_importer::import_csv * Import records for a data instance from csv data. * * @param object $cm Course module of the data instance. * @param object $data The data instance. * @param string $csvdata The csv data to be imported. * @param string $encoding The encoding of csv data. * @param string $fielddelimiter The delimiter of the csv data. * @return int Number of records added. */ function data_import_csv($cm, $data, &$csvdata, $encoding, $fielddelimiter) { debugging('Function data_import_csv has been deprecated. ' . 'Importing is now being done by \mod_data\local\csv_importer::import_csv.', DEBUG_DEVELOPER); // New function needs a file, not the file content, so we have to temporarily put the content into a file. $tmpdir = make_request_directory(); $tmpfilename = 'tmpfile.csv'; $tmpfilepath = $tmpdir . '/tmpfile.csv'; file_put_contents($tmpfilepath, $csvdata); $importer = new \mod_data\local\importer\csv_entries_importer($tmpfilepath, $tmpfilename); $importer->import_csv($cm, $data, $encoding, $fielddelimiter); return 0; } export_form.php000064400000013236152265517740007644 0ustar00libdir . '/formslib.php'); require_once($CFG->libdir . '/csvlib.class.php'); class mod_data_export_form extends moodleform { var $_datafields = array(); var $_cm; var $_data; // @param string $url: the url to post to // @param array $datafields: objects in this database public function __construct($url, $datafields, $cm, $data) { $this->_datafields = $datafields; $this->_cm = $cm; $this->_data = $data; parent::__construct($url); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function mod_data_export_form($url, $datafields, $cm, $data) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($url, $datafields, $cm, $data); } function definition() { $mform =& $this->_form; $mform->addElement('header', 'exportformat', get_string('chooseexportformat', 'data')); $optionattrs = ['class' => 'mt-1 mb-1']; // Export format type radio group. $typesarray = array(); $typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('csvwithselecteddelimiter', 'data'), 'csv', $optionattrs); // Temporarily commenting out Excel export option. See MDL-19864. //$typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('excel', 'data'), 'xls'); $typesarray[] = $mform->createElement('radio', 'exporttype', null, get_string('ods', 'data'), 'ods', $optionattrs); $mform->addGroup($typesarray, 'exportar', get_string('exportformat', 'data'), null, false); $mform->addRule('exportar', null, 'required'); $mform->setDefault('exporttype', 'csv'); // CSV delimiter list. $choices = csv_import_reader::get_delimiter_list(); $key = array_search(';', $choices); if ($key !== false) { // Array $choices contains the semicolon -> drop it (because its encrypted form also contains a semicolon): unset($choices[$key]); } $mform->addElement('select', 'delimiter_name', get_string('fielddelimiter', 'data'), $choices); $mform->hideIf('delimiter_name', 'exporttype', 'neq', 'csv'); if (array_key_exists('cfg', $choices)) { $mform->setDefault('delimiter_name', 'cfg'); } else if (get_string('listsep', 'langconfig') == ';') { $mform->setDefault('delimiter_name', 'semicolon'); } else { $mform->setDefault('delimiter_name', 'comma'); } // Fields to be exported. $mform->addElement('header', 'exportfieldsheader', get_string('chooseexportfields', 'data')); $mform->setExpanded('exportfieldsheader'); $numfieldsthatcanbeselected = 0; $exportfields = []; $unsupportedfields = []; foreach ($this->_datafields as $field) { $label = get_string('fieldnametype', 'data', (object)['name' => s($field->field->name), 'type' => $field->name()]); if ($field->text_export_supported()) { $numfieldsthatcanbeselected++; $exportfields[] = $mform->createElement('advcheckbox', 'field_' . $field->field->id, '', $label, array_merge(['group' => 1], $optionattrs)); $mform->setDefault('field_' . $field->field->id, 1); } else { $unsupportedfields[] = $label; } } $mform->addGroup($exportfields, 'exportfields', get_string('selectfields', 'data'), ['
'], false); if ($numfieldsthatcanbeselected > 1) { $this->add_checkbox_controller(1, null, null, 1); } // List fields that cannot be exported. if (!empty($unsupportedfields)) { $unsupportedfieldslist = html_writer::tag('p', get_string('unsupportedfieldslist', 'data'), ['class' => 'mt-1']); $unsupportedfieldslist .= html_writer::alist($unsupportedfields); $mform->addElement('static', 'unsupportedfields', get_string('unsupportedfields', 'data'), $unsupportedfieldslist); } // Export options. $mform->addElement('header', 'exportoptionsheader', get_string('exportoptions', 'data')); $mform->setExpanded('exportoptionsheader'); $exportoptions = []; if (core_tag_tag::is_enabled('mod_data', 'data_records')) { $exportoptions[] = $mform->createElement('checkbox', 'exporttags', get_string('includetags', 'data'), '', $optionattrs); $mform->setDefault('exporttags', 1); } $context = context_module::instance($this->_cm->id); if (has_capability('mod/data:exportuserinfo', $context)) { $exportoptions[] = $mform->createElement('checkbox', 'exportuser', get_string('includeuserdetails', 'data'), '', $optionattrs); } $exportoptions[] = $mform->createElement('checkbox', 'exporttime', get_string('includetime', 'data'), '', $optionattrs); if ($this->_data->approval) { $exportoptions[] = $mform->createElement('checkbox', 'exportapproval', get_string('includeapproval', 'data'), '', $optionattrs); } $exportoptions[] = $mform->createElement('checkbox', 'includefiles', get_string('includefiles', 'data'), '', $optionattrs); $mform->setDefault('includefiles', 1); $mform->addGroup($exportoptions, 'exportoptions', get_string('selectexportoptions', 'data'), ['
'], false); $this->add_action_buttons(true, get_string('exportentries', 'data')); } } settings.php000064400000001136152265517740007134 0ustar00fulltree) { if (empty($CFG->enablerssfeeds)) { $options = array(0 => get_string('rssglobaldisabled', 'admin')); $str = get_string('configenablerssfeeds', 'data').'
'.get_string('configenablerssfeedsdisabled2', 'admin'); } else { $options = array(0=>get_string('no'), 1=>get_string('yes')); $str = get_string('configenablerssfeeds', 'data'); } $settings->add(new admin_setting_configselect('data_enablerssfeeds', get_string('enablerssfeeds', 'admin'), $str, 0, $options)); } mod_form.php000064400000021307152265517740007100 0ustar00dirroot.'/course/moodleform_mod.php'); class mod_data_mod_form extends moodleform_mod { function definition() { global $CFG, $DB, $OUTPUT; $mform =& $this->_form; //------------------------------------------------------------------------------- $mform->addElement('header', 'general', get_string('general', 'form')); $mform->addElement('text', 'name', get_string('name'), array('size'=>'64')); if (!empty($CFG->formatstringstriptags)) { $mform->setType('name', PARAM_TEXT); } else { $mform->setType('name', PARAM_CLEANHTML); } $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->standard_intro_elements(get_string('intro', 'data')); // ---------------------------------------------------------------------- $mform->addElement('header', 'entrieshdr', get_string('entries', 'data')); $mform->addElement('selectyesno', 'approval', get_string('requireapproval', 'data')); $mform->addHelpButton('approval', 'requireapproval', 'data'); $mform->addElement('selectyesno', 'manageapproved', get_string('manageapproved', 'data')); $mform->addHelpButton('manageapproved', 'manageapproved', 'data'); $mform->setDefault('manageapproved', 1); $mform->hideIf('manageapproved', 'approval', 'eq', 0); $mform->addElement('selectyesno', 'comments', get_string('allowcomments', 'data')); if (empty($CFG->usecomments)) { $mform->hardFreeze('comments'); $mform->setConstant('comments', 0); } $countoptions = array(0=>get_string('none'))+ (array_combine(range(1, DATA_MAX_ENTRIES), // Keys. range(1, DATA_MAX_ENTRIES))); // Values. /*only show fields if there are legacy values from *before completionentries was added*/ if (!empty($this->current->requiredentries)) { $group = array(); $group[] = $mform->createElement('select', 'requiredentries', get_string('requiredentries', 'data'), $countoptions); $mform->addGroup($group, 'requiredentriesgroup', get_string('requiredentries', 'data'), array(''), false); $mform->addHelpButton('requiredentriesgroup', 'requiredentries', 'data'); $mform->addElement('html', $OUTPUT->notification( get_string('requiredentrieswarning', 'data'))); } $mform->addElement('select', 'requiredentriestoview', get_string('requiredentriestoview', 'data'), $countoptions); $mform->addHelpButton('requiredentriestoview', 'requiredentriestoview', 'data'); $mform->addElement('select', 'maxentries', get_string('maxentries', 'data'), $countoptions); $mform->addHelpButton('maxentries', 'maxentries', 'data'); // ---------------------------------------------------------------------- $mform->addElement('header', 'availibilityhdr', get_string('availability')); $mform->addElement('date_time_selector', 'timeavailablefrom', get_string('availablefromdate', 'data'), array('optional' => true)); $mform->addElement('date_time_selector', 'timeavailableto', get_string('availabletodate', 'data'), array('optional' => true)); $mform->addElement('date_time_selector', 'timeviewfrom', get_string('viewfromdate', 'data'), array('optional' => true)); $mform->addElement('date_time_selector', 'timeviewto', get_string('viewtodate', 'data'), array('optional' => true)); // ---------------------------------------------------------------------- if ($CFG->enablerssfeeds && $CFG->data_enablerssfeeds) { $mform->addElement('header', 'rsshdr', get_string('rss')); $mform->addElement('select', 'rssarticles', get_string('numberrssarticles', 'data') , $countoptions); } $this->standard_grading_coursemodule_elements(); $this->standard_coursemodule_elements(); //------------------------------------------------------------------------------- // buttons $this->add_action_buttons(); } /** * Enforce validation rules here * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array **/ public function validation($data, $files) { $errors = parent::validation($data, $files); // Check open and close times are consistent. if ($data['timeavailablefrom'] && $data['timeavailableto'] && $data['timeavailableto'] < $data['timeavailablefrom']) { $errors['timeavailableto'] = get_string('availabletodatevalidation', 'data'); } if ($data['timeviewfrom'] && $data['timeviewto'] && $data['timeviewto'] < $data['timeviewfrom']) { $errors['timeviewto'] = get_string('viewtodatevalidation', 'data'); } return $errors; } /** * Display module-specific activity completion rules. * Part of the API defined by moodleform_mod * @return array Array of string IDs of added items, empty array if none */ public function add_completion_rules() { $mform = & $this->_form; $group = []; $suffix = $this->get_suffix(); $completionentriesenabledel = 'completionentriesenabled' . $suffix; $group[] = $mform->createElement( 'checkbox', $completionentriesenabledel, '', get_string('completionentriescount', 'data') ); $completionentriesel = 'completionentries' . $suffix; $group[] = $mform->createElement( 'text', $completionentriesel, get_string('completionentriescount', 'data'), ['size' => '1'] ); $completionentriesgroupel = 'completionentriesgroup' . $suffix; $mform->addGroup( $group, $completionentriesgroupel, '', [' '], false ); $mform->hideIf($completionentriesel, $completionentriesenabledel, 'notchecked'); $mform->setDefault($completionentriesel, 1); $mform->setType($completionentriesel, PARAM_INT); /* This ensures the elements are disabled unless completion rules are enabled */ return [$completionentriesgroupel]; } /** * Called during validation. Indicates if a module-specific completion rule is selected. * * @param array $data * @return bool True if one or more rules is enabled, false if none are. */ public function completion_rule_enabled($data) { $suffix = $this->get_suffix(); return (!empty($data['completionentriesenabled' . $suffix]) && $data['completionentries' . $suffix] != 0); } /** * Set up the completion checkbox which is not part of standard data. * * @param array $defaultvalues * */ public function data_preprocessing(&$defaultvalues) { parent::data_preprocessing($defaultvalues); $suffix = $this->get_suffix(); $completionentriesenabledel = 'completionentriesenabled' . $suffix; $completionentriesel = 'completionentries' . $suffix; $defaultvalues[$completionentriesenabledel] = !empty($defaultvalues[$completionentriesel]) ? 1 : 0; if (empty($defaultvalues[$completionentriesel])) { $defaultvalues[$completionentriesel] = 1; } } /** * Allows modules to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data the form data to be modified. */ public function data_postprocessing($data) { parent::data_postprocessing($data); if (!empty($data->completionunlocked)) { $suffix = $this->get_suffix(); $completionel = 'completion' . $suffix; $completionentriesenabledel = 'completionentriesenabled' . $suffix; $autocompletion = !empty($data->{$completionel}) && $data->{$completionel} == COMPLETION_TRACKING_AUTOMATIC; if (empty($data->{$completionentriesenabledel}) || !$autocompletion) { $completionentriesel = 'completionentries' . $suffix; $data->{$completionentriesel} = 0; } } } } db/install.php000064400000002015152266364370007324 0ustar00. /** * Database activity filter post install hook * * @package filter * @subpackage data * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); function xmldb_filter_data_install() { global $DB; } db/renamedclasses.php000064400000001753152266364370010657 0ustar00. /** * Renamed classes for the filter_data plugin. * * @package filter_data * @copyright Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $renamedclasses = [ 'filter_data' => \filter_data\text_filter::class, ]; lang/en/filter_data.php000064400000002125152266364370011074 0ustar00. /** * Strings for filter_data * * @package filter * @subpackage data * @copyright 2011 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['filtername'] = 'Database auto-linking'; $string['privacy:metadata'] = 'The Database auto-linking plugin does not store any personal data.'; filter.php000064400000002260152266364370006560 0ustar00. /** * File only retained to prevent fatal errors in code that tries to require/include this. * * @todo MDL-82708 delete this file as part of Moodle 6.0 development. * @deprecated This file is no longer required in Moodle 4.5+. * @package filter_data * @copyright Vy-Shane Sin Fat * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); debugging('This file is no longer required in Moodle 4.5+. Please do not include/require it.', DEBUG_DEVELOPER); classes/text_filter.php000064400000013377152266364370011274 0ustar00. namespace filter_data; use core_filters\filter_object; /** * Filter providing automatic linking to database activity entries when found inside every Moodle text. * * @package filter_data * @copyright 2006 Vy-Shane Sin Fat * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class text_filter extends \core_filters\text_filter { #[\Override] public function filter($text, array $options = []) { global $CFG, $DB, $USER; // Trivial-cache - keyed on $cachedcourseid + $cacheduserid. static $cachedcourseid = null; static $cacheduserid = null; static $coursecontentlist = []; static $sitecontentlist = []; static $nothingtodo; // Try to get current course. // We could be in a course category so no entries for courseid == 0 will be found. $courseid = 0; if ($coursectx = $this->context->get_course_context(false)) { $courseid = $coursectx->instanceid; } if ($cacheduserid !== $USER->id) { // Invalidate all caches if the user changed. $coursecontentlist = []; $sitecontentlist = []; $cacheduserid = $USER->id; $cachedcourseid = $courseid; $nothingtodo = false; } else if ($courseid != get_site()->id && $courseid != 0 && $cachedcourseid != $courseid) { // Invalidate course-level caches if the course id changed. $coursecontentlist = []; $cachedcourseid = $courseid; $nothingtodo = false; } if ($nothingtodo === true) { return $text; } // If courseid == 0 only site entries will be returned. $site = get_site(); if ($courseid == get_site()->id || $courseid == 0) { $contentlist = & $sitecontentlist; } else { $contentlist = & $coursecontentlist; } // Create a list of all the resources to search for. It may be cached already. if (empty($contentlist)) { $coursestosearch = $courseid ? [$courseid] : []; // Add courseid if found. if (get_site()->id != $courseid) { // Add siteid if was not courseid. $coursestosearch[] = get_site()->id; } // We look for text field contents only if have autolink enabled (param1). [$coursesql, $params] = $DB->get_in_or_equal($coursestosearch); $sql = 'SELECT dc.id AS contentid, dr.id AS recordid, dc.content AS content, d.id AS dataid FROM {data} d JOIN {data_fields} df ON df.dataid = d.id JOIN {data_records} dr ON dr.dataid = d.id JOIN {data_content} dc ON dc.fieldid = df.id AND dc.recordid = dr.id WHERE d.course ' . $coursesql . ' AND df.type = \'text\' AND ' . $DB->sql_compare_text('df.param1', 1) . " = '1'"; if (!$contents = $DB->get_records_sql($sql, $params)) { $nothingtodo = true; return $text; } foreach ($contents as $key => $content) { // Trim empty or unlinkable concepts. $currentcontent = trim(strip_tags($content->content)); if (empty($currentcontent)) { unset($contents[$key]); continue; } else { $contents[$key]->content = $currentcontent; } // Rule out any small integers. See bug 1446. $currentint = intval($currentcontent); if ($currentint && (strval($currentint) == $currentcontent) && $currentint < 1000) { unset($contents[$key]); } } if (empty($contents)) { $nothingtodo = true; return $text; } usort($contents, [self::class, 'sort_entries_by_length']); foreach ($contents as $content) { $hrefopen = ''; $contentlist[] = new filter_object($content->content, $hrefopen, '', false, true); } $contentlist = filter_remove_duplicates($contentlist); // Clean dupes. } return filter_phrases($text, $contentlist); // Look for all these links in the text. } /** * Helper to sort array values by content length. * * @param mixed $content0 * @param mixed $content1 * @return int */ private static function sort_entries_by_length($content0, $content1) { $len0 = strlen($content0->content); $len1 = strlen($content1->content); if ($len0 < $len1) { return 1; } else if ($len0 > $len1) { return -1; } else { return 0; } } } tests/behat/view_entries_with_filter.feature000064400000003356152266364370015476 0ustar00@filter_data @mod_data Feature: When using data filter the entry will be linked based on autolink. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the "data" filter is "on" And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | Database intro | C1 | data1 | Scenario Outline: Text entries are linked automatically based if autolink is enabled or not. # Param1 refers to `Autolink`. Given the following "mod_data > fields" exist: | database | type | name | description | param1 | | data1 | text | Test field name | Test field description | 0 | | data1 | text | Test field ref | Test field ref description | | And the following "mod_data > entries" exist: | database | user | Test field name | Test field ref | | data1 | teacher1 | Linked entry to Data2 entry | Data1 entry | | data1 | teacher1 | Linked entry to Data1 entry | Data2 entry | When I am on the "Test database name" "data activity" page logged in as teacher1 Then "Data1 entry" "link" exist Then "Data2 entry" "link" exist Examples: | param1 | autolink | | 0 | should not | | 1 | should | tests/text_filter_test.php000064400000010371152266364370012027 0ustar00. /** * Unit tests. * * @package filter_data * @category test * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace filter_data; /** * Tests for filter_data. * * @package filter_data * @copyright 2015 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \filter_data\text_filter */ final class text_filter_test extends \advanced_testcase { /** * Tests that the filter applies the required changes. */ public function test_filter(): void { $this->resetAfterTest(true); $this->setAdminUser(); \filter_manager::reset_caches(); filter_set_global_state('data', TEXTFILTER_ON); $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); $course2 = $this->getDataGenerator()->create_course(); $coursecontext2 = \context_course::instance($course2->id); $sitecontext = \context_course::instance(SITEID); $site = get_site(); $this->add_simple_database_instance($site, array('SiteEntry')); $this->add_simple_database_instance($course1, array('CourseEntry')); $html = '

I like CourseEntry and SiteEntry

'; // Testing at course level (both site and course). $filtered = format_text($html, FORMAT_HTML, array('context' => $coursecontext1)); $this->assertMatchesRegularExpression('/title=(\'|")CourseEntry(\'|")/', $filtered); $this->assertMatchesRegularExpression('/title=(\'|")SiteEntry(\'|")/', $filtered); // Testing at site level (only site). $filtered = format_text($html, FORMAT_HTML, array('context' => $sitecontext)); $this->assertDoesNotMatchRegularExpression('/title=(\'|")CourseEntry(\'|")/', $filtered); $this->assertMatchesRegularExpression('/title=(\'|")SiteEntry(\'|")/', $filtered); // Changing to another course to test the caches invalidation (only site). $filtered = format_text($html, FORMAT_HTML, array('context' => $coursecontext2)); $this->assertDoesNotMatchRegularExpression('/title=(\'|")CourseEntry(\'|")/', $filtered); $this->assertMatchesRegularExpression('/title=(\'|")SiteEntry(\'|")/', $filtered); } /** * Adds a database instance to the provided course + a text field + adds all attached entries. * * @param \stdClass $course * @param array $entries A list of entry names. * @return void */ protected function add_simple_database_instance($course, $entries = false) { global $DB; $database = $this->getDataGenerator()->create_module('data', array('course' => $course->id)); // A database field. $field = data_get_field_new('text', $database); $fielddetail = new \stdClass(); $fielddetail->d = $database->id; $fielddetail->mode = 'add'; $fielddetail->type = 'text'; $fielddetail->sesskey = sesskey(); $fielddetail->name = 'Name'; $fielddetail->description = 'Some name'; $fielddetail->param1 = '1'; $field->define_field($fielddetail); $field->insert_field(); $recordid = data_add_record($database); // Database entries. foreach ($entries as $entrytext) { $datacontent = array(); $datacontent['fieldid'] = $field->field->id; $datacontent['recordid'] = $recordid; $datacontent['content'] = $entrytext; $contentid = $DB->insert_record('data_content', $datacontent); } } }