From a0ac82793b5b8f8b95fbbc159c9a810c566e9b61 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 24 Apr 2014 19:44:21 +0200 Subject: [PATCH] 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) --- .../drivers/kolab/tasklist_kolab_driver.php | 20 ++-- plugins/tasklist/localization/en_US.inc | 1 + plugins/tasklist/skins/larry/tasklist.css | 6 ++ .../skins/larry/templates/mainview.html | 4 + .../skins/larry/templates/taskedit.html | 30 +++++- plugins/tasklist/tasklist.js | 16 ++- plugins/tasklist/tasklist.php | 97 +++++++++++++++++++ plugins/tasklist/tasklist_ui.php | 1 + 8 files changed, 161 insertions(+), 14 deletions(-) diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 56dc9553..9497b393 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -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']; } diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index 6fc7ce16..18456fa6 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -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'; diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css index adc6fe6d..c940dff9 100644 --- a/plugins/tasklist/skins/larry/tasklist.css +++ b/plugins/tasklist/skins/larry/tasklist.css @@ -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; } diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index b7038f49..fe3f88bf 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -125,6 +125,10 @@ +
+ + +
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html index 7dc0b400..c0a5b1cb 100644 --- a/plugins/tasklist/skins/larry/templates/taskedit.html +++ b/plugins/tasklist/skins/larry/templates/taskedit.html @@ -1,10 +1,10 @@
    -
  • +
-
+

@@ -51,8 +51,32 @@
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
-
+
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 86f06246..6f11ed5a 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -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) { diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index ace7e452..be82f82f 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -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']); } diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index f2a90bef..6988a618 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -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'));