2 commits - plugins/calendar plugins/libcalendaring plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Thu Apr 24 19:50:04 CEST 2014


 plugins/calendar/calendar_ui.js                          |    2 
 plugins/calendar/lib/calendar_recurrence.php             |   12 +
 plugins/libcalendaring/lib/libcalendaring_recurrence.php |   26 ++--
 plugins/libcalendaring/libcalendaring.js                 |    4 
 plugins/libcalendaring/libcalendaring.php                |    5 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |   20 +--
 plugins/tasklist/localization/en_US.inc                  |    1 
 plugins/tasklist/skins/larry/tasklist.css                |    6 
 plugins/tasklist/skins/larry/templates/mainview.html     |    4 
 plugins/tasklist/skins/larry/templates/taskedit.html     |   30 ++++
 plugins/tasklist/tasklist.js                             |   16 ++
 plugins/tasklist/tasklist.php                            |   97 +++++++++++++++
 plugins/tasklist/tasklist_ui.php                         |    1 
 13 files changed, 198 insertions(+), 26 deletions(-)

New commits:
commit a0ac82793b5b8f8b95fbbc159c9a810c566e9b61
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Apr 24 19:44:21 2014 +0200

    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)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 56dc955..9497b39 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 6fc7ce1..18456fa 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 adc6fe6..c940dff 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 b7038f4..fe3f88b 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -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>
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 7dc0b40..c0a5b1c 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -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>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 86f0624..6f11ed5 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 ace7e45..be82f82 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 f2a90be..6988a61 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'));


commit cd40e54641c0b098c933d6909043945ef0e73b20
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Apr 24 19:41:07 2014 +0200

    Fix recurrence form serialization; better method names

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index a864261..fe2b89e 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -604,7 +604,7 @@ function rcube_calendar_ui(settings)
           priority: priority.val(),
           sensitivity: sensitivity.val(),
           status: eventstatus.val(),
-          recurrence: me.serialize_recurrence(),
+          recurrence: me.serialize_recurrence(endtime.val()),
           valarms: me.serialize_alarms('#edit-alarms'),
           attendees: event_attendees,
           deleted_attachments: rcmail.env.deleted_attachments,
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index b31026a..fae98bb 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -49,13 +49,23 @@ class calendar_recurrence extends libcalendaring_recurrence
   }
 
   /**
+   * Alias of libcalendaring_recurrence::next()
+   *
+   * @return mixed DateTime object or False if recurrence ended
+   */
+  public function next_start()
+  {
+    return $this->next();
+  }
+
+  /**
    * Get the next recurring instance of this event
    *
    * @return mixed Array with event properties or False if recurrence ended
    */
   public function next_instance()
   {
-    if ($next_start = $this->next_start()) {
+    if ($next_start = $this->next()) {
       $next = $this->event;
       $next['recurrence_id'] = $next_start->format('Y-m-d');
       $next['start'] = $next_start;
diff --git a/plugins/libcalendaring/lib/libcalendaring_recurrence.php b/plugins/libcalendaring/lib/libcalendaring_recurrence.php
index 3423ae7..bbc4976 100644
--- a/plugins/libcalendaring/lib/libcalendaring_recurrence.php
+++ b/plugins/libcalendaring/lib/libcalendaring_recurrence.php
@@ -52,17 +52,15 @@ class libcalendaring_recurrence
      * @param array  The recurrence properties
      * @param object DateTime The recurrence start date
      */
-    public function init($recurrence, $start)
+    public function init($recurrence, $start = null)
     {
-        $this->start = $start;
         $this->recurrence = $recurrence;
-        $this->dateonly = $start->_dateonly;
-        $this->next = new Horde_Date($start, $this->lib->timezone->getName());
-        $this->hour = $this->next->hour;
 
         $this->engine = new Horde_Date_Recurrence($start);
         $this->engine->fromRRule20(libcalendaring::to_rrule($recurrence));
 
+        $this->set_start($start);
+
         if (is_array($recurrence['EXDATE'])) {
             foreach ($recurrence['EXDATE'] as $exdate) {
                 if (is_a($exdate, 'DateTime')) {
@@ -80,11 +78,25 @@ class libcalendaring_recurrence
     }
 
     /**
+     * Setter for (new) recurrence start date
+     *
+     * @param object DateTime The recurrence start date
+     */
+    public function set_start($start)
+    {
+        $this->start = $start;
+        $this->dateonly = $start->_dateonly;
+        $this->next = new Horde_Date($start, $this->lib->timezone->getName());
+        $this->hour = $this->next->hour;
+        $this->engine->setRecurStart($this->next);
+    }
+
+    /**
      * Get date/time of the next occurence of this event
      *
      * @return mixed DateTime object or False if recurrence ended
      */
-    public function next_start()
+    public function next()
     {
         $time = false;
         $after = clone $this->next;
@@ -131,7 +143,7 @@ class libcalendaring_recurrence
         if ($this->recurrence['COUNT']) {
             $last = $this->start;
             $this->next = new Horde_Date($this->start, $this->lib->timezone->getName());
-            while (($next = $this->next_start()) && $c < 1000) {
+            while (($next = $this->next()) && $c < 1000) {
                 $last = $next;
                 $c++;
             }
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index c338684..1d93c13 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -615,7 +615,7 @@ function rcube_libcalendaring(settings)
     /**
      * Gather recurrence settings from form
      */
-    this.serialize_recurrence = function()
+    this.serialize_recurrence = function(timestr)
     {
         var recurrence = '',
             freq = $('#edit-recurrence-frequency').val();
@@ -630,7 +630,7 @@ function rcube_libcalendaring(settings)
             if (until == 'count')
                 recurrence.COUNT = $('#edit-recurrence-repeat-times').val();
             else if (until == 'until')
-                recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(endtime.val(), $('#edit-recurrence-enddate').val()));
+                recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val()));
 
             if (freq == 'WEEKLY') {
                 var byday = [];
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 1b680a5..09a9c68 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -397,7 +397,10 @@ class libcalendaring extends rcube_plugin
     {
         return array_map(function($alarm){
             if ($alarm['trigger'][0] == '@') {
-                try { $alarm['trigger'] = new DateTime($alarm['trigger']); }
+                try {
+                    $alarm['trigger'] = new DateTime($alarm['trigger']);
+                    $alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
+                }
                 catch (Exception $e) { /* handle this ? */ }
             }
             else if ($trigger = libcalendaring::parse_alaram_value($alarm['trigger'])) {




More information about the commits mailing list