Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  declare(strict_types=1);
  18  
  19  namespace core_reportbuilder\local\filters;
  20  
  21  use DateTimeImmutable;
  22  use lang_string;
  23  use MoodleQuickForm;
  24  use core_reportbuilder\local\helpers\database;
  25  
  26  /**
  27   * Date report filter
  28   *
  29   * This filter accepts a unix timestamp to perform date filtering on
  30   *
  31   * @package     core_reportbuilder
  32   * @copyright   2021 Paul Holden <paulh@moodle.com>
  33   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class date extends base {
  36  
  37      /** @var int Any value */
  38      public const DATE_ANY = 0;
  39  
  40      /** @var int Non-empty (positive) value */
  41      public const DATE_NOT_EMPTY = 1;
  42  
  43      /** @var int Empty (zero) value */
  44      public const DATE_EMPTY = 2;
  45  
  46      /** @var int Date within defined range */
  47      public const DATE_RANGE = 3;
  48  
  49      /** @var int Date in the last [X relative date unit(s)] */
  50      public const DATE_LAST = 4;
  51  
  52      /** @var int Date in the previous [X relative date unit(s)] Kept for backwards compatibility */
  53      public const DATE_PREVIOUS = self::DATE_LAST;
  54  
  55      /** @var int Date in current [relative date unit] */
  56      public const DATE_CURRENT = 5;
  57  
  58      /** @var int Date in the next [X relative date unit(s)] */
  59      public const DATE_NEXT = 6;
  60  
  61      /** @var int Relative date unit for a day */
  62      public const DATE_UNIT_DAY = 1;
  63  
  64      /** @var int Relative date unit for a week */
  65      public const DATE_UNIT_WEEK = 2;
  66  
  67      /** @var int Relative date unit for a month */
  68      public const DATE_UNIT_MONTH = 3;
  69  
  70      /** @var int Relative date unit for a month */
  71      public const DATE_UNIT_YEAR = 4;
  72  
  73      /**
  74       * Return an array of operators available for this filter
  75       *
  76       * @return lang_string[]
  77       */
  78      private function get_operators(): array {
  79          $operators = [
  80              self::DATE_ANY => new lang_string('filterisanyvalue', 'core_reportbuilder'),
  81              self::DATE_NOT_EMPTY => new lang_string('filterisnotempty', 'core_reportbuilder'),
  82              self::DATE_EMPTY => new lang_string('filterisempty', 'core_reportbuilder'),
  83              self::DATE_RANGE => new lang_string('filterrange', 'core_reportbuilder'),
  84              self::DATE_LAST => new lang_string('filterdatelast', 'core_reportbuilder'),
  85              self::DATE_CURRENT => new lang_string('filterdatecurrent', 'core_reportbuilder'),
  86              self::DATE_NEXT => new lang_string('filterdatenext', 'core_reportbuilder'),
  87          ];
  88  
  89          return $this->filter->restrict_limited_operators($operators);
  90      }
  91  
  92      /**
  93       * Setup form
  94       *
  95       * @param MoodleQuickForm $mform
  96       */
  97      public function setup_form(MoodleQuickForm $mform): void {
  98          // Operator selector.
  99          $operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header());
 100  
 101          $elements[] = $mform->createElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators());
 102          $mform->setType("{$this->name}_operator", PARAM_INT);
 103          $mform->setDefault("{$this->name}_operator", self::DATE_ANY);
 104  
 105          // Value selector for last and next operators.
 106          $valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header());
 107  
 108          $elements[] = $mform->createElement('text', "{$this->name}_value", $valuelabel, ['size' => 3]);
 109          $mform->setType("{$this->name}_value", PARAM_INT);
 110          $mform->setDefault("{$this->name}_value", 1);
 111          $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'eq', self::DATE_ANY);
 112          $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'eq', self::DATE_NOT_EMPTY);
 113          $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'eq', self::DATE_EMPTY);
 114          $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'eq', self::DATE_RANGE);
 115          $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'eq', self::DATE_CURRENT);
 116  
 117          // Unit selector for last and next operators.
 118          $unitlabel = get_string('filterdurationunit', 'core_reportbuilder', $this->get_header());
 119          $units = [
 120              self::DATE_UNIT_DAY => get_string('filterdatedays', 'core_reportbuilder'),
 121              self::DATE_UNIT_WEEK => get_string('filterdateweeks', 'core_reportbuilder'),
 122              self::DATE_UNIT_MONTH => get_string('filterdatemonths', 'core_reportbuilder'),
 123              self::DATE_UNIT_YEAR => get_string('filterdateyears', 'core_reportbuilder'),
 124          ];
 125  
 126          $elements[] = $mform->createElement('select', "{$this->name}_unit", $unitlabel, $units);
 127          $mform->setType("{$this->name}_unit", PARAM_INT);
 128          $mform->setDefault("{$this->name}_unit", self::DATE_UNIT_DAY);
 129          $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'eq', self::DATE_ANY);
 130          $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'eq', self::DATE_NOT_EMPTY);
 131          $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'eq', self::DATE_EMPTY);
 132          $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'eq', self::DATE_RANGE);
 133  
 134          // Add operator/value/unit group.
 135          $mform->addGroup($elements, "{$this->name}_group", '', '', false);
 136  
 137          // Date selectors for range operator.
 138          $mform->addElement('date_selector', "{$this->name}_from", get_string('filterdatefrom', 'core_reportbuilder'),
 139              ['optional' => true]);
 140          $mform->setType("{$this->name}_from", PARAM_INT);
 141          $mform->setDefault("{$this->name}_from", 0);
 142          $mform->hideIf("{$this->name}_from", "{$this->name}_operator", 'neq', self::DATE_RANGE);
 143  
 144          $mform->addElement('date_selector', "{$this->name}_to", get_string('filterdateto', 'core_reportbuilder'),
 145              ['optional' => true]);
 146          $mform->setType("{$this->name}_to", PARAM_INT);
 147          $mform->setDefault("{$this->name}_to", 0);
 148          $mform->hideIf("{$this->name}_to", "{$this->name}_operator", 'neq', self::DATE_RANGE);
 149      }
 150  
 151      /**
 152       * Return filter SQL
 153       *
 154       * @param array $values
 155       * @return array
 156       */
 157      public function get_sql_filter(array $values): array {
 158          $fieldsql = $this->filter->get_field_sql();
 159          $params = $this->filter->get_field_params();
 160  
 161          $operator = (int) ($values["{$this->name}_operator"] ?? self::DATE_ANY);
 162          $dateunitvalue = (int) ($values["{$this->name}_value"] ?? 1);
 163          $dateunit = (int) ($values["{$this->name}_unit"] ?? self::DATE_UNIT_DAY);
 164  
 165          switch ($operator) {
 166              case self::DATE_NOT_EMPTY:
 167                  $sql = "COALESCE({$fieldsql}, 0) <> 0";
 168                  break;
 169              case self::DATE_EMPTY:
 170                  $sql = "COALESCE({$fieldsql}, 0) = 0";
 171                  break;
 172              case self::DATE_RANGE:
 173                  $sql = '';
 174  
 175                  $datefrom = (int)($values["{$this->name}_from"] ?? 0);
 176                  $dateto = (int)($values["{$this->name}_to"] ?? 0);
 177  
 178                  $paramdatefrom = database::generate_param_name();
 179                  $paramdateto = database::generate_param_name();
 180  
 181                  if ($datefrom > 0 && $dateto > 0) {
 182                      $sql = "{$fieldsql} BETWEEN :{$paramdatefrom} AND :{$paramdateto}";
 183                      $params[$paramdatefrom] = $datefrom;
 184                      $params[$paramdateto] = $dateto;
 185                  } else if ($datefrom > 0) {
 186                      $sql = "{$fieldsql} >= :{$paramdatefrom}";
 187                      $params[$paramdatefrom] = $datefrom;
 188                  } else if ($dateto > 0) {
 189                      $sql = "{$fieldsql} < :{$paramdateto}";
 190                      $params[$paramdateto] = $dateto;
 191                  }
 192  
 193                  break;
 194              // Relative helper method can handle these three cases.
 195              case self::DATE_LAST:
 196              case self::DATE_CURRENT:
 197              case self::DATE_NEXT:
 198  
 199                  // Last and next operators require a unit value greater than zero.
 200                  if ($operator !== self::DATE_CURRENT && $dateunitvalue === 0) {
 201                      return ['', []];
 202                  }
 203  
 204                  $paramdatefrom = database::generate_param_name();
 205                  $paramdateto = database::generate_param_name();
 206  
 207                  $sql = "{$fieldsql} BETWEEN :{$paramdatefrom} AND :{$paramdateto}";
 208                  [
 209                      $params[$paramdatefrom],
 210                      $params[$paramdateto],
 211                  ] = self::get_relative_timeframe($operator, $dateunitvalue, $dateunit);
 212  
 213                  break;
 214              default:
 215                  // Invalid or inactive filter.
 216                  return ['', []];
 217          }
 218  
 219          return [$sql, $params];
 220      }
 221  
 222      /**
 223       * Return start and end time of given relative date period
 224       *
 225       * @param int $operator One of the ::DATE_LAST/CURRENT/NEXT constants
 226       * @param int $dateunitvalue Unit multiplier of the date unit
 227       * @param int $dateunit One of the ::DATE_UNIT_DAY/WEEK/MONTH/YEAR constants
 228       * @return int[] Timestamps representing the start/end of timeframe
 229       */
 230      private static function get_relative_timeframe(int $operator, int $dateunitvalue, int $dateunit): array {
 231          // Initialise start/end time to now.
 232          $datestart = $dateend = new DateTimeImmutable();
 233  
 234          switch ($dateunit) {
 235              case self::DATE_UNIT_DAY:
 236                  if ($operator === self::DATE_CURRENT) {
 237                      $datestart = $datestart->setTime(0, 0);
 238                      $dateend = $dateend->setTime(23, 59, 59);
 239                  } else if ($operator === self::DATE_LAST) {
 240                      $datestart = $datestart->modify("-{$dateunitvalue} day");
 241                  } else if ($operator === self::DATE_NEXT) {
 242                      $dateend = $dateend->modify("+{$dateunitvalue} day");
 243                  }
 244  
 245                  break;
 246              case self::DATE_UNIT_WEEK:
 247                  if ($operator === self::DATE_CURRENT) {
 248                      // The first day of the week is determined by site calendar configuration/preferences.
 249                      $startweekday = \core_calendar\type_factory::get_calendar_instance()->get_starting_weekday();
 250                      $weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
 251  
 252                      // If calculated start of week is after today (today is Tues/start of week is Weds), move back a week.
 253                      $datestartnow = $datestart->getTimestamp();
 254                      $datestart = $datestart->modify($weekdays[$startweekday] . ' this week')->setTime(0, 0);
 255                      if ($datestart->getTimestamp() > $datestartnow) {
 256                          $datestart = $datestart->modify('-1 week');
 257                      }
 258  
 259                      $dateend = $datestart->modify('+6 day')->setTime(23, 59, 59);
 260                  } else if ($operator === self::DATE_LAST) {
 261                      $datestart = $datestart->modify("-{$dateunitvalue} week");
 262                  } else if ($operator === self::DATE_NEXT) {
 263                      $dateend = $dateend->modify("+{$dateunitvalue} week");
 264                  }
 265  
 266                  break;
 267              case self::DATE_UNIT_MONTH:
 268                  if ($operator === self::DATE_CURRENT) {
 269                      $datestart = $datestart->modify('first day of this month')->setTime(0, 0);
 270                      $dateend = $dateend->modify('last day of this month')->setTime(23, 59, 59);
 271                  } else if ($operator === self::DATE_LAST) {
 272                      $datestart = $datestart->modify("-{$dateunitvalue} month");
 273                  } else if ($operator === self::DATE_NEXT) {
 274                      $dateend = $dateend->modify("+{$dateunitvalue} month");
 275                  }
 276  
 277                  break;
 278              case self::DATE_UNIT_YEAR:
 279                  if ($operator === self::DATE_CURRENT) {
 280                      $datestart = $datestart->modify('first day of january this year')->setTime(0, 0);
 281                      $dateend = $dateend->modify('last day of december this year')->setTime(23, 59, 59);
 282                  } else if ($operator === self::DATE_LAST) {
 283                      $datestart = $datestart->modify("-{$dateunitvalue} year");
 284                  } else if ($operator === self::DATE_NEXT) {
 285                      $dateend = $dateend->modify("+{$dateunitvalue} year");
 286                  }
 287  
 288                  break;
 289          }
 290  
 291          return [
 292              $datestart->getTimestamp(),
 293              $dateend->getTimestamp(),
 294          ];
 295      }
 296  
 297      /**
 298       * Return sample filter values
 299       *
 300       * @return array
 301       */
 302      public function get_sample_values(): array {
 303          return [
 304              "{$this->name}_operator" => self::DATE_CURRENT,
 305              "{$this->name}_unit" => self::DATE_UNIT_WEEK,
 306          ];
 307      }
 308  }