* Calendar: check recurrences for conflicts too (until configured search-time is exceeded, default 3s)

This commit is contained in:
Ralf Becker 2016-07-06 12:45:22 +02:00
parent 8597661970
commit 7a9e8f0c4c
6 changed files with 212 additions and 128 deletions

View File

@ -196,6 +196,12 @@ class calendar_bo
* @var Api\Categories * @var Api\Categories
*/ */
var $categories; var $categories;
/**
* Config values for "calendar", only used for horizont, regular calendar config is under phpgwapi
*
* @var array
*/
var $config;
/** /**
* Does a user require an extra invite grant, to be able to invite an other user, default no * Does a user require an extra invite grant, to be able to invite an other user, default no
@ -253,7 +259,7 @@ class calendar_bo
} }
//error_log(__METHOD__ . " registered resources=". array2string($this->resources)); //error_log(__METHOD__ . " registered resources=". array2string($this->resources));
$this->config = Api\Config::read('calendar'); // only used for horizont, regular calendar Api\Config is under phpgwapi $this->config = Api\Config::read('calendar'); // only used for horizont, regular calendar config is under phpgwapi
$this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite']; $this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite'];
$this->categories = new Api\Categories($this->user,'calendar'); $this->categories = new Api\Categories($this->user,'calendar');
@ -1343,6 +1349,7 @@ class calendar_bo
$param = "'$param'"; $param = "'$param'";
} }
break; break;
case 'EGroupware\\Api\\DateTime':
case 'egw_time': case 'egw_time':
case 'datetime': case 'datetime':
$p = $param; $p = $param;
@ -1363,6 +1370,7 @@ class calendar_bo
} }
$msg = str_replace('%'.($i-1),$param,$msg); $msg = str_replace('%'.($i-1),$param,$msg);
} }
error_log($msg);
if ($backtrace) error_log(function_backtrace(1)); if ($backtrace) error_log(function_backtrace(1));
} }

View File

@ -230,136 +230,19 @@ class calendar_boupdate extends calendar_bo
} }
} }
// check for conflicts only happens !$ignore_conflicts AND if start + end date are given // check for conflicts only happens !$ignore_conflicts AND if start + end date are given
if (!$ignore_conflicts && !$event['non_blocking'] && isset($event['start']) && isset($event['end'])) $checked_excluding = null;
if (!$ignore_conflicts && !$event['non_blocking'] && isset($event['start']) && isset($event['end']) &&
(($conflicts = $this->conflicts($event, $checked_excluding)) || $checked_excluding))
{ {
$types_with_quantity = array(); if ($checked_excluding) // warn user if not all recurrences have been checked
foreach($this->resources as $type => $data)
{ {
if ($data['max_quantity']) $types_with_quantity[] = $type; $conflicts['warning'] = array(
} 'start' => $checked_excluding,
// get all NOT rejected participants and evtl. their quantity 'title' => lang('Only recurrences until %1 (excluding) have been checked!', $checked_excluding->format(true)),
$quantity = $users = array();
foreach($event['participants'] as $uid => $status)
{
calendar_so::split_status($status,$q,$r);
if ($status[0] == 'R') continue; // ignore rejected participants
if ($uid < 0) // group, check it's members too
{
$users += (array)$GLOBALS['egw']->accounts->members($uid,true);
$users = array_unique($users);
}
$users[] = $uid;
if (in_array($uid[0],$types_with_quantity))
{
$quantity[$uid] = $q;
}
}
//$start = microtime(true);
$overlapping_events =& $this->search(array(
'start' => $event['start'],
'end' => $event['end'],
'users' => $users,
'ignore_acl' => true, // otherwise we get only events readable by the user
'enum_groups' => true, // otherwise group-events would not block time
'query' => array(
'cal_non_blocking' => 0,
),
'no_integration' => true, // do NOT use integration of other apps
));
//error_log(__METHOD__."() conflict check took ".number_format(microtime(true)-$start, 3).'s');
if ($this->debug > 2 || $this->debug == 'update')
{
$this->debug_message('calendar_boupdate::update() checking for potential overlapping events for users %1 from %2 to %3',false,$users,$event['start'],$event['end']);
}
$max_quantity = $possible_quantity_conflicts = $conflicts = array();
foreach((array) $overlapping_events as $k => $overlap)
{
if ($overlap['id'] == $event['id'] || // that's the event itself
$overlap['id'] == $event['reference'] || // event is an exception of overlap
$overlap['non_blocking']) // that's a non_blocking event
{
continue;
}
if ($this->debug > 3 || $this->debug == 'update')
{
$this->debug_message('calendar_boupdate::update() checking overlapping event %1',false,$overlap);
}
// check if the overlap is with a rejected participant or within the allowed quantity
$common_parts = array_intersect($users,array_keys($overlap['participants']));
foreach($common_parts as $n => $uid)
{
$status = $overlap['participants'][$uid];
calendar_so::split_status($status, $q, $r);
if ($status == 'R')
{
unset($common_parts[$n]);
continue;
}
if (is_numeric($uid) || !in_array($uid[0],$types_with_quantity))
{
continue; // no quantity check: quantity allways 1 ==> conflict
}
if (!isset($max_quantity[$uid]))
{
$res_info = $this->resource_info($uid);
$max_quantity[$uid] = $res_info[$this->resources[$uid[0]]['max_quantity']];
}
$quantity[$uid] += $q;
if ($quantity[$uid] <= $max_quantity[$uid])
{
$possible_quantity_conflicts[$uid][] =& $overlapping_events[$k]; // an other event can give the conflict
unset($common_parts[$n]);
continue;
}
// now we have a quantity conflict for $uid
}
if (count($common_parts))
{
if ($this->debug > 3 || $this->debug == 'update')
{
$this->debug_message('calendar_boupdate::update() conflicts with the following participants found %1',false,$common_parts);
}
$conflicts[$overlap['id'].'-'.$this->date2ts($overlap['start'])] =& $overlapping_events[$k];
}
}
// check if we are withing the allowed quantity and if not add all events using that resource
// seems this function is doing very strange things, it gives empty conflicts
foreach($max_quantity as $uid => $max)
{
if ($quantity[$uid] > $max)
{
foreach((array)$possible_quantity_conflicts[$uid] as $conflict)
{
$conflicts[$conflict['id'].'-'.$this->date2ts($conflict['start'])] =& $possible_quantity_conflicts[$k];
}
}
}
unset($possible_quantity_conflicts);
if (count($conflicts))
{
foreach($conflicts as $key => $conflict)
{
$conflict['participants'] = array_intersect_key((array)$conflict['participants'],$event['participants']);
if (!$this->check_perms(Acl::READ,$conflict))
{
$conflicts[$key] = array(
'id' => $conflict['id'],
'title' => lang('busy'),
'participants' => $conflict['participants'],
'start' => $conflict['start'],
'end' => $conflict['end'],
); );
} }
}
if ($this->debug > 2 || $this->debug == 'update')
{
$this->debug_message('calendar_boupdate::update() %1 conflicts found %2',false,count($conflicts),$conflicts);
}
return $conflicts; return $conflicts;
} }
}
//echo "saving $event[id]="; _debug_array($event); //echo "saving $event[id]="; _debug_array($event);
$event2save = $event; $event2save = $event;
@ -400,6 +283,182 @@ class calendar_boupdate extends calendar_bo
return $cal_id; return $cal_id;
} }
/**
* Check given event for conflicts and return them
*
* For recurring events we check a configurable fixed number of recurrences
* or we try for a fixed maximum time.
*
* @param array $event
* @param Api\DateTime& $checked_excluding =null time until which (excluding) recurrences have been checked
* @return array or events
*/
function conflicts(array $event, &$checked_excluding=null)
{
$types_with_quantity = array();
foreach($this->resources as $type => $data)
{
if ($data['max_quantity']) $types_with_quantity[] = $type;
}
// get all NOT rejected participants and evtl. their quantity
$quantity = $users = array();
foreach($event['participants'] as $uid => $status)
{
$q = $r = null;
calendar_so::split_status($status,$q,$r);
if ($status[0] == 'R') continue; // ignore rejected participants
if ($uid < 0) // group, check it's members too
{
$users = array_unique(array_merge($users, (array)$GLOBALS['egw']->accounts->members($uid,true)));
}
$users[] = $uid;
if (in_array($uid[0],$types_with_quantity))
{
$quantity[$uid] = $q;
}
}
$max_quantity = $possible_quantity_conflicts = $conflicts = array();
if ($event['recur_type'])
{
$recurences = calendar_rrule::event2rrule($event);
}
else
{
$recurences = array(new Api\DateTime((int)$event['start']));
}
$checked_excluding = null;
$max_checked = $GLOBALS['egw_info']['server']['conflict_max_checked'];
if (($max_check_time = (float)$GLOBALS['egw_info']['server']['conflict_max_check_time']) < 1.0)
{
$max_check_time = 3.0;
}
$checked = 0;
$start = microtime(true);
$duration = $event['end']-$event['start'];
foreach($recurences as $date)
{
$startts = $date->format('ts');
// abort check if configured limits are exceeded
if ($event['recur_type'] &&
($checked++ > $max_checked && $max_checked > 0 || // maximum number of checked recurrences exceeded
microtime(true) > $start+$max_check_time || // max check time exceeded
$startts > $this->config['horizont'])) // we are behind horizont for which recurring events are rendered
{
if ($this->debug > 2 || $this->debug == 'conflicts')
{
$this->debug_message(__METHOD__.'() conflict check limited to %1 recurrences, %2 seconds, until (excluding) %3',
$checked, microtime(true)-$start, $date);
}
$checked_excluding = $date;
break;
}
$overlapping_events =& $this->search(array(
'start' => $startts,
'end' => $startts+$duration,
'users' => $users,
'ignore_acl' => true, // otherwise we get only events readable by the user
'enum_groups' => true, // otherwise group-events would not block time
'query' => array(
'cal_non_blocking' => 0,
),
'no_integration' => true, // do NOT use integration of other apps
));
if ($this->debug > 2 || $this->debug == 'conflicts')
{
$this->debug_message(__METHOD__.'() checking for potential overlapping events for users %1 from %2 to %3',false,$users,$startts,$startts+$duration);
}
foreach((array) $overlapping_events as $k => $overlap)
{
if ($overlap['id'] == $event['id'] || // that's the event itself
$overlap['id'] == $event['reference'] || // event is an exception of overlap
$overlap['non_blocking']) // that's a non_blocking event
{
continue;
}
if ($this->debug > 3 || $this->debug == 'conflicts')
{
$this->debug_message(__METHOD__.'() checking overlapping event %1',false,$overlap);
}
// check if the overlap is with a rejected participant or within the allowed quantity
$common_parts = array_intersect($users,array_keys($overlap['participants']));
foreach($common_parts as $n => $uid)
{
$status = $overlap['participants'][$uid];
calendar_so::split_status($status, $q, $r);
if ($status == 'R')
{
unset($common_parts[$n]);
continue;
}
if (is_numeric($uid) || !in_array($uid[0],$types_with_quantity))
{
continue; // no quantity check: quantity allways 1 ==> conflict
}
if (!isset($max_quantity[$uid]))
{
$res_info = $this->resource_info($uid);
$max_quantity[$uid] = $res_info[$this->resources[$uid[0]]['max_quantity']];
}
$quantity[$uid] += $q;
if ($quantity[$uid] <= $max_quantity[$uid])
{
$possible_quantity_conflicts[$uid][] =& $overlapping_events[$k]; // an other event can give the conflict
unset($common_parts[$n]);
continue;
}
// now we have a quantity conflict for $uid
}
if (count($common_parts))
{
if ($this->debug > 3 || $this->debug == 'conflicts')
{
$this->debug_message(__METHOD__.'() conflicts with the following participants found %1',false,$common_parts);
}
$conflicts[$overlap['id'].'-'.$this->date2ts($overlap['start'])] =& $overlapping_events[$k];
}
}
}
//error_log(__METHOD__."() conflict check took ".number_format(microtime(true)-$start, 3).'s');
// check if we are withing the allowed quantity and if not add all events using that resource
// seems this function is doing very strange things, it gives empty conflicts
foreach($max_quantity as $uid => $max)
{
if ($quantity[$uid] > $max)
{
foreach((array)$possible_quantity_conflicts[$uid] as $conflict)
{
$conflicts[$conflict['id'].'-'.$this->date2ts($conflict['start'])] =& $possible_quantity_conflicts[$k];
}
}
}
unset($possible_quantity_conflicts);
if (count($conflicts))
{
foreach($conflicts as $key => $conflict)
{
$conflict['participants'] = array_intersect_key((array)$conflict['participants'],$event['participants']);
if (!$this->check_perms(Acl::READ,$conflict))
{
$conflicts[$key] = array(
'id' => $conflict['id'],
'title' => lang('busy'),
'participants' => $conflict['participants'],
'start' => $conflict['start'],
'end' => $conflict['end'],
);
}
}
if ($this->debug > 2 || $this->debug == 'conflicts')
{
$this->debug_message(__METHOD__.'() %1 conflicts found %2',false,count($conflicts),$conflicts);
}
}
return $conflicts;
}
/** /**
* Remove participants current user has no right to invite * Remove participants current user has no right to invite
* *

View File

@ -304,6 +304,8 @@ last changed calendar de letzte Änderung
lastname of person to notify calendar de Nachname der zu benachrichtigenden Person lastname of person to notify calendar de Nachname der zu benachrichtigenden Person
length of the time interval calendar de Länge des Zeitintervalls length of the time interval calendar de Länge des Zeitintervalls
limit number of description lines in list view (default 5, 0 for no limit) calendar de Anzahl Zeilen der Beschreibung in der Listenansicht (voreingestellt 5, 0 für alle) limit number of description lines in list view (default 5, 0 for no limit) calendar de Anzahl Zeilen der Beschreibung in der Listenansicht (voreingestellt 5, 0 für alle)
limit search for conflicts in recurrences to given number of recurrences calendar de Begrenze Suche nach Terminkonflikten auf die angegebene Anzahl Wiederholungen
limit search for conflicts in recurrences to given time in seconds (default 3) calendar de Begrenze Suche nach Terminkonflikten auf die angegebene Zeit in Sekunden (Vorgabe 3)
link title for events to show calendar de Erweiterung des Link-Titels für Kalender-Einträge link title for events to show calendar de Erweiterung des Link-Titels für Kalender-Einträge
link to view the event calendar de Verweis (Weblink) um den Termin anzuzeigen link to view the event calendar de Verweis (Weblink) um den Termin anzuzeigen
links calendar de Verknüpfungen links calendar de Verknüpfungen
@ -379,6 +381,7 @@ one month calendar de ein Monat
one week calendar de eine Woche one week calendar de eine Woche
one year calendar de ein Jahr one year calendar de ein Jahr
only group-events calendar de nur Gruppentermine only group-events calendar de nur Gruppentermine
only recurrences until %1 (excluding) have been checked! calendar de Wiederholungen wurden nur bis %1 (ausschließlich) überprüft!
only the initial date of that recuring event is checked! calendar de Nur das Startdatum diese wiederholenden Termins wird geprüft! only the initial date of that recuring event is checked! calendar de Nur das Startdatum diese wiederholenden Termins wird geprüft!
only used for first viewing of calendar, afterwards last selected view is used. calendar de Wird nur bei der Erstanzeige des Kalenders benutzt, danach immer die zuletzt ausgewählte Anzeige. only used for first viewing of calendar, afterwards last selected view is used. calendar de Wird nur bei der Erstanzeige des Kalenders benutzt, danach immer die zuletzt ausgewählte Anzeige.
open todo's: calendar de unerledigte Aufgaben: open todo's: calendar de unerledigte Aufgaben:

View File

@ -304,6 +304,8 @@ last changed calendar en Last changed
lastname of person to notify calendar en Last name of a person to notify lastname of person to notify calendar en Last name of a person to notify
length of the time interval calendar en Length of the time interval length of the time interval calendar en Length of the time interval
limit number of description lines in list view (default 5, 0 for no limit) calendar en Limit number of description lines in list view. Default is 5, 0 for no limit. limit number of description lines in list view (default 5, 0 for no limit) calendar en Limit number of description lines in list view. Default is 5, 0 for no limit.
limit search for conflicts in recurrences to given number of recurrences calendar en Limit search for conflicts in recurrences to given number of recurrences
limit search for conflicts in recurrences to given time in seconds (default 3) calendar en Limit search for conflicts in recurrences to given time in seconds (default 3)
link title for events to show calendar en Link title for events to show link title for events to show calendar en Link title for events to show
link to view the event calendar en Link to view the event link to view the event calendar en Link to view the event
links calendar en Links links calendar en Links
@ -379,6 +381,7 @@ one month calendar en One month
one week calendar en One week one week calendar en One week
one year calendar en One year one year calendar en One year
only group-events calendar en Only group events only group-events calendar en Only group events
only recurrences until %1 (excluding) have been checked! calendar en Only recurrences until %1 (excluding) have been checked!
only the initial date of that recuring event is checked! calendar en Only the initial date of that recurring event is checked! only the initial date of that recuring event is checked! calendar en Only the initial date of that recurring event is checked!
only used for first viewing of calendar, afterwards last selected view is used. calendar en Only used for first viewing of calendar, afterwards last selected view is used. only used for first viewing of calendar, afterwards last selected view is used. calendar en Only used for first viewing of calendar, afterwards last selected view is used.
open todo's: calendar en Open ToDo's: open todo's: calendar en Open ToDo's:

View File

@ -105,6 +105,17 @@
<option value="yes">Yes</option> <option value="yes">Yes</option>
</select> </select>
</row> </row>
<row>
<description value="Scheduling conflict" span="all" class="subHeader"/>
</row>
<row>
<description value="Limit search for conflicts in recurrences to given time in seconds (default 3)"/>
<float id="newsettings[conflict_max_check_time]" min="1" max="30"/>
</row>
<row>
<description value="Limit search for conflicts in recurrences to given number of recurrences"/>
<float id="newsettings[conflict_max_checked]"/>
</row>
</rows> </rows>
</grid> </grid>
</template> </template>

View File

@ -3,7 +3,7 @@
<!-- $Id$ --> <!-- $Id$ -->
<overlay> <overlay>
<template id="calendar.conflicts" template="" lang="" group="0" version="1.0.1.001"> <template id="calendar.conflicts" template="" lang="" group="0" version="1.0.1.001">
<description value=" Scheduling conflict" class="calendar_size120b"/> <description value="Scheduling conflict" class="calendar_size120b"/>
<grid> <grid>
<columns> <columns>
<column/> <column/>