Handle recurring tasks (#2713)

- Render recurrence form as new tab in edit dialog
- Display recurrence summary in task details
- When marking a recurring task complete:
  * shift dates and alarms to next occurrence
  * only if recurrence end reached save as completed
  * save a copy with status completed (sort of a journal)
This commit is contained in:
Thomas Bruederli 2014-04-24 19:44:21 +02:00
parent cd40e54641
commit a0ac82793b
8 changed files with 161 additions and 14 deletions

View file

@ -587,19 +587,22 @@ class tasklist_kolab_driver extends tasklist_driver
'flagged' => $record['priority'] == 1,
'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100),
'parent_id' => $record['parent_id'],
'recurrence' => $record['recurrence'],
);
// convert from DateTime to internal date format
if (is_a($record['due'], 'DateTime')) {
$task['date'] = $record['due']->format('Y-m-d');
$due = $this->plugin->lib->adjust_timezone($record['due']);
$task['date'] = $due->format('Y-m-d');
if (!$record['due']->_dateonly)
$task['time'] = $record['due']->format('H:i');
$task['time'] = $due->format('H:i');
}
// convert from DateTime to internal date format
if (is_a($record['start'], 'DateTime')) {
$task['startdate'] = $record['start']->format('Y-m-d');
$start = $this->plugin->lib->adjust_timezone($record['start']);
$task['startdate'] = $start->format('Y-m-d');
if (!$record['start']->_dateonly)
$task['starttime'] = $record['start']->format('H:i');
$task['starttime'] = $start->format('H:i');
}
if (is_a($record['dtstamp'], 'DateTime')) {
$task['changed'] = $record['dtstamp'];
@ -661,13 +664,12 @@ class tasklist_kolab_driver extends tasklist_driver
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// copy recurrence rules as long as the web client doesn't support it.
// that way it doesn't get removed when saving through the web client (#2713)
if ($old['recurrence']) {
// copy recurrence rules if the client didn't submit it (#2713)
if (!array_key_exists('recurrence', $object) && $old['recurrence']) {
$object['recurrence'] = $old['recurrence'];
}

View file

@ -18,6 +18,7 @@ $labels['description'] = 'Description';
$labels['datetime'] = 'Due';
$labels['start'] = 'Start';
$labels['alarms'] = 'Reminder';
$labels['repeat'] = 'Repeat';
$labels['all'] = 'All';
$labels['flagged'] = 'Flagged';

View file

@ -748,6 +748,12 @@ a.morelink:hover {
margin: 0.5em 0;
}
#taskedit .border-after {
padding-bottom: 0.8em;
margin-bottom: 0.8em;
border-bottom: 2px solid #fafafa;
}
#taskedit-attachments {
margin: 0.6em 0;
}

View file

@ -125,6 +125,10 @@
<span class="task-text"></span>
<span id="task-time"></span>
</div>
<div id="task-recurrence" class="form-section">
<label><roundcube:label name="tasklist.repeat" /></label>
<span class="task-text"></span>
</div>
<div id="task-alarm" class="form-section">
<label><roundcube:label name="tasklist.alarms" /></label>
<span class="task-text"></span>

View file

@ -1,10 +1,10 @@
<div id="taskedit" class="uidialog uidialog-tabbed">
<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
<ul>
<li><a href="#taskedit-tab-1"><roundcube:label name="tasklist.tabsummary" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-tab-2"><roundcube:label name="tasklist.tabattachments" /></a></li>
<li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
</ul>
<!-- basic info -->
<div id="taskedit-tab-1">
<div id="taskedit-panel-main">
<div class="form-section">
<label for="taskedit-title"><roundcube:label name="tasklist.title" /></label>
<br />
@ -51,8 +51,32 @@
<roundcube:object name="plugin.tasklist_select" id="taskedit-tasklist" tabindex="26" />
</div>
</div>
<!-- recurrence settings -->
<div id="taskedit-panel-recurrence">
<div class="form-section border-after">
<roundcube:object name="plugin.recurrence_form" part="frequency" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-daily">
<roundcube:object name="plugin.recurrence_form" part="daily" class="form-section" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-weekly">
<roundcube:object name="plugin.recurrence_form" part="weekly" class="form-section" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-monthly">
<roundcube:object name="plugin.recurrence_form" part="monthly" class="form-section" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-yearly">
<roundcube:object name="plugin.recurrence_form" part="yearly" class="form-section" />
</div>
<div class="recurrence-form" id="recurrence-form-until">
<roundcube:object name="plugin.recurrence_form" part="until" class="form-section" />
</div>
<div class="recurrence-form" id="recurrence-form-rdate">
<roundcube:object name="plugin.recurrence_form" part="rdate" class="form-section" />
</div>
</div>
<!-- attachments list (with upload form) -->
<div id="taskedit-tab-2">
<div id="taskedit-panel-attachments">
<div id="taskedit-attachments">
<roundcube:object name="plugin.attachments_list" id="taskedit-attachment-list" class="attachmentslist" />
</div>

View file

@ -385,8 +385,9 @@ function rcube_tasklist_ui(settings)
completeness_slider.slider('value', parseInt(this.value))
});
// register events on alarm fields
// register events on alarms and recurrence fields
me.init_alarms_edit('#taskedit-alarms');
me.init_recurrence_edit('#eventedit');
$('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings);
@ -1160,13 +1161,20 @@ function rcube_tasklist_ui(settings)
});
}
if (rec.recurrence && rec.recurrence_text) {
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
}
else {
$('#task-recurrence').hide();
}
// build attachments list
$('#task-attachments').hide();
if ($.isArray(rec.attachments)) {
task_show_attachments(rec.attachments || [], $('#task-attachments').children('.task-text'), rec);
if (rec.attachments.length > 0) {
$('#task-attachments').show();
}
}
}
// define dialog buttons
@ -1268,6 +1276,9 @@ function rcube_tasklist_ui(settings)
// set alarm(s)
me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []);
// set recurrence
me.set_recurrence_edit(rec);
// attachments
rcmail.enable_command('remove-attachment', list.editable);
me.selected_task.deleted_attachments = [];
@ -1298,6 +1309,7 @@ function rcube_tasklist_ui(settings)
me.selected_task.tags = [];
me.selected_task.attachments = [];
me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms');
me.selected_task.recurrence = me.serialize_recurrence(rectime.val());
// do some basic input validation
if (!me.selected_task.title || !me.selected_task.title.length) {

View file

@ -197,10 +197,16 @@ class tasklist extends rcube_plugin
case 'edit':
$rec = $this->prepare_task($rec);
$clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
if ($success = $this->driver->edit_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
$this->cleanup_task($rec);
// add clone from recurring task
if ($clone && $this->driver->create_task($clone)) {
$refresh[] = $this->driver->get_task($clone);
}
// move all childs if list assignment was changed
if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) {
foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) {
@ -419,6 +425,36 @@ class tasklist extends rcube_plugin
$rec['valarms'] = $valarms;
}
// convert the submitted recurrence settings
if (is_array($rec['recurrence'])) {
$refdate = null;
if (!empty($rec['date'])) {
$refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
}
else if (!empty($rec['startdate'])) {
$refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
}
if ($refdate) {
$rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate);
// translate count into an absolute end date.
// why? because when shifting completed tasks to the next recurrence,
// the initial start date to count from gets lost.
if ($rec['recurrence']['COUNT']) {
$engine = libcalendaring::get_recurrence();
$engine->init($rec['recurrence'], $refdate);
if ($until = $engine->end()) {
$rec['recurrence']['UNTIL'] = $until;
unset($rec['recurrence']['COUNT']);
}
}
}
else { // recurrence requires a reference date
$rec['recurrence'] = '';
}
}
$attachments = array();
$taskid = $rec['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
@ -480,6 +516,62 @@ class tasklist extends rcube_plugin
}
}
/**
* When flagging a recurring task as complete,
* clone it and shift dates to the next occurrence
*/
private function handle_recurrence(&$rec, $old)
{
$clone = null;
if ($rec['complete'] == 1.0 && $old && $old['complete'] < 1.0 && is_array($rec['recurrence'])) {
$engine = libcalendaring::get_recurrence();
$rrule = $rec['recurrence'];
$engine->init($rrule);
$updates = array();
// compute the next occurrence of date attributes
foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) {
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
$engine->set_start($date);
if ($next = $engine->next()) {
$updates[$date_key] = $next->format('Y-m-d');
if (!empty($rec[$time_key]))
$updates[$time_key] = $next->format('H:i');
}
}
// shift absolute alarm dates
if (!empty($updates) && is_array($rec['valarms'])) {
$updates['valarms'] = array();
unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited
$engine->init($rrule);
foreach ($rec['valarms'] as $i => $alarm) {
if ($alarm['trigger'] instanceof DateTime) {
$engine->set_start($alarm['trigger']);
if ($next = $engine->next()) {
$alarm['trigger'] = $next;
}
}
$updates['valarms'][$i] = $alarm;
}
}
if (!empty($updates)) {
// clone task to save a completed copy
$clone = $rec;
$clone['uid'] = $this->generate_uid();
$clone['parent_id'] = $rec['id'];
unset($clone['id'], $clone['recurrence'], $clone['attachments']);
// update the task but unset completed flag
$rec = array_merge($rec, $updates);
$rec['complete'] = $old['complete'];
}
}
return $clone;
}
/**
* Dispatcher for tasklist actions initiated by the client
@ -677,6 +769,11 @@ class tasklist extends rcube_plugin
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
}
if ($rec['recurrence']) {
$rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
}
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}

View file

@ -74,6 +74,7 @@ class tasklist_ui
$this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist'));
$this->plugin->register_handler('plugin.tags_editline', array($this, 'tags_editline'));
$this->plugin->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
$this->plugin->register_handler('plugin.recurrence_form', array($this->plugin->lib, 'recurrence_form'));
$this->plugin->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
$this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
$this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));