Commit 9a9d76a2 authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski

Fix creation of recurrent tickets outside calendar hours

parent d111c802
......@@ -356,6 +356,20 @@ class Calendar extends CommonDropdown {
}
/**
* Determines if calendar has, at least, one working day.
*
* @since 9.4.3
*
* @return boolean
**/
public function hasAWorkingDay() {
$durations = $this->getDurationsCache();
return false !== $durations && array_sum($durations) > 0;
}
/**
* Is the time passed is in a working hour
*
......
......@@ -307,91 +307,99 @@ class TicketRecurrent extends CommonDropdown {
/**
* Compute next creation date of a ticket
* Compute next creation date of a ticket.
*
* New parameter in version 0.84 : $calendars_id
* @param string $begin_date Begin date of the recurrent ticket in 'Y-m-d H:i:s' format.
* @param string $end_date End date of the recurrent ticket in 'Y-m-d H:i:s' format,
* or 'NULL' or empty value.
* @param string|integer $periodicity Periodicity of creation, could be:
* - an integer corresponding to seconds,
* - a string using "/([0-9]+)(MONTH|YEAR)/" pattern.
* @param integer $create_before Anticipated creation delay in seconds.
* @param integer|null $calendars_id ID of the calendar to use to restrict creation to working hours,
* or 0 / null for no calendar.
*
* @param $begin_date datetime Begin date of the recurrent ticket
* @param $end_date datetime End date of the recurrent ticket
* @param $periodicity timestamp Periodicity of creation
* @param $create_before timestamp Create before specific timestamp
* @param $calendars_id integer ID of the calendar to used
* @return string Next creation date in 'Y-m-d H:i:s' format.
*
* @return datetime next creation date
**/
* @since 0.84 $calendars_id parameter added
*/
function computeNextCreationDate($begin_date, $end_date, $periodicity, $create_before,
$calendars_id) {
if (empty($begin_date) || ($begin_date == 'NULL')) {
$now = time();
$periodicity_pattern = '/([0-9]+)(MONTH|YEAR)/';
if (false === DateTime::createFromFormat('Y-m-d H:i:s', $begin_date)) {
// Invalid begin date.
return 'NULL';
}
if (!empty($end_date) && ($end_date <> 'NULL')) {
if (strtotime($end_date) < time()) {
return 'NULL';
}
$has_end_date = false !== DateTime::createFromFormat('Y-m-d H:i:s', $end_date);
if ($has_end_date && strtotime($end_date) < $now) {
// End date is in past.
return 'NULL';
}
$check = true;
if (preg_match('/([0-9]+)MONTH/', $periodicity)
|| preg_match('/([0-9]+)YEAR/', $periodicity)) {
$check = false;
if (!is_int($periodicity) && !preg_match($periodicity_pattern, $periodicity)) {
// Invalid periodicity.
return 'NULL';
}
// Compute periodicity values
$periodicity_as_interval = null;
$periodicity_in_seconds = $periodicity;
$matches = [];
if (preg_match($periodicity_pattern, $periodicity, $matches)) {
$periodicity_as_interval = "{$matches[1]} {$matches[2]}";
$periodicity_in_seconds = $matches[1]
* MONTH_TIMESTAMP
* ('YEAR' === $matches[2] ? 12 : 1);
} else if ($periodicity % DAY_TIMESTAMP == 0) {
$periodicity_as_interval = ($periodicity / DAY_TIMESTAMP) . ' DAY';
} else {
$periodicity_as_interval = ($periodicity / HOUR_TIMESTAMP) . ' HOUR';
}
if ($check
&& ($create_before > $periodicity)) {
Session::addMessageAfterRedirect(__('Invalid frequency. It must be greater than the preliminary creation.'),
false, ERROR);
// Check that anticipated creation delay is greater than periodicity.
if ($create_before > $periodicity_in_seconds) {
Session::addMessageAfterRedirect(
__('Invalid frequency. It must be greater than the preliminary creation.'),
false,
ERROR
);
return 'NULL';
}
if ($periodicity <> 0) {
// Standard time computation
$timestart = strtotime($begin_date) - $create_before;
$now = time();
if ($now > $timestart) {
$value = $periodicity;
$step = "second";
if (preg_match('/([0-9]+)MONTH/', $periodicity, $matches)) {
$value = $matches[1];
$step = 'MONTH';
} else if (preg_match('/([0-9]+)YEAR/', $periodicity, $matches)) {
$value = $matches[1];
$step = 'YEAR';
} else {
if (($value%DAY_TIMESTAMP)==0) {
$value = $value/DAY_TIMESTAMP;
$step = "DAY";
} else {
$value = $value/HOUR_TIMESTAMP;
$step = "HOUR";
}
}
// First occurence of creation
$occurence_time = strtotime($begin_date);
$creation_time = $occurence_time - $create_before;
while ($timestart < $now) {
$timestart = strtotime("+ $value $step", $timestart);
}
}
// Time start over end date
if (!empty($end_date) && ($end_date <> 'NULL')) {
if ($timestart > strtotime($end_date)) {
return 'NULL';
}
// Add steps while creation time is in past
while ($creation_time < $now) {
$creation_time = strtotime("+ $periodicity_as_interval", $creation_time);
$occurence_time = $creation_time + $create_before;
// Stop if end date reached
if ($has_end_date && $occurence_time > strtotime($end_date)) {
return 'NULL';
}
}
// Add steps while start time is not in working hours
$calendar = new Calendar();
if ($calendars_id && $calendar->getFromDB($calendars_id) && $calendar->hasAWorkingDay()) {
while (!$calendar->isAWorkingHour($occurence_time)) {
$creation_time = strtotime("+ $periodicity_as_interval", $creation_time);
$occurence_time = $creation_time + $create_before;
$calendar = new Calendar();
if ($calendars_id
&& $calendar->getFromDB($calendars_id)) {
$durations = $calendar->getDurationsCache();
if (array_sum($durations) > 0) { // working days exists
while (!$calendar->isAWorkingDay($timestart)) {
$timestart = strtotime("+ 1 day", $timestart);
}
// Stop if end date reached
if ($has_end_date && $occurence_time > strtotime($end_date)) {
return 'NULL';
}
}
return date("Y-m-d H:i:s", $timestart);
}
return 'NULL';
return date("Y-m-d H:i:s", $creation_time);
}
......
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI 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 2 of the License, or
* (at your option) any later version.
*
* GLPI 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 GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace tests\units;
use \DbTestCase;
/* Test for inc/ticketrecurrent.class.php */
class TicketRecurrent extends DbTestCase {
/**
* Data provider for self::testConvertTagToImage().
*/
protected function computeNextCreationDateProvider() {
$start_of_current_month = date('Y-m-01 00:00:00');
$end_of_next_year = date('Y-m-d 23:59:59', strtotime('last day of next year'));
// Create a calendar where evey day except today is a working day
$calendar = new \Calendar();
$segment = new \CalendarSegment();
$calendar_id = $calendar->add(['name' => 'TicketRecurrent testing calendar']);
$this->integer($calendar_id)->isGreaterThan(0);
for ($day = 0; $day <= 6; $day++) {
if ($day == date('w')) {
continue;
}
$segment_id = $segment->add(
[
'calendars_id' => $calendar_id,
'day' => $day,
'begin' => '09:00:00',
'end' => '19:00:00'
]
);
$this->integer($segment_id)->isGreaterThan(0);
}
return [
// Empty begin date
[
'begin_date' => '',
'end_date' => $end_of_next_year,
'periodicity' => HOUR_TIMESTAMP,
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => 'NULL',
],
// Invalid begin date
[
'begin_date' => '',
'end_date' => $end_of_next_year,
'periodicity' => HOUR_TIMESTAMP,
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => 'NULL',
],
// Empty periodicity
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => '',
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => 'NULL',
],
// Invalid periodicity
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => '3WEEK',
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => 'NULL',
],
// Invalid anticipated creation delay compared to periodicity
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => HOUR_TIMESTAMP,
'create_before' => HOUR_TIMESTAMP * 2,
'calendars_id' => 0,
'expected_value' => 'NULL',
],
// End date in past
[
'begin_date' => '2018-03-26 15:00:00',
'end_date' => '2019-01-12 00:00:00',
'periodicity' => '1MONTH',
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => 'NULL',
],
// Valid case 1: ticket created every hour with no anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => HOUR_TIMESTAMP,
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => date('Y-m-d H:00:00', strtotime('+ 1 hour')),
],
// Valid case 2: ticket created every hour with anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => HOUR_TIMESTAMP,
'create_before' => HOUR_TIMESTAMP,
'calendars_id' => 0,
'expected_value' => date('Y-m-d H:00:00', strtotime('+ 1 hour')),
],
// Valid case 3: ticket created every hour with no anticipation and with calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => HOUR_TIMESTAMP,
'create_before' => HOUR_TIMESTAMP,
'calendars_id' => $calendar_id,
'expected_value' => date('Y-m-d 08:00:00', strtotime('tomorrow')), // 1 hour anticipation
],
// Valid case 4: ticket created every day with no anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => DAY_TIMESTAMP,
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => date('Y-m-d 00:00:00', strtotime('+ 1 day')),
],
// Valid case 5: ticket created every hour with anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => DAY_TIMESTAMP,
'create_before' => HOUR_TIMESTAMP * 2,
'calendars_id' => 0,
'expected_value' => date('Y-m-d 22:00:00'), // 2 hours anticipation
],
// Valid case 6: ticket created every hour with no anticipation and with calendar
[
'begin_date' => date('Y-m-01 09:00:00'), // first day of month at 9am
'end_date' => $end_of_next_year,
'periodicity' => DAY_TIMESTAMP,
'create_before' => HOUR_TIMESTAMP * 2,
'calendars_id' => $calendar_id,
'expected_value' => date('Y-m-d 07:00:00', strtotime('tomorrow')), // 2 hours anticipation
],
// Valid case 7: ticket created every 2 month with no anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => '2MONTH',
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => date('Y-m-01 00:00:00', strtotime('+ 2 month')),
],
// Valid case 8: ticket created every month with anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => '1MONTH',
'create_before' => DAY_TIMESTAMP * 5,
'calendars_id' => 0,
'expected_value' => date('Y-m-d 00:00:00', strtotime('+ 1 month', strtotime($start_of_current_month . ' - 5 days'))), // 5 days anticipation
],
// Valid case 9: ticket created every year with no anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => '1YEAR',
'create_before' => 0,
'calendars_id' => 0,
'expected_value' => date('Y-m-01 00:00:00', strtotime('+ 1 year')),
],
// Valid case 10: ticket created every year with anticipation and no calendar
[
'begin_date' => $start_of_current_month,
'end_date' => $end_of_next_year,
'periodicity' => '1YEAR',
'create_before' => DAY_TIMESTAMP * 4,
'calendars_id' => 0,
'expected_value' => date('Y-m-d 00:00:00', strtotime('+ 1 year', strtotime($start_of_current_month . ' - 4 days'))), // 4 day anticipation
],
];
}
/**
* @param string $begin_date
* @param string $end_date
* @param string|integer $periodicity
* @param integer $create_before
* @param integer $calendars_id
* @param string $expected_value
*
* @dataProvider computeNextCreationDateProvider
*/
public function testComputeNextCreationDate(
$begin_date,
$end_date,
$periodicity,
$create_before,
$calendars_id,
$expected_value) {
$ticketRecurrent = new \TicketRecurrent();
$value = $ticketRecurrent->computeNextCreationDate(
$begin_date,
$end_date,
$periodicity,
$create_before,
$calendars_id
);
$this->string($value)->isIdenticalTo($expected_value);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment