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