diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php index 465ba901..b3c6a42e 100644 --- a/plugins/libkolab/lib/kolab_format_task.php +++ b/plugins/libkolab/lib/kolab_format_task.php @@ -44,7 +44,7 @@ class kolab_format_task extends kolab_format_xcal $this->obj->setPercentComplete(intval($object['complete'])); $status = kolabformat::StatusUndefined; - if ($object['complete'] == 100) + if ($object['complete'] == 100 && !array_key_exists('status', $object)) $status = kolabformat::StatusCompleted; else if ($object['status'] && array_key_exists($object['status'], $this->status_map)) $status = $this->status_map[$object['status']]; @@ -113,7 +113,7 @@ class kolab_format_task extends kolab_format_xcal { $tags = array(); - if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100) + if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status']))) $tags[] = 'x-complete'; if ($this->data['priority'] == 1) diff --git a/plugins/tasklist/drivers/database/SQL/mysql.initial.sql b/plugins/tasklist/drivers/database/SQL/mysql.initial.sql index 2746ca4c..aae636da 100644 --- a/plugins/tasklist/drivers/database/SQL/mysql.initial.sql +++ b/plugins/tasklist/drivers/database/SQL/mysql.initial.sql @@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS `tasks` ( `starttime` varchar(5) DEFAULT NULL, `flagged` tinyint(4) NOT NULL DEFAULT '0', `complete` float NOT NULL DEFAULT '0', + `status` enum('','NEEDS-ACTION','IN-PROCESS','COMPLETED','CANCELLED') NOT NULL DEFAULT '', `alarms` varchar(255) DEFAULT NULL, `recurrence` varchar(255) DEFAULT NULL, `organizer` varchar(255) DEFAULT NULL, @@ -48,4 +49,4 @@ CREATE TABLE IF NOT EXISTS `tasks` ( REFERENCES `tasklists`(`tasklist_id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */; -REPLACE INTO `system` (`name`, `value`) VALUES ('tasklist-database-version', '2013011000'); +REPLACE INTO `system` (`name`, `value`) VALUES ('tasklist-database-version', '2014051900'); diff --git a/plugins/tasklist/drivers/database/SQL/mysql/2014051900.sql b/plugins/tasklist/drivers/database/SQL/mysql/2014051900.sql new file mode 100644 index 00000000..e5f9a620 --- /dev/null +++ b/plugins/tasklist/drivers/database/SQL/mysql/2014051900.sql @@ -0,0 +1,3 @@ +ALTER TABLE `tasks` ADD `status` ENUM('','NEEDS-ACTION','IN-PROCESS','COMPLETED','CANCELLED') NOT NULL DEFAULT '' AFTER `complete`; + +UPDATE `tasks` SET status='COMPLETED' WHERE complete=1.0 AND status=''; diff --git a/plugins/tasklist/drivers/database/SQL/postgres.initial.sql b/plugins/tasklist/drivers/database/SQL/postgres.initial.sql index 4a445128..e5665c2d 100644 --- a/plugins/tasklist/drivers/database/SQL/postgres.initial.sql +++ b/plugins/tasklist/drivers/database/SQL/postgres.initial.sql @@ -49,6 +49,7 @@ CREATE TABLE tasks ( starttime varchar(5) DEFAULT NULL, flagged smallint NOT NULL DEFAULT 0, complete float NOT NULL DEFAULT 0, + status varchar(16) NOT NULL DEFAULT '', alarms varchar(255) DEFAULT NULL, recurrence varchar(255) DEFAULT NULL, organizer varchar(255) DEFAULT NULL, @@ -60,4 +61,4 @@ CREATE TABLE tasks ( CREATE INDEX tasks_tasklisting_idx ON tasks (tasklist_id, del, date); CREATE INDEX tasks_uid_idx ON tasks (uid); -INSERT INTO system (name, value) VALUES ('tasklist-database-version', '2013011000'); +INSERT INTO system (name, value) VALUES ('tasklist-database-version', '2014051900'); diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index d9bf4145..6c1e0ea4 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -24,6 +24,8 @@ class tasklist_database_driver extends tasklist_driver { + const IS_COMPLETE_SQL = "(status='COMPLETED' OR (complete=1 AND status=''))"; + public $undelete = true; // yes, we can public $sortable = false; public $alarm_types = array('DISPLAY'); @@ -224,7 +226,7 @@ class tasklist_database_driver extends tasklist_driver $result = $this->rc->db->query(sprintf( "SELECT task_id, flagged, date FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) - AND del=0 AND complete<1", + AND del=0 AND NOT " . self::IS_COMPLETE_SQL, join(',', $list_ids) )); @@ -286,9 +288,9 @@ class tasklist_database_driver extends tasklist_driver $sql_add = ' AND date IS NULL'; if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) - $sql_add .= ' AND complete=1'; + $sql_add .= ' AND ' . self::IS_COMPLETE_SQL; else if (empty($filter['since'])) // don't show complete tasks by default - $sql_add .= ' AND complete<1'; + $sql_add .= ' AND NOT ' . self::IS_COMPLETE_SQL; if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED) $sql_add .= ' AND flagged=1'; @@ -435,7 +437,7 @@ class tasklist_database_driver extends tasklist_driver $result = $this->rc->db->query(sprintf( "SELECT * FROM " . $this->db_tasks . " WHERE tasklist_id IN (%s) - AND notify <= %s AND complete < 1", + AND notify <= %s AND NOT " . self::IS_COMPLETE_SQL, join(',', $list_ids), $this->rc->db->fromunixtime($time) )); @@ -529,7 +531,7 @@ class tasklist_database_driver extends tasklist_driver $prop['recurrence'] = $this->serialize_recurrence($prop['recurrence']); } - foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence') as $col) { + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { if (empty($prop[$col])) $prop[$col] = null; } @@ -537,8 +539,8 @@ class tasklist_database_driver extends tasklist_driver $notify_at = $this->_get_notification($prop); $result = $this->rc->db->query(sprintf( "INSERT INTO " . $this->db_tasks . " - (tasklist_id, uid, parent_id, created, changed, title, date, time, startdate, starttime, description, tags, flagged, complete, alarms, recurrence, notify) - VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (tasklist_id, uid, parent_id, created, changed, title, date, time, startdate, starttime, description, tags, flagged, complete, status, alarms, recurrence, notify) + VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $this->rc->db->now(), $this->rc->db->now() ), @@ -554,6 +556,7 @@ class tasklist_database_driver extends tasklist_driver join(',', (array)$prop['tags']), $prop['flagged'] ? 1 : 0, intval($prop['complete']), + $prop['status'], $prop['alarms'], $prop['recurrence'], $notify_at @@ -586,7 +589,7 @@ class tasklist_database_driver extends tasklist_driver if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]); } - foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence') as $col) { + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms', 'recurrence', 'status') as $col) { if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col])); } @@ -694,7 +697,7 @@ class tasklist_database_driver extends tasklist_driver */ private function _get_notification($task) { - if ($task['valarms'] && $task['complete'] < 1) { + if ($task['valarms'] && !$this->is_complete($task)) { $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index ad36777b..31185231 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -307,7 +307,7 @@ class tasklist_kolab_driver extends tasklist_driver foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) { $rec = $this->_to_rcube_task($record); - if ($rec['complete'] >= 1.0) // don't count complete tasks + if ($this->is_complete($rec)) // don't count complete tasks continue; $counts['all']++; @@ -603,7 +603,8 @@ class tasklist_kolab_driver extends tasklist_driver 'description' => $record['description'], 'tags' => array_filter((array)$record['categories']), 'flagged' => $record['priority'] == 1, - 'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100), + 'complete' => floatval($record['complete'] / 100), + 'status' => $record['status'], 'parent_id' => $record['parent_id'], 'recurrence' => $record['recurrence'], ); @@ -672,7 +673,7 @@ class tasklist_kolab_driver extends tasklist_driver } $object['complete'] = $task['complete'] * 100; - if ($task['complete'] == 1.0) + if ($task['complete'] == 1.0 && empty($task['complete'])) $object['status'] = 'COMPLETED'; if ($task['flagged']) diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index 6c31fa7a..76480f55 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -41,7 +41,7 @@ * 'categories' => 'Task category', * 'flagged' => 'Boolean value whether this record is flagged', * 'complete' => 'Float value representing the completeness state (range 0..1)', - * 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential) + * 'status' => 'Task status string according to (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) RFC 2445', * 'valarms' => array( // List of reminders (new format), each represented as a hash array: * array( * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object @@ -270,6 +270,17 @@ abstract class tasklist_driver */ public function get_attachment_body($id, $task) { } + /** + * Helper method to determine whether the given task is considered "complete" + * + * @param array $task Hash array with event properties: + * @return boolean True if complete, False otherwiese + */ + public function is_complete($task) + { + return ($task['complete'] >= 1.0 && empty($task['status'])) || $task['status'] === 'COMPLETED'; + } + /** * List availabale categories * The default implementation reads them from config/user prefs diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index 18456fa6..714e79cd 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -19,6 +19,11 @@ $labels['datetime'] = 'Due'; $labels['start'] = 'Start'; $labels['alarms'] = 'Reminder'; $labels['repeat'] = 'Repeat'; +$labels['status'] = 'Status'; +$labels['status-needs-action'] = 'Needs action'; +$labels['status-in-process'] = 'In process'; +$labels['status-completed'] = 'Completed'; +$labels['status-cancelled'] = 'Cancelled'; $labels['all'] = 'All'; $labels['flagged'] = 'Flagged'; diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index fe3f88bf..1761683b 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -141,6 +141,10 @@ +
+ + +
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html index 31ee79c2..97f604db 100644 --- a/plugins/tasklist/skins/larry/templates/taskedit.html +++ b/plugins/tasklist/skins/larry/templates/taskedit.html @@ -46,6 +46,10 @@  %
+
+ + +
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index fcf43062..4bc82b0a 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -264,7 +264,7 @@ function rcube_tasklist_ui(settings) if (rcmail.busy) return false; - rec.complete = e.target.checked ? 1 : 0; + rec.status = e.target.checked ? 'COMPLETED' : (rec.complete == 1 ? 'NEEDS-ACTION' : ''); li.toggleClass('complete'); save_task(rec, 'edit'); return true; @@ -815,7 +815,7 @@ function rcube_tasklist_ui(settings) var div = $('
').addClass('taskhead').html( '
' + - '' + + '' + '' + '' + text2html(Q(rec.title)) + '' + '' + tags_html + '' + @@ -835,7 +835,7 @@ function rcube_tasklist_ui(settings) revertDuration: 300 }); - if (rec.complete == 1.0) + if (is_complete(rec)) div.addClass('complete'); if (rec.flagged) div.addClass('flagged'); @@ -951,12 +951,20 @@ function rcube_tasklist_ui(settings) */ function task_cmp(a, b) { - var d = Math.floor(a.complete) - Math.floor(b.complete); + var d = is_complete(a) - is_complete(b); if (!d) d = (b._hasdate-0) - (a._hasdate-0); if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999); return d; } + /** + * Determine whether the given task should be displayed as "complete" + */ + function is_complete(rec) + { + return ((rec.complete == 1.0 && !rec.status) || rec.status === 'COMPLETED') ? 1 : 0; + } + /** * */ @@ -1144,6 +1152,7 @@ function rcube_tasklist_ui(settings) $('#task-starttime').html(Q(rec.starttime || '')); $('#task-alarm')[(rec.alarms_text ? 'show' : 'hide')]().children('.task-text').html(Q(rec.alarms_text)); $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); + $('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').html(rcmail.gettext('status-'+String(rec.status).toLowerCase(),'tasklist')); $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : '')); var itags = get_inherited_tags(rec); @@ -1257,6 +1266,7 @@ function rcube_tasklist_ui(settings) var recstarttime = $('#taskedit-starttime').val(rec.starttime || ''); var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100); completeness_slider.slider('value', complete.val()); + var taskstatus = $('#taskedit-status').val(rec.status || ''); var tasklist = $('#taskedit-tasklist').val(rec.list || me.selected_list).prop('disabled', rec.parent_id ? true : false); // tag-edit line @@ -1308,7 +1318,7 @@ function rcube_tasklist_ui(settings) var buttons = {}; buttons[rcmail.gettext('save', 'tasklist')] = function() { // copy form field contents into task object to save - $.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, list:tasklist }, function(key,input){ + $.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){ me.selected_task[key] = input.val(); }); me.selected_task.tags = []; diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index 65376d7c..d8ba4d04 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -214,7 +214,7 @@ class tasklist extends rcube_plugin $child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']); if ($this->driver->move_task($child)) { $r = $this->driver->get_task($child); - if ((bool)($filter & self::FILTER_MASK_COMPLETE) == ($r['complete'] == 1.0)) { + if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) { $refresh[] = $r; } } @@ -237,7 +237,7 @@ class tasklist extends rcube_plugin $child['id'] = $cid; if ($this->driver->move_task($child)) { $r = $this->driver->get_task($child); - if ((bool)($filter & self::FILTER_MASK_COMPLETE) == ($r['complete'] == 1.0)) { + if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) { $refresh[] = $r; } } @@ -524,7 +524,7 @@ class tasklist extends rcube_plugin private function handle_recurrence(&$rec, $old) { $clone = null; - if ($rec['complete'] == 1.0 && $old && $old['complete'] < 1.0 && is_array($rec['recurrence'])) { + if ($this->driver->is_complete($rec) && $old && $this->driver->is_complete($old) && is_array($rec['recurrence'])) { $engine = libcalendaring::get_recurrence(); $rrule = $rec['recurrence']; $updates = array(); @@ -569,6 +569,7 @@ class tasklist extends rcube_plugin // update the task but unset completed flag $rec = array_merge($rec, $updates); $rec['complete'] = $old['complete']; + $rec['satus'] = $old['satus']; } } @@ -718,7 +719,7 @@ class tasklist extends rcube_plugin $tags = array_merge($tags, (array)$rec['tags']); // apply filter; don't trust the driver on this :-) - if ((!$f && $rec['complete'] < 1.0) || ($rec['mask'] & $f)) + if ((!$f && !$this->driver->is_complete($rec)) || ($rec['mask'] & $f)) $data[] = $rec; } @@ -845,7 +846,7 @@ class tasklist extends rcube_plugin if ($rec['flagged']) $mask |= self::FILTER_MASK_FLAGGED; - if ($rec['complete'] == 1.0) + if ($this->driver->is_complete($rec)) $mask |= self::FILTER_MASK_COMPLETE; if (empty($rec['date'])) diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index 6988a618..f6d4b3ad 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -67,7 +67,7 @@ class tasklist_ui { $this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists')); $this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select')); - $this->plugin->register_handler('plugin.category_select', array($this, 'category_select')); + $this->plugin->register_handler('plugin.status_select', array($this, 'status_select')); $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); $this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form')); $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview')); @@ -129,6 +129,21 @@ class tasklist_ui return html::tag('ul', $attrib, $li, html::$common_attrib); } + /** + * Render HTML form for task status selector + */ + function status_select($attrib = array()) + { + $attrib['name'] = 'status'; + $select = new html_select($attrib); + $select->add('---', ''); + $select->add($this->plugin->gettext('status-needs-action'), 'NEEDS-ACTION'); + $select->add($this->plugin->gettext('status-in-process'), 'IN-PROCESS'); + $select->add($this->plugin->gettext('status-completed'), 'COMPLETED'); + $select->add($this->plugin->gettext('status-cancelled'), 'CANCELLED'); + + return $select->show(null); + } /** * Render a HTML select box for list selection