. /** * This file contains the customcert date range element. * * @package customcertelement_daterange * @copyright 2018 Dmitrii Metelkin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace customcertelement_daterange; use \mod_customcert\element_helper; defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); require_once($CFG->dirroot . '/lib/grade/constants.php'); /** * The customcert date range element. * * @package customcertelement_daterange * @copyright 2018 Dmitrii Metelkin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class element extends \mod_customcert\element { /** * Max recurring period in seconds. */ const MAX_RECURRING_PERIOD = 31556926; // 12 months. /** * Current year placeholder string. */ const CURRENT_YEAR_PLACEHOLDER = '{{current_year}}'; /** * First year in a date range placeholder string. */ const RANGE_FIRST_YEAR_PLACEHOLDER = '{{range_first_year}}'; /** * Last year in a date range placeholder string. */ const RANGE_LAST_YEAR_PLACEHOLDER = '{{range_last_year}}'; /** * First year in a date range placeholder string. */ const RECUR_RANGE_FIRST_YEAR_PLACEHOLDER = '{{recurring_range_first_year}}'; /** * Last year in a date range placeholder string. */ const RECUR_RANGE_LAST_YEAR_PLACEHOLDER = '{{recurring_range_last_year}}'; /** * A year in the user's date. */ const DATE_YEAR_PLACEHOLDER = '{{date_year}}'; /** * Date - Issue */ const DATE_ISSUE = -1; /** * Date - Completion */ const DATE_COMPLETION = -2; /** * Date - Course start */ const DATE_COURSE_START = -3; /** * Date - Course end */ const DATE_COURSE_END = -4; /** * Date - Course grade date */ const DATE_COURSE_GRADE = -5; /** * Date - Course current date */ const DATE_CURRENT_DATE = -6; /** * This function renders the form elements when adding a customcert element. * * @param \MoodleQuickForm $mform the edit form instance */ public function render_form_elements($mform) { global $COURSE; // Get the possible date options. $dateoptions = array(); $dateoptions[self::DATE_ISSUE] = get_string('issueddate', 'customcertelement_daterange'); $dateoptions[self::DATE_CURRENT_DATE] = get_string('currentdate', 'customcertelement_daterange'); $dateoptions[self::DATE_COMPLETION] = get_string('completiondate', 'customcertelement_daterange'); $dateoptions[self::DATE_COURSE_START] = get_string('coursestartdate', 'customcertelement_daterange'); $dateoptions[self::DATE_COURSE_END] = get_string('courseenddate', 'customcertelement_daterange'); $dateoptions[self::DATE_COURSE_GRADE] = get_string('coursegradedate', 'customcertelement_daterange'); $dateoptions = $dateoptions + element_helper::get_grade_items($COURSE); $mform->addElement('select', 'dateitem', get_string('dateitem', 'customcertelement_daterange'), $dateoptions); $mform->addHelpButton('dateitem', 'dateitem', 'customcertelement_daterange'); parent::render_form_elements($mform); $mform->addElement('header', 'dateranges', get_string('dateranges', 'customcertelement_daterange')); $mform->addElement('static', 'help', '', get_string('help', 'customcertelement_daterange')); $mform->addElement('static', 'placeholders', '', get_string('placeholders', 'customcertelement_daterange')); $mform->addElement('text', 'fallbackstring', get_string('fallbackstring', 'customcertelement_daterange')); $mform->addHelpButton('fallbackstring', 'fallbackstring', 'customcertelement_daterange'); $mform->setType('fallbackstring', PARAM_NOTAGS); if (empty($this->get_decoded_data()->dateranges)) { $repeats = 1; } else { $repeats = count($this->get_decoded_data()->dateranges); } $ranges = []; $ranges[] = $mform->createElement('html', '
'); $ranges[] = $mform->createElement( 'date_selector', 'startdate', get_string('start', 'customcertelement_daterange') ); $ranges[] = $mform->createElement( 'date_selector', 'enddate', get_string('end', 'customcertelement_daterange') ); $ranges[] = $mform->createElement( 'checkbox', 'recurring', get_string('recurring', 'customcertelement_daterange') ); $ranges[] = $mform->createElement( 'text', 'datestring', get_string('datestring', 'customcertelement_daterange'), ['class' => 'datestring'] ); $ranges[] = $mform->createElement( 'advcheckbox', 'rangedelete', get_string('setdeleted', 'customcertelement_daterange'), '', [], [0, 1] ); $rangeoptions = array(); $rangeoptions['startdate']['type'] = PARAM_INT; $rangeoptions['enddate']['type'] = PARAM_INT; $rangeoptions['recurring']['type'] = PARAM_INT; $rangeoptions['recurring']['helpbutton'] = ['recurring', 'customcertelement_daterange']; $rangeoptions['datestring']['type'] = PARAM_NOTAGS; $rangeoptions['rangedelete']['type'] = PARAM_BOOL; $addstring = get_string('addrange', 'customcertelement_daterange'); $this->get_edit_element_form()->repeat_elements($ranges, $repeats, $rangeoptions, 'repeats', 'add', 1, $addstring, true); } /** * A helper function to build consistent form element name. * * @param string $name * @param string $num * * @return string */ protected function build_element_name($name, $num) { return $name . '[' . $num . ']'; } /** * Get decoded data stored in DB. * * @return \stdClass */ protected function get_decoded_data() { return json_decode($this->get_data()); } /** * Sets the data on the form when editing an element. * * @param \MoodleQuickForm $mform the edit form instance */ public function definition_after_data($mform) { if (!empty($this->get_data()) && !$mform->isSubmitted()) { $element = $mform->getElement('dateitem'); $element->setValue($this->get_decoded_data()->dateitem); $element = $mform->getElement('fallbackstring'); $element->setValue($this->get_decoded_data()->fallbackstring); foreach ($this->get_decoded_data()->dateranges as $key => $range) { $mform->setDefault($this->build_element_name('startdate', $key), $range->startdate); $mform->setDefault($this->build_element_name('enddate', $key), $range->enddate); $mform->setDefault($this->build_element_name('datestring', $key), $range->datestring); $mform->setDefault($this->build_element_name('recurring', $key), $range->recurring); } } parent::definition_after_data($mform); } /** * Performs validation on the element values. * * @param array $data the submitted data * @param array $files the submitted files * @return array the validation errors */ public function validate_form_elements($data, $files) { $errors = parent::validate_form_elements($data, $files); // Check if at least one range is set. $error = get_string('error:atleastone', 'customcertelement_daterange'); for ($i = 0; $i < $data['repeats']; $i++) { if (empty($data['rangedelete'][$i])) { $error = ''; } } if (!empty($error)) { $errors['help'] = $error; } // Check that datestring is set dataranges what aren't need to be deleted. for ($i = 0; $i < $data['repeats']; $i++) { // Skip elements that needs to be deleted. if (!empty($data['rangedelete'][$i])) { continue; } if (empty($data['datestring'][$i])) { $name = $this->build_element_name('datestring', $i); $errors[$name] = get_string('error:datestring', 'customcertelement_daterange'); } // Check that end date is correctly set. if ( $data['startdate'][$i] >= $data['enddate'][$i] ) { $errors[$this->build_element_name('enddate', $i)] = get_string('error:enddate', 'customcertelement_daterange'); } $rangeperiod = $data['enddate'][$i] - $data['startdate'][$i]; // Check that recurring dateranges are not longer than 12 months. if (!empty($data['recurring'][$i]) && $rangeperiod >= self::MAX_RECURRING_PERIOD ) { $errors[$this->build_element_name('enddate', $i)] = get_string('error:recurring', 'customcertelement_daterange'); } } return $errors; } /** * This will handle how form data will be saved into the data column in the * customcert_elements table. * * @param \stdClass $data the form data * @return string the json encoded array */ public function save_unique_data($data) { $arrtostore = array( 'dateitem' => $data->dateitem, 'fallbackstring' => $data->fallbackstring, 'dateranges' => [], ); for ($i = 0; $i < $data->repeats; $i++) { if (empty($data->rangedelete[$i])) { $arrtostore['dateranges'][] = [ 'startdate' => $data->startdate[$i], 'enddate' => $data->enddate[$i], 'datestring' => $data->datestring[$i], 'recurring' => !empty($data->recurring[$i]), ]; } } // Encode these variables before saving into the DB. return json_encode($arrtostore); } /** * Handles rendering the element on the pdf. * * @param \pdf $pdf the pdf object * @param bool $preview true if it is a preview, false otherwise * @param \stdClass $user the user we are rendering this for */ public function render($pdf, $preview, $user) { global $DB; // If there is no element data, we have nothing to display. if (empty($this->get_data())) { return; } $courseid = element_helper::get_courseid($this->id); $dateitem = $this->get_decoded_data()->dateitem; // If we are previewing this certificate then just show a demonstration date. if ($preview) { $date = time(); } else { // Get the page. $page = $DB->get_record('customcert_pages', array('id' => $this->get_pageid()), '*', MUST_EXIST); // Get the customcert this page belongs to. $customcert = $DB->get_record('customcert', array('templateid' => $page->templateid), '*', MUST_EXIST); // Now we can get the issue for this user. $issue = $DB->get_record('customcert_issues', array('userid' => $user->id, 'customcertid' => $customcert->id), '*', MUST_EXIST); switch ($dateitem) { case self::DATE_ISSUE: $date = $issue->timecreated; break; case self::DATE_CURRENT_DATE: $date = time(); break; case self::DATE_COMPLETION: // Get the last completion date. $sql = "SELECT MAX(c.timecompleted) as timecompleted FROM {course_completions} c WHERE c.userid = :userid AND c.course = :courseid"; if ($timecompleted = $DB->get_record_sql($sql, array('userid' => $issue->userid, 'courseid' => $courseid))) { if (!empty($timecompleted->timecompleted)) { $date = $timecompleted->timecompleted; } } break; case self::DATE_COURSE_START: $date = $DB->get_field('course', 'startdate', array('id' => $courseid)); break; case self::DATE_COURSE_END: $date = $DB->get_field('course', 'enddate', array('id' => $courseid)); break; case self::DATE_COURSE_GRADE: $grade = element_helper::get_course_grade_info( $courseid, GRADE_DISPLAY_TYPE_DEFAULT, $user->id ); if ($grade && !empty($grade->get_dategraded())) { $date = $grade->get_dategraded(); } break; default: if (strpos($dateitem, 'gradeitem:') === 0) { $gradeitemid = substr($dateitem, 10); $grade = element_helper::get_grade_item_info( $gradeitemid, $dateitem, $user->id ); } else { $grade = element_helper::get_mod_grade_info( $dateitem, GRADE_DISPLAY_TYPE_DEFAULT, $user->id ); } if ($grade && !empty($grade->get_dategraded())) { $date = $grade->get_dategraded(); } break; } } // Ensure that a date has been set. if (!empty($date)) { element_helper::render_content($pdf, $this, $this->get_daterange_string($date)); } } /** * Get daterange string. * * @param int $date Unix stamp date. * * @return string */ protected function get_daterange_string($date) { $matchedrange = null; $outputstring = ''; $formatdata = []; $formatdata['date'] = $date; foreach ($this->get_decoded_data()->dateranges as $key => $range) { if ($this->is_recurring_range($range)) { if ($matchedrange = $this->get_matched_recurring_range($date, $range)) { $outputstring = $matchedrange->datestring; $formatdata['range'] = $range; $formatdata['recurringrange'] = $matchedrange; break; } } else { if ($this->is_date_in_range($date, $range)) { $outputstring = $range->datestring; $formatdata['range'] = $range; break; } } } if (empty($outputstring) && !empty($this->get_decoded_data()->fallbackstring)) { $outputstring = $this->get_decoded_data()->fallbackstring; } return $this->format_date_string($outputstring, $formatdata); } /** * Returns whether or not a range is recurring. * * @param \stdClass $range Range object. * * @return bool */ protected function is_recurring_range(\stdClass $range) { return !empty($range->recurring); } /** * Check if the provided date is in the date range. * * @param int $date Unix timestamp date to check. * @param \stdClass $range Range object. * * @return bool */ protected function is_date_in_range($date, \stdClass $range) { return ($date >= $range->startdate && $date <= $range->enddate); } /** * Check if provided date is in the recurring date range. * * @param int $date Unix timestamp date to check. * @param \stdClass $range Range object. * * @return bool */ protected function is_date_in_recurring_range($date, \stdClass $range) { $intdate = $this->build_number_from_date($date); $intstart = $this->build_number_from_date($range->startdate); $intend = $this->build_number_from_date($range->enddate); if (!$this->has_turn_of_the_year($range)) { if ($intdate >= $intstart && $intdate <= $intend) { return true; } } else { if ($intdate >= $intstart && $intdate >= $intend) { return true; } if ($intdate <= $intstart && $intdate <= $intend) { return true; } } return false; } /** * Check if provided recurring range has a turn of the year. * * @param \stdClass $reccurringrange Range object. * * @return bool */ protected function has_turn_of_the_year(\stdClass $reccurringrange) { return date('Y', $reccurringrange->startdate) != date('Y', $reccurringrange->enddate); } /** * Check if provided date is in the start year of the recurring range with a turn of the year. * * @param int $date Unix timestamp date to check. * @param \stdClass $range Range object. * * @return bool */ protected function in_start_year($date, \stdClass $range) { $intdate = $this->build_number_from_date($date); $intstart = $this->build_number_from_date($range->startdate); $intend = $this->build_number_from_date($range->enddate); return $intdate >= $intstart && $intdate >= $intend; } /** * Check if provided date is in the end year of the recurring range with a turn of the year. * * @param int $date Unix timestamp date to check. * @param \stdClass $range Range object. * * @return bool */ protected function in_end_year($date, \stdClass $range) { $intdate = $this->build_number_from_date($date); $intstart = $this->build_number_from_date($range->startdate); $intend = $this->build_number_from_date($range->enddate); return $intdate <= $intstart && $intdate <= $intend; } /** * Return matched recurring date range. * * As recurring date ranges do not depend on the year, * we will use a date's year to build a new matched recurring date range with * start year and end year. This is required to replace placeholders like range_first_year and range_last_year. * * @param int $date Unix timestamp date to check. * @param \stdClass $range Range object. * * @return \stdClass || null */ protected function get_matched_recurring_range($date, \stdClass $range) { if (!$this->is_date_in_recurring_range($date, $range)) { return null; } $matchedrage = clone $range; if ($this->has_turn_of_the_year($matchedrage)) { if ($this->in_start_year($date, $matchedrage)) { $startyear = date('Y', $date); $endyear = $startyear + 1; $matchedrage->startdate = strtotime(date('d.m.', $matchedrage->startdate) . $startyear); $matchedrage->enddate = strtotime(date('d.m.', $matchedrage->enddate) . $endyear); return $matchedrage; } if ($this->in_end_year($date, $matchedrage)) { $endyear = date('Y', $date); $startyear = $endyear - 1; $matchedrage->startdate = strtotime(date('d.m.', $matchedrage->startdate) . $startyear); $matchedrage->enddate = strtotime(date('d.m.', $matchedrage->enddate) . $endyear); return $matchedrage; } } else { $matchedrage->startdate = strtotime(date('d.m.', $matchedrage->startdate) . date('Y', $date)); $matchedrage->enddate = strtotime(date('d.m.', $matchedrage->enddate) . date('Y', $date)); return $matchedrage; } return null; } /** * Build number representation of the provided date. * * @param int $date Unix timestamp date to check. * * @return int */ protected function build_number_from_date($date) { return (int)date('md', $date); } /** * Format date string based on different types of placeholders. * * @param string $datestring The date string * @param array $formatdata A list of format data. * * @return string */ protected function format_date_string($datestring, array $formatdata) { foreach ($this->get_placeholders() as $search => $replace) { $datestring = str_replace($search, $replace, $datestring); } if (!empty($formatdata['date'])) { foreach ($this->get_date_placeholders($formatdata['date']) as $search => $replace) { $datestring = str_replace($search, $replace, $datestring); } } if (!empty($formatdata['range'])) { foreach ($this->get_range_placeholders($formatdata['range']) as $search => $replace) { $datestring = str_replace($search, $replace, $datestring); } } if (!empty($formatdata['recurringrange'])) { foreach ($this->get_recurring_range_placeholders($formatdata['recurringrange']) as $search => $replace) { $datestring = str_replace($search, $replace, $datestring); } } return $datestring; } /** * Return a list of placeholders to replace in date string as search => $replace pairs. * * @return array */ protected function get_placeholders() { return [ self::CURRENT_YEAR_PLACEHOLDER => date('Y', time()), ]; } /** * Return a list of user's date related placeholders to replace in date string as search => $replace pairs. * @param int $date Unix timestamp date to check. * * @return array */ protected function get_date_placeholders($date) { return [ self::DATE_YEAR_PLACEHOLDER => date('Y', $date), ]; } /** * Return a list of range related placeholders to replace in date string as search => $replace pairs. * * @param \stdClass $range * * @return array */ protected function get_range_placeholders(\stdClass $range) { return [ self::RANGE_FIRST_YEAR_PLACEHOLDER => date('Y', $range->startdate), self::RANGE_LAST_YEAR_PLACEHOLDER => date('Y', $range->enddate), ]; } /** * Return a list of recurring range s placeholders to replace in date string as search => $replace pairs. * * @param \stdClass $range * * @return array */ protected function get_recurring_range_placeholders(\stdClass $range) { return [ self::RECUR_RANGE_FIRST_YEAR_PLACEHOLDER => date('Y', $range->startdate), self::RECUR_RANGE_LAST_YEAR_PLACEHOLDER => date('Y', $range->enddate), ]; } /** * Render the element in html. * * This function is used to render the element when we are using the * drag and drop interface to position it. * * @return string the html */ public function render_html() { // If there is no element data, we have nothing to display. if (empty($this->get_data())) { return; } return element_helper::render_html_content($this, get_string('preview', 'customcertelement_daterange', $this->get_name())); } /** * This function is responsible for handling the restoration process of the element. * * We will want to update the course module the date element is pointing to as it will * have changed in the course restore. * * @param \restore_customcert_activity_task $restore */ public function after_restore($restore) { global $DB; $data = $this->get_decoded_data(); if ($newitem = \restore_dbops::get_backup_ids_record($restore->get_restoreid(), 'course_module', $data->dateitem)) { $data->dateitem = $newitem->newitemid; $DB->set_field('customcert_elements', 'data', $this->save_unique_data($data), array('id' => $this->get_id())); } } }