Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256712
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
154 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index bcc994c2..8e56923d 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -1,619 +1,667 @@
<?php
/**
* Database driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class tasklist_database_driver extends tasklist_driver
{
public $undelete = true; // yes, we can
public $sortable = false;
public $alarm_types = array('DISPLAY','EMAIL');
private $rc;
private $plugin;
private $cache = array();
private $lists = array();
private $list_ids = '';
private $db_tasks = 'tasks';
private $db_lists = 'tasklists';
private $sequence_tasks = 'task_ids';
private $sequence_lists = 'tasklist_ids';
/**
* Default constructor
*/
public function __construct($plugin)
{
$this->rc = $plugin->rc;
$this->plugin = $plugin;
// read database config
$this->db_lists = $this->rc->config->get('db_table_lists', $this->db_lists);
$this->db_tasks = $this->rc->config->get('db_table_tasks', $this->db_tasks);
$this->sequence_lists = $this->rc->config->get('db_sequence_lists', $this->sequence_lists);
$this->sequence_tasks = $this->rc->config->get('db_sequence_tasks', $this->sequence_tasks);
$this->_read_lists();
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists()
{
$hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', '')));
if (!empty($this->rc->user->ID)) {
$list_ids = array();
$result = $this->rc->db->query(
"SELECT *, tasklist_id AS id FROM " . $this->db_lists . "
WHERE user_id=?
ORDER BY CASE WHEN name='INBOX' THEN 0 ELSE 1 END, name",
$this->rc->user->ID
);
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$arr['showalarms'] = intval($arr['showalarms']);
$arr['active'] = !in_array($arr['id'], $hidden);
$arr['editable'] = true;
$this->lists[$arr['id']] = $arr;
$list_ids[] = $this->rc->db->quote($arr['id']);
}
$this->list_ids = join(',', $list_ids);
}
}
/**
* Get a list of available tasks lists from this source
*/
public function get_lists()
{
// attempt to create a default list for this user
if (empty($this->lists)) {
if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
$this->_read_lists();
}
return $this->lists;
}
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* @return mixed ID of the new list on success, False on error
* @see tasklist_driver::create_list()
*/
public function create_list($prop)
{
$result = $this->rc->db->query(
"INSERT INTO " . $this->db_lists . "
(user_id, name, color, showalarms)
VALUES (?, ?, ?, ?)",
$this->rc->user->ID,
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0
);
if ($result)
return $this->rc->db->insert_id($this->sequence_lists);
return false;
}
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* @return boolean True on success, Fales on failure
* @see tasklist_driver::edit_list()
*/
public function edit_list($prop)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_lists . "
SET name=?, color=?, showalarms=?
WHERE tasklist_id=?
AND user_id=?",
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0,
$prop['id'],
$this->rc->user->ID
);
return $this->rc->db->affected_rows($query);
}
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* @return boolean True on success, Fales on failure
* @see tasklist_driver::subscribe_list()
*/
public function subscribe_list($prop)
{
$hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', '')));
if ($prop['active'])
unset($hidden[$prop['id']]);
else
$hidden[$prop['id']] = 1;
return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden))));
}
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* @return boolean True on success, Fales on failure
* @see tasklist_driver::remove_list()
*/
public function remove_list($prop)
{
$list_id = $prop['id'];
if ($this->lists[$list_id]) {
// delete all tasks linked with this list
$this->rc->db->query(
"DELETE FROM " . $this->db_tasks . "
WHERE tasklist_id=?",
$lisr_id
);
// delete list record
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_lists . "
WHERE tasklist_id=?
AND user_id=?",
$list_id,
$this->rc->user->ID
);
return $this->rc->db->affected_rows($query);
}
return false;
}
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|today|tomorrow|overdue|nodate)
* @see tasklist_driver::count_tasks()
*/
function count_tasks($lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
// only allow to select from lists of this user
$list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
$today_date = new DateTime('now', $this->plugin->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$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",
join(',', $list_ids)
));
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$counts['all']++;
if ($rec['flagged'])
$counts['flagged']++;
if (empty($rec['date']))
$counts['nodate']++;
else if ($rec['date'] == $today)
$counts['today']++;
else if ($rec['date'] == $tomorrow)
$counts['tomorrow']++;
else if ($rec['date'] < $today)
$counts['overdue']++;
}
return $counts;
}
/**
* Get all taks records matching the given filter
*
* @param array Hash array wiht filter criterias
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
* @see tasklist_driver::list_tasks()
*/
function list_tasks($filter, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
// only allow to select from lists of this user
$list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
$sql_add = '';
// add filter criteria
if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
$sql_add .= ' AND (date IS NULL OR date >= ?)';
$datefrom = $filter['from'];
}
if ($filter['to']) {
if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE)
$sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')';
else
$sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')';
}
// special case 'today': also show all events with date before today
if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) {
$datefrom = date('Y-m-d', 0);
}
if ($filter['mask'] & tasklist::FILTER_MASK_NODATE)
$sql_add = ' AND date IS NULL';
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
$sql_add .= ' AND complete=1';
else // don't show complete tasks by default
$sql_add .= ' AND complete<1';
if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED)
$sql_add .= ' AND flagged=1';
// compose (slow) SQL query for searching
// FIXME: improve searching using a dedicated col and normalized values
if ($filter['search']) {
$sql_query = array();
foreach (array('title','description','organizer','attendees') as $col)
$sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%');
$sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
}
$tasks = array();
if (!empty($list_ids)) {
$datecol = $this->rc->db->quote_identifier('date');
$timecol = $this->rc->db->quote_identifier('time');
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND del=0
%s
ORDER BY parent_id, task_id ASC",
join(',', $list_ids),
$sql_add
),
$datefrom
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$tasks[] = $this->_read_postprocess($rec);
}
}
return $tasks;
}
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
public function get_task($prop)
{
if (is_string($prop))
$prop['uid'] = $prop;
$query_col = $prop['id'] ? 'task_id' : 'uid';
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND %s=?
AND del=0",
$this->list_ids,
$query_col
),
$prop['id'] ? $prop['id'] : $prop['uid']
);
if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
return $this->_read_postprocess($rec);
}
return false;
}
+ /**
+ * Get all decendents of the given task record
+ *
+ * @param mixed Hash array with task properties or task UID
+ * @param boolean True if all childrens children should be fetched
+ * @return array List of all child task IDs
+ */
+ public function get_childs($prop, $recursive = false)
+ {
+ // resolve UID first
+ if (is_string($prop)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT task_id AS id, tasklist_id AS list FROM " . $this->db_tasks . "
+ WHERE tasklist_id IN (%s)
+ AND uid=?",
+ $this->list_ids
+ ),
+ $prop);
+ $prop = $this->rc->db->fetch_assoc($result);
+ }
+
+ $childs = array();
+ $task_ids = array($prop['id']);
+
+ // query for childs (recursively)
+ while (!empty($task_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT task_id AS id FROM " . $this->db_tasks . "
+ WHERE tasklist_id IN (%s)
+ AND parent_id IN (%s)
+ AND del=0",
+ $this->list_ids,
+ join(',', array_map(array($this->rc->db, 'quote'), $task_ids))
+ ));
+
+ $task_ids = array();
+ while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ $childs[] = $rec['id'];
+ $task_ids[] = $rec['id'];
+ }
+
+ if (!$recursive)
+ break;
+ }
+
+ return $childs;
+ }
+
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* @see tasklist_driver::pending_alarms()
*/
public function pending_alarms($time, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
// only allow to select from calendars with activated alarms
$list_ids = array();
foreach ($lists as $lid) {
if ($this->lists[$lid] && $this->lists[$lid]['showalarms'])
$list_ids[] = $lid;
}
$list_ids = array_map(array($this->rc->db, 'quote'), $list_ids);
$alarms = array();
if (!empty($list_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_tasks . "
WHERE tasklist_id IN (%s)
AND notify <= %s AND complete < 1",
join(',', $list_ids),
$this->rc->db->fromunixtime($time)
));
while ($result && ($rec = $this->rc->db->fetch_assoc($result)))
$alarms[] = $this->_read_postprocess($rec);
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see tasklist_driver::dismiss_alarm()
*/
public function dismiss_alarm($task_id, $snooze = 0)
{
// set new notifyat time or unset if not snoozed
$notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s, notify=?
WHERE task_id=?
AND tasklist_id IN (" . $this->list_ids . ")",
$this->rc->db->now()),
$notify_at,
$task_id
);
return $this->rc->db->affected_rows($query);
}
/**
* Map some internal database values to match the generic "API"
*/
private function _read_postprocess($rec)
{
$rec['id'] = $rec['task_id'];
$rec['list'] = $rec['tasklist_id'];
$rec['changed'] = new DateTime($rec['changed']);
$rec['tags'] = array_filter(explode(',', $rec['tags']));
if (!$rec['parent_id'])
unset($rec['parent_id']);
unset($rec['task_id'], $rec['tasklist_id'], $rec['created']);
return $rec;
}
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of this file)
* @return mixed New event ID on success, False on error
* @see tasklist_driver::create_task()
*/
public function create_task($prop)
{
// check list permissions
$list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists));
if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly'])
return false;
foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) {
if (empty($prop[$col]))
$prop[$col] = null;
}
$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, alarms, notify)
VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$this->rc->db->now(),
$this->rc->db->now()
),
$list_id,
$prop['uid'],
$prop['parent_id'],
$prop['title'],
$prop['date'],
$prop['time'],
$prop['startdate'],
$prop['starttime'],
strval($prop['description']),
join(',', (array)$prop['tags']),
$prop['alarms'],
$notify_at
);
if ($result)
return $this->rc->db->insert_id($this->sequence_tasks);
return false;
}
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties
* @return boolean True on success, False on error
* @see tasklist_driver::edit_task()
*/
public function edit_task($prop)
{
$sql_set = array();
foreach (array('title', 'description', 'flagged', 'complete') as $col) {
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') as $col) {
if (isset($prop[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col]));
}
if (isset($prop['tags']))
$sql_set[] = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop['tags']));
if (isset($prop['date']) || isset($prop['time']) || isset($prop['alarms'])) {
$notify_at = $this->_get_notification($prop);
$sql_set[] = $this->rc->db->quote_identifier('notify') . '=' . (empty($notify_at) ? 'NULL' : $this->rc->db->quote($notify_at));
}
// moved from another list
if ($prop['_fromlist'] && ($newlist = $prop['list'])) {
$sql_set[] = 'tasklist_id=' . $this->rc->db->quote($newlist);
}
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s %s
WHERE task_id=?
AND tasklist_id IN (%s)",
$this->rc->db->now(),
($sql_set ? ', ' . join(', ', $sql_set) : ''),
$this->list_ids
),
$prop['id']
);
return $this->rc->db->affected_rows($query);
}
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* @return boolean True on success, False on error
* @see tasklist_driver::move_task()
*/
public function move_task($prop)
{
return $this->edit_task($prop);
}
/**
* Remove a single task from the database
*
* @param array Hash array with task properties
* @param boolean Remove record irreversible
* @return boolean True on success, False on error
* @see tasklist_driver::delete_task()
*/
public function delete_task($prop, $force = true)
{
$task_id = $prop['id'];
if ($task_id && $force) {
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_tasks . "
WHERE task_id=?
AND tasklist_id IN (" . $this->list_ids . ")",
$task_id
);
}
else if ($task_id) {
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s, del=1
WHERE task_id=?
AND tasklist_id IN (%s)",
$this->rc->db->now(),
$this->list_ids
),
$task_id
);
}
return $this->rc->db->affected_rows($query);
}
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties
* @return boolean True on success, False on error
* @see tasklist_driver::undelete_task()
*/
public function undelete_task($prop)
{
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_tasks . "
SET changed=%s, del=0
WHERE task_id=?
AND tasklist_id IN (%s)",
$this->rc->db->now(),
$this->list_ids
),
$prop['id']
);
return $this->rc->db->affected_rows($query);
}
/**
* Compute absolute time to notify the user
*/
private function _get_notification($task)
{
if ($task['alarms'] && $task['complete'] < 1 || strpos($task['alarms'], '@') !== false) {
$alarm = calendarlibcalendaring::get_next_alarm($task);
if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
return date('Y-m-d H:i:s', $alarm['time']);
}
return null;
}
}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 60f9ccbb..b70a4a09 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -1,798 +1,847 @@
<?php
/**
* Kolab Groupware driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class tasklist_kolab_driver extends tasklist_driver
{
// features supported by the backend
public $alarms = false;
public $attachments = true;
public $undelete = false; // task undelete action
public $alarm_types = array('DISPLAY');
private $rc;
private $plugin;
private $lists;
private $folders = array();
private $tasks = array();
/**
* Default constructor
*/
public function __construct($plugin)
{
$this->rc = $plugin->rc;
$this->plugin = $plugin;
$this->_read_lists();
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists()
{
// already read sources
if (isset($this->lists))
return $this->lists;
// get all folders that have type "task"
$this->folders = kolab_storage::get_folders('task');
$this->lists = array();
// convert to UTF8 and sort
$names = array();
$default_folder = null;
foreach ($this->folders as $i => $folder) {
$names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
$this->folders[$folder->name] = $folder;
if ($folder->default)
$default_folder = $folder->name;
}
asort($names, SORT_LOCALE_STRING);
// put default folder (aka INBOX) on top of the list
if ($default_folder) {
$default_name = $names[$default_folder];
unset($names[$default_folder]);
$names = array_merge(array($default_folder => $default_name), $names);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$listnames = array();
$prefs = $this->rc->config->get('kolab_tasklists', array());
foreach ($names as $utf7name => $name) {
$folder = $this->folders[$utf7name];
$path_imap = explode($delim, $name);
$editname = array_pop($path_imap); // pop off raw name part
$path_imap = join($delim, $path_imap);
$name = kolab_storage::folder_displayname(kolab_storage::object_name($utf7name), $listnames);
if ($folder->get_owner() == $_SESSION['username']) {
$readonly = false;
$alarms = true;
}
else {
$alarms = false;
$readonly = true;
if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$readonly = false;
}
}
$list_id = kolab_storage::folder_id($utf7name);
$tasklist = array(
'id' => $list_id,
'name' => $name,
'editname' => $editname,
'color' => 'CC0000',
'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
'editable' => !$readonly,
'active' => $folder->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
'parentfolder' => $path_imap,
'default' => $folder->default,
'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
$this->lists[$tasklist['id']] = $tasklist;
$this->folders[$tasklist['id']] = $folder;
}
}
/**
* Get a list of available task lists from this source
*/
public function get_lists()
{
// attempt to create a default list for this user
if (empty($this->lists)) {
if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
$this->_read_lists();
}
return $this->lists;
}
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
public function create_list($prop)
{
$prop['type'] = 'task';
$prop['subscribed'] = kolab_storage::SERVERSIDE_SUBSCRIPTION; // subscribe to folder by default
$folder = kolab_storage::folder_update($prop);
if ($folder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
$id = kolab_storage::folder_id($folder);
$prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
if (isset($prop['showalarms']))
$prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_tasklists'][$id])
$this->rc->user->save_prefs($prefs);
return $id;
}
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
public function edit_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
$prop['oldname'] = $folder->name;
$prop['type'] = 'task';
$newfolder = kolab_storage::folder_update($prop);
if ($newfolder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
$id = kolab_storage::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
unset($prefs['kolab_tasklists'][$prop['id']]);
if (isset($prop['showalarms']))
$prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_tasklists'][$id])
$this->rc->user->save_prefs($prefs);
return $id;
}
return false;
}
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* @return boolean True on success, Fales on failure
*/
public function subscribe_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
return $folder->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION);
}
return false;
}
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
public function remove_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
if (kolab_storage::folder_delete($folder->name))
return true;
else
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
public function count_tasks($lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
$today_date = new DateTime('now', $this->plugin->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
foreach ($lists as $list_id) {
$folder = $this->folders[$list_id];
foreach ((array)$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
continue;
$counts['all']++;
if ($rec['flagged'])
$counts['flagged']++;
if (empty($rec['date']))
$counts['nodate']++;
else if ($rec['date'] == $today)
$counts['today']++;
else if ($rec['date'] == $tomorrow)
$counts['tomorrow']++;
else if ($rec['date'] < $today)
$counts['overdue']++;
}
}
return $counts;
}
/**
* Get all taks records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
public function list_tasks($filter, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
$results = array();
// query Kolab storage
$query = array();
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
$query[] = array('tags','~','x-complete');
else
$query[] = array('tags','!~','x-complete');
// full text search (only works with cache enabled)
if ($filter['search']) {
$search = mb_strtolower($filter['search']);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', '~', $word);
}
}
foreach ($lists as $list_id) {
$folder = $this->folders[$list_id];
foreach ((array)$folder->select($query) as $record) {
$task = $this->_to_rcube_task($record);
$task['list'] = $list_id;
// TODO: post-filter tasks returned from storage
$results[] = $task;
}
}
return $results;
}
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
public function get_task($prop)
{
- $id = is_array($prop) ? $prop['uid'] : $prop;
+ $id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop;
$list_id = is_array($prop) ? $prop['list'] : null;
$folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
// find task in the available folders
- foreach ($folders as $folder) {
+ foreach ($folders as $list_id => $folder) {
+ if (is_numeric($list_id))
+ continue;
if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
$this->tasks[$id] = $this->_to_rcube_task($object);
+ $this->tasks[$id]['list'] = $list_id;
break;
}
}
return $this->tasks[$id];
}
+ /**
+ * Get all decendents of the given task record
+ *
+ * @param mixed Hash array with task properties or task UID
+ * @param boolean True if all childrens children should be fetched
+ * @return array List of all child task IDs
+ */
+ public function get_childs($prop, $recursive = false)
+ {
+ if (is_string($prop)) {
+ $task = $this->get_task($prop);
+ $prop = array('id' => $task['id'], 'list' => $task['list']);
+ }
+
+ $childs = array();
+ $list_id = $prop['list'];
+ $task_ids = array($prop['id']);
+ $folder = $this->folders[$list_id];
+
+ // query for childs (recursively)
+ while ($folder && !empty($task_ids)) {
+ $query_ids = array();
+ foreach ($task_ids as $task_id) {
+ $query = array(array('tags','=','x-parent:' . $task_id));
+ foreach ((array)$folder->select($query) as $record) {
+ // don't rely on kolab_storage_folder filtering
+ if ($record['parent_id'] == $task_id) {
+ $childs[] = $record['uid'];
+ $query_ids[] = $record['uid'];
+ }
+ }
+ }
+
+ if (!$recursive)
+ break;
+
+ $task_ids = $query_ids;
+ }
+
+ return $childs;
+ }
+
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* @see tasklist_driver::pending_alarms()
*/
public function pending_alarms($time, $lists = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->session->get_keep_alive());
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return array();
if ($lists && is_string($lists))
$lists = explode(',', $lists);
$time = $slot + $interval;
$tasks = array();
$query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete'));
foreach ($this->lists as $lid => $list) {
// skip lists with alarms disabled
if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
continue;
$folder = $this->folders[$lid];
foreach ((array)$folder->select($query) as $record) {
if (!$record['alarms']) // don't trust query :-)
continue;
$task = $this->_to_rcube_task($record);
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($task, 'task');
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
$id = $task['id'];
$tasks[$id] = $task;
$tasks[$id]['notifyat'] = $alarm['time'];
}
}
}
// get alarm information stored in local database
if (!empty($tasks)) {
$task_ids = array_map(array($this->rc->db, 'quote'), array_keys($tasks));
$result = $this->rc->db->query(sprintf(
"SELECT * FROM kolab_alarms
WHERE event_id IN (%s) AND user_id=?",
join(',', $task_ids),
$this->rc->db->now()
),
$this->rc->user->ID
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$dbdata[$rec['event_id']] = $rec;
}
}
$alarms = array();
foreach ($tasks as $id => $task) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat'];
if ($notifyat <= $time)
$alarms[] = $task;
}
return $alarms;
}
/**
* (User) feedback after showing an alarm notification
* This should mark the alarm as 'shown' or snooze it for the given amount of time
*
* @param string Task identifier
* @param integer Suspend the alarm for this number of seconds
*/
public function dismiss_alarm($id, $snooze = 0)
{
// delete old alarm entry
$this->rc->db->query(
"DELETE FROM kolab_alarms
WHERE event_id=? AND user_id=?",
$id,
$this->rc->user->ID
);
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(
"INSERT INTO kolab_alarms
(event_id, user_id, dismissed, notifyat)
VALUES(?, ?, ?, ?)",
$id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_task($record)
{
$task = array(
'id' => $record['uid'],
'uid' => $record['uid'],
'title' => $record['title'],
# 'location' => $record['location'],
'description' => $record['description'],
'tags' => (array)$record['categories'],
'flagged' => $record['priority'] == 1,
'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100),
'parent_id' => $record['parent_id'],
);
// convert from DateTime to internal date format
if (is_a($record['due'], 'DateTime')) {
$task['date'] = $record['due']->format('Y-m-d');
$task['time'] = $record['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');
$task['starttime'] = $record['start']->format('h:i');
}
if (is_a($record['dtstamp'], 'DateTime')) {
$task['changed'] = $record['dtstamp'];
}
if ($record['alarms']) {
$task['alarms'] = $record['alarms'];
}
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
$attachments[] = $attachment;
}
}
$task['attachments'] = $attachments;
}
return $task;
}
/**
* Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_task($task, $old = array())
{
$object = $task;
$object['categories'] = (array)$task['tags'];
if (!empty($task['date'])) {
$object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
if (empty($task['time']))
$object['due']->_dateonly = true;
unset($object['date']);
}
if (!empty($task['startdate'])) {
$object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
if (empty($task['starttime']))
$object['start']->_dateonly = true;
unset($object['startdate']);
}
$object['complete'] = $task['complete'] * 100;
if ($task['complete'] == 1.0)
$object['status'] = 'COMPLETED';
if ($task['flagged'])
$object['priority'] = 1;
else
$object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0;
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// delete existing attachment(s)
if (!empty($task['deleted_attachments'])) {
foreach ($task['deleted_attachments'] as $attachment) {
if (is_array($object['_attachments'])) {
foreach ($object['_attachments'] as $idx => $att) {
if ($att['id'] == $attachment)
$object['_attachments'][$idx] = false;
}
}
}
unset($task['deleted_attachments']);
}
// in kolab_storage attachments are indexed by content-id
if (is_array($task['attachments'])) {
foreach ($task['attachments'] as $idx => $attachment) {
$key = null;
// Roundcube ID has nothing to do with the storage ID, remove it
if ($attachment['content']) {
unset($attachment['id']);
}
else {
foreach ((array)$old['_attachments'] as $cid => $oldatt) {
if ($oldatt && $attachment['id'] == $oldatt['id'])
$key = $cid;
}
}
// replace existing entry
if ($key) {
$object['_attachments'][$key] = $attachment;
}
// append as new attachment
else {
$object['_attachments'][] = $attachment;
}
}
unset($object['attachments']);
}
unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
return $object;
}
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return mixed New task ID on success, False on error
*/
public function create_task($task)
{
return $this->edit_task($task);
}
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return boolean True on success, False on error
*/
public function edit_task($task)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
// moved from another folder
if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
- if (!$fromfolder->move($task['uid'], $folder->name))
+ if (!$fromfolder->move($task['id'], $folder->name))
return false;
unset($task['_fromlist']);
}
// load previous version of this task to merge
if ($task['id']) {
- $old = $folder->get_object($task['uid']);
+ $old = $folder->get_object($task['id']);
if (!$old || PEAR::isError($old))
return false;
+
+ // merge existing properties if the update isn't complete
+ if (!isset($task['title']) || !isset($task['complete']))
+ $task += $this->_to_rcube_task($old);
}
// generate new task object from RC input
$object = $this->_from_rcube_task($task, $old);
$saved = $folder->save($object, 'task', $task['id']);
if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving task object to Kolab server"),
true, false);
$saved = false;
}
else {
$task = $this->_to_rcube_task($object);
$task['list'] = $list_id;
- $this->tasks[$task['uid']] = $task;
+ $this->tasks[$task['id']] = $task;
}
return $saved;
}
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* @return boolean True on success, False on error
* @see tasklist_driver::move_task()
*/
public function move_task($task)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
// execute move command
if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
return $fromfolder->move($task['id'], $folder->name);
}
return false;
}
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
public function delete_task($task, $force = true)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
- return $folder->delete($task['uid']);
+ return $folder->delete($task['id']);
}
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
// TODO: implement this
return false;
}
/**
* Get attachment properties
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function get_attachment($id, $task)
{
$task['uid'] = $task['id'];
$task = $this->get_task($task);
if ($task && !empty($task['attachments'])) {
foreach ($task['attachments'] as $att) {
if ($att['id'] == $id)
return $att;
}
}
return null;
}
/**
* Get attachment body
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return string Attachment body
*/
public function get_attachment_body($id, $task)
{
if ($storage = $this->folders[$task['list']]) {
return $storage->get_attachment($task['id'], $id);
}
return false;
}
/**
*
*/
public function tasklist_edit_form($fieldprop)
{
$select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), null);
$fieldprop['parent'] = array(
'id' => 'taskedit-parentfolder',
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show(''),
);
$formfields = array();
foreach (array('name','parent','showalarms') as $f) {
$formfields[$f] = $fieldprop[$f];
}
return parent::tasklist_edit_form($formfields);
}
}
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 4e987f67..f8107996 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -1,268 +1,277 @@
<?php
/**
* Driver interface for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Struct of an internal task object how it is passed from/to the driver classes:
*
* $task = array(
* 'id' => 'Task ID used for editing', // must be unique for the current user
* 'parent_id' => 'ID of parent task', // null if top-level task
* 'uid' => 'Unique identifier of this task',
* 'list' => 'Task list identifier to add the task to or where the task is stored',
* 'changed' => <DateTime>, // Last modification date/time of the record
* 'title' => 'Event title/summary',
* 'description' => 'Event description',
* 'tags' => array(), // List of tags for this task
* 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set
* 'time' => 'Due time', // as string of format hh::ii or null if no due time is set
* 'startdate' => 'Start date' // Delay start of the task until that date
* 'starttime' => 'Start time' // ...and time
* '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)
* 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before due time)
* '_fromlist' => 'List identifier where the task was stored before',
* );
*/
/**
* Driver interface for the Tasklist plugin
*/
abstract class tasklist_driver
{
// features supported by the backend
public $alarms = false;
public $attachments = false;
public $undelete = false; // task undelete action
public $sortable = false;
public $alarm_types = array('DISPLAY');
public $last_error;
/**
* Get a list of available task lists from this source
*/
abstract function get_lists();
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
abstract function create_list($prop);
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
abstract function edit_list($prop);
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* @return boolean True on success, Fales on failure
*/
abstract function subscribe_list($prop);
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
abstract function remove_list($prop);
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
abstract function count_tasks($lists = null);
/**
* Get all taks records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
abstract function list_tasks($filter, $lists = null);
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* id: Task identifier
* uid: Unique identifier of this task
* date: Task due date
* time: Task due time
* title: Task title/summary
*/
abstract function pending_alarms($time, $lists = null);
/**
* (User) feedback after showing an alarm notification
* This should mark the alarm as 'shown' or snooze it for the given amount of time
*
* @param string Task identifier
* @param integer Suspend the alarm for this number of seconds
*/
abstract function dismiss_alarm($id, $snooze = 0);
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
abstract public function get_task($prop);
+ /**
+ * Get decendents of the given task record
+ *
+ * @param mixed Hash array with task properties or task UID
+ * @param boolean True if all childrens children should be fetched
+ * @return array List of all child task IDs
+ */
+ abstract public function get_childs($prop, $recursive = false);
+
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of this file)
* @return mixed New event ID on success, False on error
*/
abstract function create_task($prop);
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of this file)
* @return boolean True on success, False on error
*/
abstract function edit_task($prop);
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* id: Task identifier
* list: New list identifier to move to
* _fromlist: Previous list identifier
* @return boolean True on success, False on error
*/
abstract function move_task($prop);
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
abstract function delete_task($prop, $force = true);
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
return false;
}
/**
* Get attachment properties
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function get_attachment($id, $task) { }
/**
* Get attachment body
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
*
* @return string Attachment body
*/
public function get_attachment_body($id, $task) { }
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
$rcmail = rcmail::get_instance();
return $rcmail->config->get('tasklist_categories', array());
}
/**
* Build the edit/create form for lists.
* This gives the drivers the opportunity to add more list properties
*
* @param array List with form fields to be rendered
* @return string HTML content of the form
*/
public function tasklist_edit_form($formfields)
{
$html = '';
foreach ($formfields as $prop => $field) {
$html .= html::div('form-section',
html::label($field['id'], $field['label']) .
$field['value']);
}
return $html;
}
}
diff --git a/plugins/tasklist/localization/de_CH.inc b/plugins/tasklist/localization/de_CH.inc
index ac060951..4fbb9768 100644
--- a/plugins/tasklist/localization/de_CH.inc
+++ b/plugins/tasklist/localization/de_CH.inc
@@ -1,63 +1,69 @@
<?php
$labels = array();
$labels['navtitle'] = 'Aufgaben';
$labels['lists'] = 'Aufgabenlisten';
$labels['list'] = 'Liste';
$labels['tags'] = 'Tags';
$labels['newtask'] = 'Neue Aufgabe';
$labels['createnewtask'] = 'Neue Aufgabe eingeben (z.B. Samstag, Rasenmähen)';
$labels['createfrommail'] = 'Als Aufgabe speichern';
$labels['mark'] = 'Markieren';
$labels['unmark'] = 'Markierung aufheben';
$labels['edit'] = 'Bearbeiten';
$labels['delete'] = 'Löschen';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschreibung';
$labels['datetime'] = 'Datum/Zeit';
$labels['start'] = 'Beginn';
$labels['alarms'] = 'Erinnerung';
$labels['all'] = 'Alle';
$labels['flagged'] = 'Markiert';
$labels['complete'] = 'Erledigt';
$labels['overdue'] = 'Überfällig';
$labels['today'] = 'Heute';
$labels['tomorrow'] = 'Morgen';
$labels['next7days'] = 'Nächste 7 Tage';
$labels['later'] = 'Später';
$labels['nodate'] = 'kein Datum';
$labels['removetag'] = 'Löschen';
$labels['taskdetails'] = 'Details';
$labels['newtask'] = 'Neue Aufgabe';
$labels['edittask'] = 'Aufgabe bearbeiten';
$labels['save'] = 'Speichern';
$labels['cancel'] = 'Abbrechen';
$labels['addsubtask'] = 'Neue Teilaufgabe';
+$labels['deletetask'] = 'Aufgabe löschen';
+$labels['deletethisonly'] = 'Nur diese Aufgabe löschen';
+$labels['deletewithchilds'] = 'Mit allen Teilaufhaben löschen';
+
$labels['tabsummary'] = 'Übersicht';
$labels['tabrecurrence'] = 'Wiederholung';
$labels['tabattachments'] = 'Anhänge';
$labels['tabsharing'] = 'Freigabe';
$labels['editlist'] = 'Liste bearbeiten';
$labels['createlist'] = 'Neue Liste';
$labels['listactions'] = 'Listenoptionen...';
$labels['listname'] = 'Name';
$labels['showalarms'] = 'Erinnerungen anzeigen';
$labels['import'] = 'Importieren';
// date words
$labels['on'] = 'am';
$labels['at'] = 'um';
$labels['this'] = 'diesen';
$labels['next'] = 'nächsten';
// mesages
$labels['savingdata'] = 'Daten werden gespeichert...';
$labels['errorsaving'] = 'Fehler beim Speichern.';
$labels['notasksfound'] = 'Für die aktuellen Kriterien wurden keine Aufgaben gefunden.';
$labels['invalidstartduedates'] = 'Beginn der Aufgabe darf nicht grösser als das Enddatum sein.';
+$labels['deletetasktconfirm'] = 'Möchten Sie diese Aufgabe wirklich löschen?';
+$labels['deleteparenttasktconfirm'] = 'Möchten Sie diese Aufgabe inklusive aller Teilaufgaben wirklich löschen?';
$labels['deletelistconfirm'] = 'Möchten Sie diese Liste mit allen Aufgaben wirklich löschen?';
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index c6575165..7d5415ab 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -1,63 +1,68 @@
<?php
$labels = array();
$labels['navtitle'] = 'Tasks';
$labels['lists'] = 'Tasklists';
$labels['list'] = 'Tasklist';
$labels['tags'] = 'Tags';
$labels['newtask'] = 'New Task';
$labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';
$labels['createfrommail'] = 'Save as task';
$labels['mark'] = 'Mark';
$labels['unmark'] = 'Unmark';
$labels['edit'] = 'Edit';
$labels['delete'] = 'Delete';
$labels['title'] = 'Title';
$labels['description'] = 'Description';
$labels['datetime'] = 'Date/Time';
$labels['start'] = 'Start';
$labels['alarms'] = 'Reminder';
$labels['all'] = 'All';
$labels['flagged'] = 'Flagged';
$labels['complete'] = 'Complete';
$labels['overdue'] = 'Overdue';
$labels['today'] = 'Today';
$labels['tomorrow'] = 'Tomorrow';
$labels['next7days'] = 'Next 7 days';
$labels['later'] = 'Later';
$labels['nodate'] = 'no date';
$labels['removetag'] = 'Remove';
$labels['taskdetails'] = 'Details';
$labels['newtask'] = 'New Task';
$labels['edittask'] = 'Edit Task';
$labels['save'] = 'Save';
$labels['cancel'] = 'Cancel';
$labels['addsubtask'] = 'Add subtask';
+$labels['deletetask'] = 'Delete task';
+$labels['deletethisonly'] = 'Delete this task only';
+$labels['deletewithchilds'] = 'Delete with all subtasks';
$labels['tabsummary'] = 'Summary';
$labels['tabrecurrence'] = 'Recurrence';
$labels['tabattachments'] = 'Attachments';
$labels['tabsharing'] = 'Sharing';
$labels['editlist'] = 'Edit list';
$labels['createlist'] = 'Add list';
$labels['listactions'] = 'List options...';
$labels['listname'] = 'Name';
$labels['showalarms'] = 'Show alarms';
$labels['import'] = 'Import';
// date words
$labels['on'] = 'on';
$labels['at'] = 'at';
$labels['this'] = 'this';
$labels['next'] = 'next';
// mesages
$labels['savingdata'] = 'Saving data...';
$labels['errorsaving'] = 'Failed to save data.';
$labels['notasksfound'] = 'No tasks found for the given criteria';
$labels['invalidstartduedates'] = 'Start date must not be greater than due date.';
+$labels['deletetasktconfirm'] = 'Do you really want to delete this task?';
+$labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task and all its subtasks?';
$labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 2e885e6f..56f29cdc 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1,1545 +1,1606 @@
/**
* Client scripts for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function rcube_tasklist_ui(settings)
{
// extend base class
rcube_libcalendaring.call(this, settings);
/* constants */
var FILTER_MASK_ALL = 0;
var FILTER_MASK_TODAY = 1;
var FILTER_MASK_TOMORROW = 2;
var FILTER_MASK_WEEK = 4;
var FILTER_MASK_LATER = 8;
var FILTER_MASK_NODATE = 16;
var FILTER_MASK_OVERDUE = 32;
var FILTER_MASK_FLAGGED = 64;
var FILTER_MASK_COMPLETE = 128;
var filter_masks = {
all: FILTER_MASK_ALL,
today: FILTER_MASK_TODAY,
tomorrow: FILTER_MASK_TOMORROW,
week: FILTER_MASK_WEEK,
later: FILTER_MASK_LATER,
nodate: FILTER_MASK_NODATE,
overdue: FILTER_MASK_OVERDUE,
flagged: FILTER_MASK_FLAGGED,
complete: FILTER_MASK_COMPLETE
};
/* private vars */
var selector = 'all';
var tagsfilter = [];
var filtermask = FILTER_MASK_ALL;
var loadstate = { filter:-1, lists:'', search:null };
var idcount = 0;
var saving_lock;
var ui_loading;
var taskcounts = {};
var listindex = [];
var listdata = {};
var tags = [];
var draghelper;
var search_request;
var search_query;
var completeness_slider;
var me = this;
// general datepicker settings
var datepicker_settings = {
// translate from PHP format to datepicker format
dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings['first_day'],
// dayNamesMin: settings['days_short'],
// monthNames: settings['months'],
// monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
var extended_datepicker_settings;
/* public members */
this.tasklists = rcmail.env.tasklists;
this.selected_task;
this.selected_list;
/* public methods */
this.init = init;
this.edit_task = task_edit_dialog;
this.delete_task = delete_task;
this.add_childtask = add_childtask;
this.quicksearch = quicksearch;
this.reset_search = reset_search;
this.list_remove = list_remove;
this.list_edit_dialog = list_edit_dialog;
this.unlock_saving = unlock_saving;
/* imports */
var Q = this.quote_html;
var text2html = this.text2html;
var event_date_text = this.event_date_text;
var format_datetime = this.format_datetime;
var parse_datetime = this.parse_datetime;
var date2unixtime = this.date2unixtime;
var fromunixtime = this.fromunixtime;
var init_alarms_edit = this.init_alarms_edit;
/**
* initialize the tasks UI
*/
function init()
{
// initialize task list selectors
for (var id in me.tasklists) {
if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
init_tasklist_li(li, id);
}
if (me.tasklists[id].editable && !me.selected_list) {
me.selected_list = id;
rcmail.enable_command('addtask', true);
$(li).click();
}
}
// register server callbacks
rcmail.addEventListener('plugin.data_ready', data_ready);
rcmail.addEventListener('plugin.refresh_task', update_taskitem);
rcmail.addEventListener('plugin.update_counts', update_counts);
rcmail.addEventListener('plugin.insert_tasklist', insert_list);
rcmail.addEventListener('plugin.update_tasklist', update_list);
rcmail.addEventListener('plugin.destroy_tasklist', destroy_list);
rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); });
rcmail.addEventListener('plugin.unlock_saving', unlock_saving);
// start loading tasks
fetch_counts();
list_tasks();
// register event handlers for UI elements
$('#taskselector a').click(function(e){
if (!$(this).parent().hasClass('inactive'))
list_tasks(this.href.replace(/^.*#/, ''));
return false;
});
// quick-add a task
$(rcmail.gui_objects.quickaddform).submit(function(e){
var tasktext = this.elements.text.value,
rec = { id:-(++idcount), title:tasktext, readonly:true, mask:0, complete:0 };
save_task({ tempid:rec.id, raw:tasktext, list:me.selected_list }, 'new');
render_task(rec);
$('#listmessagebox').hide();
// clear form
this.reset();
return false;
});
// click-handler on tags list
$(rcmail.gui_objects.tagslist).click(function(e){
if (e.target.nodeName != 'LI')
return false;
var item = $(e.target),
tag = item.data('value');
// reset selection on regular clicks
var index = tagsfilter.indexOf(tag);
var shift = e.shiftKey || e.ctrlKey || e.metaKey;
if (!shift) {
if (tagsfilter.length > 1)
index = -1;
$('li', this).removeClass('selected');
tagsfilter = [];
}
// add tag to filter
if (index < 0) {
item.addClass('selected');
tagsfilter.push(tag);
}
else if (shift) {
item.removeClass('selected');
var a = tagsfilter.slice(0,index);
tagsfilter = a.concat(tagsfilter.slice(index+1));
}
list_tasks();
e.preventDefault();
return false;
})
.mousedown(function(e){
// disable content selection with the mouse
e.preventDefault();
return false;
});
// click-handler on task list items (delegate)
$(rcmail.gui_objects.resultlist).click(function(e){
var item = $(e.target);
var className = e.target.className;
if (item.hasClass('childtoggle')) {
item = item.parent().find('.taskhead');
className = 'childtoggle';
}
else if (!item.hasClass('taskhead'))
item = item.closest('div.taskhead');
// ignore
if (!item.length)
return;
var id = item.data('id'),
li = item.parent(),
rec = listdata[id];
switch (className) {
case 'childtoggle':
rec.collapsed = !rec.collapsed;
li.children('.childtasks:first').toggle();
$(e.target).toggleClass('collapsed').html(rec.collapsed ? '▶' : '▼');
rcmail.http_post('tasks/task', { action:'collapse', t:{ id:rec.id, list:rec.list }, collapsed:rec.collapsed?1:0 });
break;
case 'complete':
rec.complete = e.target.checked ? 1 : 0;
li.toggleClass('complete');
save_task(rec, 'edit');
return true;
case 'flagged':
rec.flagged = rec.flagged ? 0 : 1;
li.toggleClass('flagged');
save_task(rec, 'edit');
break;
case 'date':
var link = $(e.target).html(''),
input = $('<input type="text" size="10" />').appendTo(link).val(rec.date || '')
input.datepicker($.extend({
onClose: function(dateText, inst) {
if (dateText != rec.date) {
rec.date = dateText;
save_task(rec, 'edit');
}
input.datepicker('destroy').remove();
link.html(dateText || rcmail.gettext('nodate','tasklist'));
},
}, extended_datepicker_settings)
)
.datepicker('setDate', rec.date)
.datepicker('show');
break;
case 'delete':
delete_task(id);
break;
case 'actions':
var pos, ref = $(e.target),
menu = $('#taskitemmenu');
if (menu.is(':visible') && menu.data('refid') == id) {
menu.hide();
}
else {
pos = ref.offset();
pos.top += ref.outerHeight();
pos.left += ref.width() - menu.outerWidth();
menu.css({ top:pos.top+'px', left:pos.left+'px' }).show();
menu.data('refid', id);
me.selected_task = rec;
}
e.bubble = false;
break;
default:
if (e.target.nodeName != 'INPUT')
task_show_dialog(id);
break;
}
return false;
})
.dblclick(function(e){
var id, rec, item = $(e.target);
if (!item.hasClass('taskhead'))
item = item.closest('div.taskhead');
if (item.length && (id = item.data('id')) && (rec = listdata[id])) {
var list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
if (rec.readonly || !list.editable)
task_show_dialog(id);
else
task_edit_dialog(id, 'edit');
clearSelection();
}
});
// handle global document clicks: close popup menus
$(document.body).click(clear_popups);
// extended datepicker settings
var extended_datepicker_settings = $.extend({
showButtonPanel: true,
beforeShow: function(input, inst) {
setTimeout(function(){
$(input).datepicker('widget').find('button.ui-datepicker-close')
.html(rcmail.gettext('nodate','tasklist'))
.attr('onclick', '')
.click(function(e){
$(input).datepicker('setDate', null).datepicker('hide');
});
}, 1);
},
}, datepicker_settings);
}
/**
* initialize task edit form elements
*/
function init_taskedit()
{
$('#taskedit').tabs();
completeness_slider = $('#taskedit-completeness-slider').slider({
range: 'min',
slide: function(e, ui){
var v = completeness_slider.slider('value');
if (v >= 98) v = 100;
if (v <= 2) v = 0;
$('#taskedit-completeness').val(v);
}
});
$('#taskedit-completeness').change(function(e){
completeness_slider.slider('value', parseInt(this.value))
});
// register events on alarm fields
init_alarms_edit('#taskedit');
$('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings);
$('a.edit-nodate').click(function(){
var sel = $(this).attr('rel');
if (sel) $(sel).val('');
return false;
});
}
/**
* Request counts from the server
*/
function fetch_counts()
{
var active = active_lists();
if (active.length)
rcmail.http_request('counts', { lists:active.join(',') });
else
update_counts({});
}
/**
* List tasks matching the given selector
*/
function list_tasks(sel)
{
if (rcmail.busy)
return;
if (sel && filter_masks[sel] !== undefined) {
filtermask = filter_masks[sel];
selector = sel;
}
var active = active_lists(),
basefilter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL,
reload = active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query;
if (active.length && reload) {
ui_loading = rcmail.set_busy(true, 'loading');
rcmail.http_request('fetch', { filter:basefilter, lists:active.join(','), q:search_query }, true);
}
else if (reload)
data_ready({ data:[], lists:'', filter:basefilter, search:search_query });
else
render_tasklist();
$('#taskselector li.selected').removeClass('selected');
$('#taskselector li.'+selector).addClass('selected');
}
/**
* Remove all tasks of the given list from the UI
*/
function remove_tasks(list_id)
{
// remove all tasks of the given list from index
var newindex = $.grep(listindex, function(id, i){
return listdata[id] && listdata[id].list != list_id;
});
listindex = newindex;
render_tasklist();
// avoid reloading
me.tasklists[list_id].active = false;
loadstate.lists = active_lists();
}
/**
* Callback if task data from server is ready
*/
function data_ready(response)
{
listdata = {};
listindex = [];
loadstate.lists = response.lists;
loadstate.filter = response.filter;
loadstate.search = response.search;
for (var id, i=0; i < response.data.length; i++) {
id = response.data[i].id;
listindex.push(id);
listdata[id] = response.data[i];
listdata[id].children = [];
// register a forward-pointer to child tasks
if (listdata[id].parent_id && listdata[listdata[id].parent_id])
listdata[listdata[id].parent_id].children.push(id);
}
render_tasklist();
append_tags(response.tags || []);
rcmail.set_busy(false, 'loading', ui_loading);
}
/**
*
*/
function render_tasklist()
{
// clear display
var id, rec,
count = 0,
msgbox = $('#listmessagebox').hide(),
list = $(rcmail.gui_objects.resultlist).html('');
for (var i=0; i < listindex.length; i++) {
id = listindex[i];
rec = listdata[id];
if (match_filter(rec)) {
render_task(rec);
count++;
}
}
fix_tree_toggles();
if (!count)
msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
}
/**
* Show/hide child toggle buttons on all visible task items
*/
function fix_tree_toggles()
{
$('.taskitem', rcmail.gui_objects.resultlist).each(function(i,elem){
var li = $(elem),
rec = listdata[li.attr('rel')],
childs = $('.childtasks li', li);
$('.childtoggle', li)[(childs.length ? 'show' : 'hide')]();
})
}
/**
*
*/
function append_tags(taglist)
{
// find new tags
var newtags = [];
for (var i=0; i < taglist.length; i++) {
if (tags.indexOf(taglist[i]) < 0)
newtags.push(taglist[i]);
}
tags = tags.concat(newtags);
// append new tags to tag cloud
$.each(newtags, function(i, tag){
$('<li>').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(rcmail.gui_objects.tagslist);
});
// re-sort tags list
$(rcmail.gui_objects.tagslist).children('li').sortElements(function(a,b){
return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1;
});
}
/**
*
*/
function update_counts(counts)
{
// got new data
if (counts)
taskcounts = counts;
// iterate over all selector links and update counts
$('#taskselector a').each(function(i, elem){
var link = $(elem),
f = link.parent().attr('class').replace(/\s\w+/, '');
if (f != 'all')
link.children('span').html(taskcounts[f] || '')[(taskcounts[f] ? 'show' : 'hide')]();
});
// spacial case: overdue
$('#taskselector li.overdue')[(taskcounts.overdue ? 'removeClass' : 'addClass')]('inactive');
}
/**
* Callback from server to update a single task item
*/
function update_taskitem(rec)
{
// handle a list of task records
if ($.isArray(rec)) {
$.each(rec, function(i,r){ update_taskitem(r); });
return;
}
var id = rec.id,
oldid = rec.tempid || id,
oldindex = listindex.indexOf(oldid),
list = me.tasklists[rec.list];
if (oldindex >= 0)
listindex[oldindex] = id;
else
listindex.push(id);
listdata[id] = rec;
if (list.active)
render_task(rec, oldid);
else
$('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).remove();
append_tags(rec.tags || []);
fix_tree_toggles();
}
/**
* Submit the given (changed) task record to the server
*/
function save_task(rec, action)
{
if (!rcmail.busy) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
return true;
}
return false;
}
/**
* Remove saving lock and free the UI for new input
*/
function unlock_saving()
{
if (saving_lock)
rcmail.set_busy(false, null, saving_lock);
}
/**
* Render the given task into the tasks list
*/
function render_task(rec, replace)
{
var tags_html = '';
for (var j=0; rec.tags && j < rec.tags.length; j++)
tags_html += '<span class="tag">' + Q(rec.tags[j]) + '</span>';
var div = $('<div>').addClass('taskhead').html(
'<div class="progressbar"><div class="progressvalue" style="width:' + (rec.complete * 100) + '%"></div></div>' +
'<input type="checkbox" name="completed[]" value="1" class="complete" ' + (rec.complete == 1.0 ? 'checked="checked" ' : '') + '/>' +
'<span class="flagged"></span>' +
'<span class="title">' + Q(rec.title) + '</span>' +
'<span class="tags">' + tags_html + '</span>' +
'<span class="date">' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '</span>' +
'<a href="#" class="actions">V</a>'
)
.data('id', rec.id)
.draggable({
revert: 'invalid',
addClasses: false,
cursorAt: { left:-10, top:12 },
helper: draggable_helper,
appendTo: 'body',
start: draggable_start,
stop: draggable_stop,
revertDuration: 300
});
if (rec.complete == 1.0)
div.addClass('complete');
if (rec.flagged)
div.addClass('flagged');
if (!rec.date)
div.addClass('nodate');
if ((rec.mask & FILTER_MASK_OVERDUE))
div.addClass('overdue');
var li, inplace = false, parent = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : null;
if (replace && (li = $('li[rel="'+replace+'"]', rcmail.gui_objects.resultlist)) && li.length) {
li.children('div.taskhead').first().replaceWith(div);
li.attr('rel', rec.id);
inplace = true;
}
else {
li = $('<li>')
.attr('rel', rec.id)
.addClass('taskitem')
.append((rec.collapsed ? '<span class="childtoggle collapsed">▶' : '<span class="childtoggle expanded">▼') + '</span>')
.append(div)
.append('<ul class="childtasks" style="' + (rec.collapsed ? 'display:none' : '') + '"></ul>');
if (!parent || !parent.length)
li.appendTo(rcmail.gui_objects.resultlist);
}
if (!inplace && parent && parent.length)
li.appendTo(parent);
if (replace) {
resort_task(rec, li, true);
// TODO: remove the item after a while if it doesn't match the current filter anymore
}
}
/**
* Move the given task item to the right place in the list
*/
function resort_task(rec, li, animated)
{
var dir = 0, index, slice, next_li, next_id, next_rec;
// animated moving
var insert_animated = function(li, before, after) {
if (before && li.next().get(0) == before.get(0))
return; // nothing to do
else if (after && li.prev().get(0) == after.get(0))
return; // nothing to do
var speed = 300;
li.slideUp(speed, function(){
if (before) li.insertBefore(before);
else if (after) li.insertAfter(after);
li.slideDown(speed);
});
}
// remove from list index
var oldlist = listindex.join('%%%');
var oldindex = listindex.indexOf(rec.id);
if (oldindex >= 0) {
slice = listindex.slice(0,oldindex);
listindex = slice.concat(listindex.slice(oldindex+1));
}
// find the right place to insert the task item
li.siblings().each(function(i, elem){
next_li = $(elem);
next_id = next_li.attr('rel');
next_rec = listdata[next_id];
if (next_id == rec.id) {
next_li = null;
return 1; // continue
}
if (next_rec && task_cmp(rec, next_rec) > 0) {
return 1; // continue;
}
else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) {
if (animated) insert_animated(li, next_li);
else li.insertBefore(next_li);
next_li = null;
return false;
}
});
index = listindex.indexOf(next_id);
if (next_li) {
if (animated) insert_animated(li, null, next_li);
else li.insertAfter(next_li);
index++;
}
// insert into list index
if (next_id && index >= 0) {
slice = listindex.slice(0,index);
slice.push(rec.id);
listindex = slice.concat(listindex.slice(index));
}
else { // restore old list index
listindex = oldlist.split('%%%');
}
}
/**
* Compare function of two task records.
* (used for sorting)
*/
function task_cmp(a, b)
{
var d = Math.floor(a.complete) - Math.floor(b.complete);
if (!d) d = (b._hasdate-0) - (a._hasdate-0);
if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999);
return d;
}
/**
*
*/
function get_all_childs(id)
{
var cid, childs = [];
for (var i=0; listdata[id].children && i < listdata[id].children.length; i++) {
cid = listdata[id].children[i];
childs.push(cid);
childs = childs.concat(get_all_childs(cid));
}
return childs;
}
/* Helper functions for drag & drop functionality */
function draggable_helper()
{
if (!draghelper)
draghelper = $('<div class="taskitem-draghelper">✔</div>');
return draghelper;
}
function draggable_start(event, ui)
{
$('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({
hoverClass: 'droptarget',
accept: droppable_accept,
drop: draggable_dropped,
addClasses: false
});
$(this).parent().addClass('dragging');
$('#rootdroppable').show();
}
function draggable_stop(event, ui)
{
$(this).parent().removeClass('dragging');
$('#rootdroppable').hide();
}
function droppable_accept(draggable)
{
var drag_id = draggable.data('id'),
drop_id = $(this).data('id'),
drag_rec = listdata[drag_id] || {},
drop_rec = listdata[drop_id];
// drop target is another list
if (drag_rec && $(this).data('type') == 'tasklist') {
var drop_list = me.tasklists[drop_id],
from_list = me.tasklists[drag_rec.list];
return !drag_rec.parent_id && drop_id != drag_rec.list && drop_list && drop_list.editable && from_list && from_list.editable;
}
if (drop_rec && drop_rec.list != drag_rec.list)
return false;
if (drop_id == drag_rec.parent_id)
return false;
while (drop_rec && drop_rec.parent_id) {
if (drop_rec.parent_id == drag_id)
return false;
drop_rec = listdata[drop_rec.parent_id];
}
return true;
}
function draggable_dropped(event, ui)
{
var drop_id = $(this).data('id'),
task_id = ui.draggable.data('id'),
rec = listdata[task_id],
parent, li;
// dropped on another list -> move
if ($(this).data('type') == 'tasklist') {
if (rec) {
- var ids = [ rec.id ],
- childs = get_all_childs(rec.id);
- if (childs.length)
- ids = ids.concat(childs);
- save_task({ id:ids, list:drop_id, _fromlist:rec.list }, 'move');
+ save_task({ id:rec.id, list:drop_id, _fromlist:rec.list }, 'move');
rec.list = drop_id;
}
}
// dropped on a new parent task or root
else {
parent = drop_id ? $('li[rel="'+drop_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : $(rcmail.gui_objects.resultlist)
if (rec && parent.length) {
// submit changes to server
rec.parent_id = drop_id || 0;
save_task(rec, 'edit');
li = ui.draggable.parent();
li.slideUp(300, function(){
li.appendTo(parent);
resort_task(rec, li);
li.slideDown(300);
fix_tree_toggles();
});
}
}
}
/**
* Show task details in a dialog
*/
function task_show_dialog(id)
{
var $dialog = $('#taskshow').dialog('close'), rec;;
if (!(rec = listdata[id]) || clear_popups({}))
return;
me.selected_task = rec;
// fill dialog data
$('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none');
$('#task-title').html(Q(rec.title || ''));
$('#task-description').html(text2html(rec.description || '', 300, 6))[(rec.description ? 'show' : 'hide')]();
$('#task-date')[(rec.date ? 'show' : 'hide')]().children('.task-text').html(Q(rec.date || rcmail.gettext('nodate','tasklist')));
$('#task-time').html(Q(rec.time || ''));
$('#task-start')[(rec.startdate ? 'show' : 'hide')]().children('.task-text').html(Q(rec.startdate || ''));
$('#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-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
var taglist = $('#task-tags')[(rec.tags && rec.tags.length ? 'show' : 'hide')]().children('.task-text').empty();
if (rec.tags && rec.tags.length) {
$.each(rec.tags, function(i,val){
$('<span>').addClass('tag-element').html(Q(val)).data('value', val).appendTo(taglist);
});
}
// 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
var buttons = {};
buttons[rcmail.gettext('edit','tasklist')] = function() {
task_edit_dialog(me.selected_task.id, 'edit');
$dialog.dialog('close');
};
buttons[rcmail.gettext('delete','tasklist')] = function() {
if (delete_task(me.selected_task.id))
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('taskdetails', 'tasklist'),
close: function() {
$dialog.dialog('destroy').appendTo(document.body);
},
buttons: buttons,
minWidth: 500,
width: 580
}).show();
}
/**
* Opens the dialog to edit a task
*/
function task_edit_dialog(id, action, presets)
{
$('#taskshow').dialog('close');
var rec = listdata[id] || presets,
$dialog = $('<div>'),
editform = $('#taskedit'),
list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] :
(me.selected_list ? me.tasklists[me.selected_list] : { editable: action=='new' });
if (!list.editable || (action == 'edit' && (!rec || rec.readonly)))
return false;
me.selected_task = $.extend({}, rec); // clone task object
// assign temporary id
if (!me.selected_task.id)
me.selected_task.id = -(++idcount);
// reset dialog first
$('#taskeditform').get(0).reset();
// fill form data
var title = $('#taskedit-title').val(rec.title || '');
var description = $('#taskedit-description').val(rec.description || '');
var recdate = $('#taskedit-date').val(rec.date || '');
var rectime = $('#taskedit-time').val(rec.time || '');
var recstartdate = $('#taskedit-startdate').val(rec.startdate || '');
var recstarttime = $('#taskedit-starttime').val(rec.starttime || '');
var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100);
completeness_slider.slider('value', complete.val());
var tasklist = $('#taskedit-tasklist').val(rec.list || 0).prop('disabled', rec.parent_id ? true : false);
// tag-edit line
var tagline = $(rcmail.gui_objects.edittagline).empty();
$.each(typeof rec.tags == 'object' && rec.tags.length ? rec.tags : [''], function(i,val){
$('<input>')
.attr('name', 'tags[]')
.attr('tabindex', '3')
.addClass('tag')
.val(val)
.appendTo(tagline);
});
$('input.tag', rcmail.gui_objects.edittagline).tagedit({
animSpeed: 100,
allowEdit: false,
checkNewEntriesCaseSensitive: false,
autocompleteOptions: { source: tags, minLength: 0 },
texts: { removeLinkTitle: rcmail.gettext('removetag', 'tasklist') }
});
// set alarm(s)
if (rec.alarms) {
if (typeof rec.alarms == 'string')
rec.alarms = rec.alarms.split(';');
for (var alarm, i=0; i < rec.alarms.length; i++) {
alarm = String(rec.alarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('#taskedit select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('#taskedit select.edit-alarm-offset').val('@');
$('#taskedit input.edit-alarm-date').val(format_datetime(ondate, 1));
$('#taskedit input.edit-alarm-time').val(format_datetime(ondate, 2));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('#taskedit input.edit-alarm-value').val(RegExp.$2);
$('#taskedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
break; // only one alarm is currently supported
}
}
// set correct visibility by triggering onchange handlers
$('#taskedit select.edit-alarm-type, #taskedit select.edit-alarm-offset').change();
// attachments
rcmail.enable_command('remove-attachment', list.editable);
me.selected_task.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
rcmail.env.compose_id = me.selected_task.id; // for rcmail.async_upload_form()
if ($.isArray(rec.attachments)) {
task_show_attachments(rec.attachments, $('#taskedit-attachments'), rec, true);
}
else {
$('#taskedit-attachments > ul').empty();
}
// show/hide tabs according to calendar's feature support
$('#taskedit-tab-attachments')[(list.attachments||rec.attachments?'show':'hide')]();
// activate the first tab
$('#taskedit').tabs('select', 0);
// define dialog buttons
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){
me.selected_task[key] = input.val();
});
me.selected_task.tags = [];
me.selected_task.attachments = [];
// do some basic input validation
if (me.selected_task.startdate && me.selected_task.date) {
var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.startdate, datepicker_settings);
var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.date, datepicker_settings);
if (startdate > duedate) {
alert(rcmail.gettext('invalidstartduedates', 'tasklist'));
return false;
}
}
$('input[name="tags[]"]', rcmail.gui_objects.edittagline).each(function(i,elem){
if (elem.value)
me.selected_task.tags.push(elem.value);
});
// serialize alarm settings
var alarm = $('#taskedit select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('#taskedit select.edit-alarm-offset').val();
if (offset == '@')
me.selected_task.alarms = '@' + date2unixtime(parse_datetime($('#taskedit input.edit-alarm-time').val(), $('#taskedit input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('#taskedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
me.selected_task.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments) {
if (i.match(/^rcmfile(.+)/))
me.selected_task.attachments.push(RegExp.$1);
}
// task assigned to a new list
if (me.selected_task.list && me.selected_task.list != rec.list) {
me.selected_task._fromlist = rec.list;
-
- // also move all childs
- var childs = get_all_childs(me.selected_task.id);
- if (childs.length)
- save_task({ id:childs, list:me.selected_task.list, _fromlist:rec.list }, 'move');
}
me.selected_task.complete = complete.val() / 100;
if (isNaN(me.selected_task.complete))
me.selected_task.complete = null;
if (!me.selected_task.list && list.id)
me.selected_task.list = list.id;
if (save_task(me.selected_task, action))
$dialog.dialog('close');
};
if (action != 'new') {
buttons[rcmail.gettext('delete', 'tasklist')] = function() {
if (delete_task(rec.id))
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'tasklist')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'),
close: function() {
editform.hide().appendTo(document.body);
$dialog.dialog('destroy').remove();
},
buttons: buttons,
minHeight: 460,
minWidth: 500,
width: 580
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE
title.select();
}
/**
* Open a task attachment either in a browser window for inline view or download it
*/
function load_attachment(rec, att)
{
// can't open temp attachments
if (!rec.id || rec.id < 0)
return false;
var qstring = '_id='+urlencode(att.id)+'&_t='+urlencode(rec.recurrence_id||rec.id)+'&_list='+urlencode(rec.list);
// open attachment in frame if it's of a supported mimetype
// similar as in app.js and calendar_ui.js
if (att.id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubetaskattachment');
if (rcmail.attachment_win) {
window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
return;
}
}
rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
};
/**
* Build task attachments list
*/
function task_show_attachments(list, container, rec, edit)
{
var i, id, len, content, li, elem,
ul = $('<ul>').addClass('attachmentslist');
for (i=0, len=list.length; i<len; i++) {
elem = list[i];
li = $('<li>').addClass(elem.classname);
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
content = $('<a>')
.attr('href', '#delete')
.attr('title', rcmail.gettext('delete'))
.addClass('delete')
.click({ id:elem.id }, function(e) {
remove_attachment(this, e.data.id);
return false;
});
if (!rcmail.env.deleteicon) {
content.html(rcmail.gettext('delete'));
}
else {
$('<img>').attr('src', rcmail.env.deleteicon).attr('alt', rcmail.gettext('delete')).appendTo(content);
}
li.append(content);
}
// name/link
$('<a>')
.attr('href', '#load')
.addClass('file')
.html(elem.name).click({ task:rec, att:elem }, function(e) {
load_attachment(e.data.task, e.data.att);
return false;
}).appendTo(li);
ul.append(li);
}
if (edit && rcmail.gui_objects.attachmentlist) {
ul.id = rcmail.gui_objects.attachmentlist.id;
rcmail.gui_objects.attachmentlist = ul.get(0);
}
container.empty().append(ul);
};
/**
*
*/
var remove_attachment = function(elem, id)
{
$(elem.parentNode).hide();
me.selected_task.deleted_attachments.push(id);
delete rcmail.env.attachments[id];
};
/**
*
*/
function add_childtask(id)
{
var rec = listdata[id];
task_edit_dialog(null, 'new', { parent_id:id, list:rec.list });
}
/**
* Delete the given task
*/
function delete_task(id)
{
var rec = listdata[id];
- if (rec && confirm("Delete this?")) {
- saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
- rcmail.http_post('task', { action:'delete', t:rec, filter:filtermask });
- $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
- return true;
+ if (!rec || rec.readonly)
+ return false;
+
+ var html, buttons = [{
+ text: rcmail.gettext('cancel', 'tasklist'),
+ click: function() {
+ $(this).dialog('close');
+ }
+ }];
+
+ if (rec.children && rec.children.length) {
+ html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
+ buttons.push({
+ text: rcmail.gettext('deletethisonly','tasklist'),
+ click: function() {
+ _delete_task(id, 0);
+ $(this).dialog('close');
+ }
+ });
+ buttons.push({
+ text: rcmail.gettext('deletewithchilds','tasklist'),
+ click: function() {
+ _delete_task(id, 1);
+ $(this).dialog('close');
+ }
+ });
}
-
- return false;
+ else {
+ html = rcmail.gettext('deletetasktconfirm','tasklist');
+ buttons.push({
+ text: rcmail.gettext('delete','tasklist'),
+ click: function() {
+ _delete_task(id, 0);
+ $(this).dialog('close');
+ }
+ });
+ }
+
+ var $dialog = $('<div>').html(html);
+ $dialog.dialog({
+ modal: true,
+ width: 520,
+ dialogClass: 'warning',
+ title: rcmail.gettext('deletetask', 'tasklist'),
+ buttons: buttons,
+ close: function(){
+ $dialog.dialog('destroy').hide();
+ }
+ }).addClass('tasklist-confirm').show();
+
+ return true;
+ }
+
+ /**
+ * Subfunction to submit the delete command after confirm
+ */
+ function _delete_task(id, mode)
+ {
+ var rec = listdata[id],
+ li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
+
+ saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+ rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, mode:mode, filter:filtermask });
+
+ // move childs to parent/root
+ if (mode != 1) {
+ var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
+ if (!parent_node || !parent_node.length)
+ parent_node = rcmail.gui_objects.resultlist;
+
+ $.each(rec.children, function(i,cid) {
+ var child = listdata[cid];
+ child.parent_id = rec.parent_id;
+ resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
+ });
+ }
+
+ li.remove();
}
/**
* Check if the given task matches the current filtermask and tag selection
*/
function match_filter(rec)
{
var match = !filtermask || (filtermask & rec.mask) > 0;
if (match && tagsfilter.length) {
match = rec.tags && rec.tags.length;
for (var i=0; match && i < tagsfilter.length; i++) {
if (rec.tags.indexOf(tagsfilter[i]) < 0)
match = false;
}
}
return match;
}
/**
*
*/
function list_edit_dialog(id)
{
var list = me.tasklists[id],
$dialog = $('#tasklistform').dialog('close');
editform = $('#tasklisteditform');
if (!list)
list = { name:'', editable:true, showalarms:true };
// fill edit form
var name = $('#taskedit-tasklistame').prop('disabled', !list.editable).val(list.editname || list.name),
alarms = $('#taskedit-showalarms').prop('checked', list.showalarms).get(0),
parent = $('#taskedit-parentfolder').val(list.parentfolder);
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('save','tasklist')] = function() {
// do some input validation
if (!name.val() || name.val().length < 2) {
alert(rcmail.gettext('invalidlistproperties', 'tasklist'));
name.select();
return;
}
// post data to server
var data = editform.serializeJSON();
if (list.id)
data.id = list.id;
if (alarms)
data.showalarms = alarms.checked ? 1 : 0;
if (parent.length)
data.parentfolder = $('option:selected', parent).val();
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasklist', { action:(list.id ? 'edit' : 'new'), l:data });
$dialog.dialog('close');
};
buttons[rcmail.gettext('cancel','tasklist')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((list.id ? 'editlist' : 'createlist'), 'tasklist'),
close: function() { $dialog.dialog('destroy').hide(); },
buttons: buttons,
minWidth: 400,
width: 420
}).show();
}
/**
*
*/
function list_remove(id)
{
var list = me.tasklists[id];
if (list && list.editable && confirm(rcmail.gettext('deletelistconfirm', 'tasklist'))) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasklist', { action:'remove', l:{ id:list.id } });
return true;
}
return false;
}
/**
* Callback from server to finally remove the given list
*/
function destroy_list(prop)
{
var list = me.tasklists[prop.id],
li = rcmail.get_folder_li(prop.id, 'rcmlitasklist');
if (li) {
$(li).remove();
}
if (list) {
list.active = false;
// delete me.tasklists[prop.id];
unlock_saving();
remove_tasks(list.id);
}
}
/**
*
*/
function insert_list(prop)
{
var li = $('<li>').attr('id', 'rcmlitasklist'+prop.id)
.append('<input type="checkbox" name="_list[]" value="'+prop.id+'" checked="checked" />')
.append('<span class="handle"> </span>')
.append('<span class="listname">'+Q(prop.name)+'</span>');
$(rcmail.gui_objects.folderlist).append(li);
me.tasklists[prop.id] = prop;
init_tasklist_li(li.get(0), prop.id);
}
/**
*
*/
function update_list(prop)
{
var id = prop.oldid || prop.id,
li = rcmail.get_folder_li(id, 'rcmlitasklist');
if (me.tasklists[id] && li) {
delete me.tasklists[id];
me.tasklists[prop.id] = prop;
$(li).data('id', prop.id);
$('#'+li.id+' input').data('id', prop.id);
$('.listname', li).html(Q(prop.name));
}
}
/**
* Execute search
*/
function quicksearch()
{
var q;
if (rcmail.gui_objects.qsearchbox && (q = rcmail.gui_objects.qsearchbox.value)) {
var id = 'search-'+q;
var resources = [];
for (var rid in me.tasklists) {
if (me.tasklists[rid].active) {
resources.push(rid);
}
}
id += '@'+resources.join(',');
// ignore if query didn't change
if (search_request == id)
return;
search_request = id;
search_query = q;
list_tasks('all');
}
else // empty search input equals reset
this.reset_search();
}
/**
* Reset search and get back to normal listing
*/
function reset_search()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (search_request) {
search_request = search_query = null;
list_tasks();
}
}
/**** Utility functions ****/
/**
* Clear any text selection
* (text is probably selected when double-clicking somewhere)
*/
function clearSelection()
{
if (document.selection && document.selection.empty) {
document.selection.empty() ;
}
else if (window.getSelection) {
var sel = window.getSelection();
if (sel && sel.removeAllRanges)
sel.removeAllRanges();
}
}
/**
* Hide all open popup menus
*/
function clear_popups(e)
{
var count = 0, target = e.target;
if (target && target.className == 'inner')
target = e.target.parentNode;
$('.popupmenu:visible').each(function(i, elem){
var menu = $(elem), id = elem.id;
if (target.id != id+'link' && (!menu.data('sticky') || !target_overlaps(e.target, elem))) {
menu.hide();
count++;
}
});
return count;
}
/**
* Check whether the event target is a descentand of the given element
*/
function target_overlaps(target, elem)
{
while (target.parentNode) {
if (target.parentNode == elem)
return true;
target = target.parentNode;
}
return false;
}
/**
*
*/
function active_lists()
{
var active = [];
for (var id in me.tasklists) {
if (me.tasklists[id].active)
active.push(id);
}
return active;
}
/**
* Register event handlers on a tasklist (folder) item
*/
function init_tasklist_li(li, id)
{
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.tasklists[id]) { // add or remove event source on click
me.tasklists[id].active = this.checked;
fetch_counts();
if (!this.checked) remove_tasks(id);
else list_tasks(null);
rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:me.tasklists[id].active?1:0 } });
}
}).data('id', id).get(0).checked = me.tasklists[id].active || false;
$(li).click(function(e){
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlitasklist');
rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[id].editable);
me.selected_list = id;
})
.dblclick(function(e){
list_edit_dialog($(this).data('id'));
})
.data('id', id)
.data('type', 'tasklist')
.addClass(me.tasklists[id].editable ? null : 'readonly');
}
// init dialog by default
init_taskedit();
}
// extend jQuery
// from http://james.padolsey.com/javascript/sorting-elements-with-jquery/
jQuery.fn.sortElements = (function(){
var sort = [].sort;
return function(comparator, getSortable) {
getSortable = getSortable || function(){ return this };
var last = null;
return sort.call(this, comparator).each(function(i){
// at this point the array is sorted, so we can just detach each one from wherever it is, and add it after the last
var node = $(getSortable.call(this));
var parent = node.parent();
if (last) last.after(node);
else parent.prepend(node);
last = node;
});
};
})();
/* tasklist plugin UI initialization */
var rctasks;
window.rcmail && rcmail.addEventListener('init', function(evt) {
rctasks = new rcube_tasklist_ui(rcmail.env.libcal_settings);
// register button commands
rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true);
//rcmail.register_command('print', function(){ rctasks.print_list(); }, true);
rcmail.register_command('list-create', function(){ rctasks.list_edit_dialog(null); }, true);
rcmail.register_command('list-edit', function(){ rctasks.list_edit_dialog(rctasks.selected_list); }, false);
rcmail.register_command('list-remove', function(){ rctasks.list_remove(rctasks.selected_list); }, false);
rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
rctasks.init();
});
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 1c12c6b8..b28b2e39 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1,837 +1,880 @@
<?php
/**
* Tasks plugin for Roundcube webmail
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class tasklist extends rcube_plugin
{
const FILTER_MASK_TODAY = 1;
const FILTER_MASK_TOMORROW = 2;
const FILTER_MASK_WEEK = 4;
const FILTER_MASK_LATER = 8;
const FILTER_MASK_NODATE = 16;
const FILTER_MASK_OVERDUE = 32;
const FILTER_MASK_FLAGGED = 64;
const FILTER_MASK_COMPLETE = 128;
const SESSION_KEY = 'tasklist_temp';
public static $filter_masks = array(
'today' => self::FILTER_MASK_TODAY,
'tomorrow' => self::FILTER_MASK_TOMORROW,
'week' => self::FILTER_MASK_WEEK,
'later' => self::FILTER_MASK_LATER,
'nodate' => self::FILTER_MASK_NODATE,
'overdue' => self::FILTER_MASK_OVERDUE,
'flagged' => self::FILTER_MASK_FLAGGED,
'complete' => self::FILTER_MASK_COMPLETE,
);
public $task = '?(?!login|logout).*';
public $rc;
public $lib;
public $driver;
public $timezone;
public $ui;
private $collapsed_tasks = array();
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->rc = rcmail::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('tasks', 'tasklist');
// load plugin configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print'));
$this->timezone = $this->lib->timezone;
if ($this->rc->task == 'tasks' && $this->rc->action != 'save-pref') {
$this->load_driver();
// register calendar actions
$this->register_action('index', array($this, 'tasklist_view'));
$this->register_action('task', array($this, 'task_action'));
$this->register_action('tasklist', array($this, 'tasklist_action'));
$this->register_action('counts', array($this, 'fetch_counts'));
$this->register_action('fetch', array($this, 'fetch_tasks'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
}
else if ($this->rc->task == 'mail') {
// TODO: register hooks to catch ical/vtodo email attachments
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
// $this->add_hook('message_load', array($this, 'mail_message_load'));
// $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'tasklist-create-from-mail',
'label' => 'tasklist.createfrommail',
'type' => 'link',
'classact' => 'icon taskaddlink active',
'class' => 'icon taskaddlink',
'innerclass' => 'icon taskadd',
))),
'messagemenu');
}
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
$this->ui->init();
}
// add hooks for alarms handling
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('tasklist_driver', 'database');
$driver_class = 'tasklist_' . $driver_name . '_driver';
require_once($this->home . '/drivers/tasklist_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('libkolab');
default:
$this->driver = new $driver_class($this);
break;
}
$this->rc->output->set_env('tasklist_driver', $driver_name);
}
/**
* Dispatcher for task-related actions initiated by the client
*/
public function task_action()
{
+ $filter = intval(get_input_value('filter', RCUBE_INPUT_GPC));
$action = get_input_value('action', RCUBE_INPUT_GPC);
$rec = get_input_value('t', RCUBE_INPUT_POST, true);
$oldrec = $rec;
$success = $refresh = false;
switch ($action) {
case 'new':
$oldrec = null;
$rec = $this->prepare_task($rec);
$rec['uid'] = $this->generate_uid();
$temp_id = $rec['tempid'];
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
$this->cleanup_task($rec);
}
break;
case 'edit':
$rec = $this->prepare_task($rec);
if ($success = $this->driver->edit_task($rec)) {
$refresh = $this->driver->get_task($rec);
$this->cleanup_task($rec);
}
break;
case 'move':
- $recs = array();
foreach ((array)$rec['id'] as $id) {
$r = $rec;
$r['id'] = $id;
if ($this->driver->move_task($r)) {
- $r = $this->driver->get_task($r);
- $this->encode_task($r);
- $refresh[] = $r;
+ $refresh[] = $this->driver->get_task($r);
$success = true;
+
+ // move all childs, too
+ foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist'])) as $cid) {
+ $child = $rec;
+ $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)) {
+ $refresh[] = $r;
+ }
+ }
+ }
}
}
break;
case 'delete':
- if (!($success = $this->driver->delete_task($rec, false)))
+ $mode = intval(get_input_value('mode', RCUBE_INPUT_POST));
+ $oldrec = $this->driver->get_task($rec);
+ if ($success = $this->driver->delete_task($rec, false)) {
+ // delete/modify all childs
+ foreach ($this->driver->get_childs($rec, $mode) as $cid) {
+ $child = array('id' => $cid, 'list' => $rec['list']);
+
+ if ($mode == 1) { // delete all childs
+ if ($this->driver->delete_task($child, false)) {
+ if ($this->driver->undelete)
+ $_SESSION['tasklist_undelete'][$rec['id']][] = $cid;
+ }
+ else
+ $success = false;
+ }
+ else {
+ $child['parent_id'] = strval($oldrec['parent_id']);
+ $this->driver->edit_task($child);
+ }
+ }
+ }
+
+ if (!$success)
$this->rc->output->command('plugin.reload_data');
break;
case 'undelete':
- if ($success = $this->driver->undelete_task($rec))
- $refresh = $this->driver->get_task($rec);
+ if ($success = $this->driver->undelete_task($rec)) {
+ $refresh[] = $this->driver->get_task($rec);
+ foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) {
+ if ($this->driver->undelete_task($rec)) {
+ $refresh[] = $this->driver->get_task($rec);
+ }
+ }
+ }
break;
case 'collapse':
if ($collapsed = intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) {
$this->collapsed_tasks[] = $rec['id'];
}
else {
$i = array_search($rec['id'], $this->collapsed_tasks);
if ($i !== false)
unset($this->collapsed_tasks[$i]);
}
$this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));
return; // avoid further actions
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
- if ($refresh['id'])
+ if ($refresh['id']) {
$this->encode_task($refresh);
+ }
+ else if (is_array($refresh)) {
+ foreach ($refresh as $i => $r)
+ $this->encode_task($refresh[$i]);
+ }
$this->rc->output->command('plugin.refresh_task', $refresh);
}
}
/**
* repares new/edited task properties before save
*/
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
if ($rec['raw']) {
foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
$locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
$normwords[] = $word;
$datewords[] = $word;
}
foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
$locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
$normwords[] = $month;
$datewords[] = $month;
}
foreach (array('on','this','next','at') as $word) {
$fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
$fillwords[] = $word;
}
$raw = trim($rec['raw']);
$date_str = '';
// translate localized keywords
$raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
$raw = preg_replace($locwords, $normwords, $raw);
// find date pattern
$date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
if (preg_match($date_pattern, $raw, $m)) {
$date_str .= $m[1] . $m[2] . $m[3];
$raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
// add year to date string
if ($m[1] && !$m[3])
$date_str .= date('Y');
}
// find time pattern
$time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
if (preg_match($time_pattern, $raw, $m)) {
$has_time = true;
$date_str .= ($date_str ? ' ' : 'today ') . $m[1];
$raw = preg_replace($time_pattern, '', $raw);
}
// yes, raw input matched a (valid) date
if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
$rec['date'] = $date->format('Y-m-d');
if ($has_time)
$rec['time'] = $date->format('H:i');
$rec['title'] = $raw;
}
else
$rec['title'] = $rec['raw'];
}
// normalize input from client
if (isset($rec['complete'])) {
$rec['complete'] = floatval($rec['complete']);
if ($rec['complete'] > 1)
$rec['complete'] /= 100;
}
if (isset($rec['flagged']))
$rec['flagged'] = intval($rec['flagged']);
// fix for garbage input
if ($rec['description'] == 'null')
$rec['description'] = '';
foreach ($rec as $key => $val) {
if ($val === 'null')
$rec[$key] = null;
}
if (!empty($rec['date'])) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['date'] = $date->format('Y-m-d');
if (!empty($rec['time']))
$rec['time'] = $date->format('H:i');
}
catch (Exception $e) {
$rec['date'] = $rec['time'] = null;
}
}
if (!empty($rec['startdate'])) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdate'] = $date->format('Y-m-d');
if (!empty($rec['starttime']))
$rec['starttime'] = $date->format('H:i');
}
catch (Exception $e) {
$rec['startdate'] = $rec['starttime'] = null;
}
}
// alarms cannot work without a date
if ($rec['alarms'] && !$rec['date'] && !$rec['startdate'] && strpos($task['alarms'], '@') === false)
$rec['alarms'] = '';
$attachments = array();
$taskid = $rec['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
unset($attachments[$id]['abort'], $attachments[$id]['group']);
}
}
}
}
$rec['attachments'] = $attachments;
if (is_numeric($rec['id']) && $rec['id'] < 0)
unset($rec['id']);
return $rec;
}
/**
* Releases some resources after successful save
*/
private function cleanup_task(&$rec)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* Dispatcher for tasklist actions initiated by the client
*/
public function tasklist_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$list = get_input_value('l', RCUBE_INPUT_POST, true);
$success = false;
if (isset($list['showalarms']))
$list['showalarms'] = intval($list['showalarms']);
switch ($action) {
case 'new':
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
break;
case 'edit':
if ($newid = $this->driver->edit_list($list)) {
$list['oldid'] = $list['id'];
$list['id'] = $newid;
$this->rc->output->command('plugin.update_tasklist', $list);
$success = true;
}
break;
case 'subscribe':
$success = $this->driver->subscribe_list($list);
break;
case 'remove':
if (($success = $this->driver->remove_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
$this->rc->output->command('plugin.unlock_saving');
}
/**
* Get counts for active tasks divided into different selectors
*/
public function fetch_counts()
{
if (isset($_REQUEST['lists'])) {
$lists = get_input_value('lists', RCUBE_INPUT_GPC);
}
else {
foreach ($this->driver->get_lists() as $list) {
if ($list['active'])
$lists[] = $list['id'];
}
}
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
/**
* Adjust the cached counts after changing a task
*/
public function update_counts($oldrec, $newrec)
{
// rebuild counts until this function is finally implemented
$this->fetch_counts();
// $this->rc->output->command('plugin.update_counts', $counts);
}
/**
*
*/
public function fetch_tasks()
{
$f = intval(get_input_value('filter', RCUBE_INPUT_GPC));
$search = get_input_value('q', RCUBE_INPUT_GPC);
$filter = array('mask' => $f, 'search' => $search);
$lists = get_input_value('lists', RCUBE_INPUT_GPC);;
/*
// convert magic date filters into a real date range
switch ($f) {
case self::FILTER_MASK_TODAY:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $filter['to'] = $today->format('Y-m-d');
break;
case self::FILTER_MASK_TOMORROW:
$tomorrow = new DateTime('now + 1 day', $this->timezone);
$filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
break;
case self::FILTER_MASK_OVERDUE:
$yesterday = new DateTime('yesterday', $this->timezone);
$filter['to'] = $yesterday->format('Y-m-d');
break;
case self::FILTER_MASK_WEEK:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $today->format('Y-m-d');
$weekend = new DateTime('now + 7 days', $this->timezone);
$filter['to'] = $weekend->format('Y-m-d');
break;
case self::FILTER_MASK_LATER:
$date = new DateTime('now + 8 days', $this->timezone);
$filter['from'] = $date->format('Y-m-d');
break;
}
*/
$data = $tags = $this->task_tree = $this->task_titles = array();
foreach ($this->driver->list_tasks($filter, $lists) as $rec) {
if ($rec['parent_id']) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
$this->encode_task($rec);
if (!empty($rec['tags']))
$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))
$data[] = $rec;
}
// sort tasks according to their hierarchy level and due date
array_walk($data, array($this, 'task_walk_tree'));
usort($data, array($this, 'task_sort_cmp'));
$this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags))));
}
/**
* Prepare the given task record before sending it to the client
*/
private function encode_task(&$rec)
{
$rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']);
$rec['changed'] = is_object($rec['changed']) ? $rec['changed']->format('U') : null;
if ($rec['date']) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['datetime'] = intval($date->format('U'));
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
$rec['_hasdate'] = 1;
}
catch (Exception $e) {
$rec['date'] = $rec['datetime'] = null;
}
}
else {
$rec['date'] = $rec['datetime'] = null;
$rec['_hasdate'] = 0;
}
if ($rec['startdate']) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdatetime'] = intval($date->format('U'));
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
}
catch (Exception $e) {
$rec['startdate'] = $rec['startdatetime'] = null;
}
}
if ($rec['alarms'])
$rec['alarms_text'] = libcalendaring::alarms_text($rec['alarms']);
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']);
}
if (in_array($rec['id'], $this->collapsed_tasks))
$rec['collapsed'] = true;
$this->task_titles[$rec['id']] = $rec['title'];
}
/**
* Callback function for array_walk over all tasks.
* Sets tree depth and parent titles
*/
private function task_walk_tree(&$rec)
{
$rec['_depth'] = 0;
$parent_id = $this->task_tree[$rec['id']];
while ($parent_id) {
$rec['_depth']++;
$rec['parent_title'] = $this->task_titles[$parent_id];
$parent_id = $this->task_tree[$parent_id];
}
}
/**
* Compare function for task list sorting.
* Nested tasks need to be sorted to the end.
*/
private function task_sort_cmp($a, $b)
{
$d = $a['_depth'] - $b['_depth'];
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
}
/**
* Compute the filter mask of the given task
*
* @param array Hash array with Task record properties
* @return int Filter mask
*/
public function filter_mask($rec)
{
static $today, $tomorrow, $weeklimit;
if (!$today) {
$today_date = new DateTime('now', $this->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$week_date = new DateTime('now + 7 days', $this->timezone);
$weeklimit = $week_date->format('Y-m-d');
}
$mask = 0;
$start = $rec['startdate'] ?: '1900-00-00';
$duedate = $rec['date'] ?: '3000-00-00';
if ($rec['flagged'])
$mask |= self::FILTER_MASK_FLAGGED;
if ($rec['complete'] == 1.0)
$mask |= self::FILTER_MASK_COMPLETE;
if (empty($rec['date']))
$mask |= self::FILTER_MASK_NODATE;
else if ($rec['date'] < $today)
$mask |= self::FILTER_MASK_OVERDUE;
if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
$mask |= self::FILTER_MASK_TODAY;
if ($duedate <= $tomorrow || ($rec['startdate'] && $start <= $tomorrow))
$mask |= self::FILTER_MASK_TOMORROW;
if (($start > $tomorrow || $duedate > $tomorrow) && $duedate <= $weeklimit)
$mask |= self::FILTER_MASK_WEEK;
if ($start > $weeklimit || $duedate > $weeklimit)
$mask |= self::FILTER_MASK_LATER;
return $mask;
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function tasklist_view()
{
$this->ui->init();
$this->ui->init_templates();
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['tasklist.'.$label] = $this->gettext($label);
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
// "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
// "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
// "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
/**
* Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
* This will check for pending notifications and pass them to the client
*/
public function pending_alarms($p)
{
$this->load_driver();
if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
foreach ($alarms as $alarm) {
// encode alarm object to suit the expectations of the calendaring code
if ($alarm['date'])
$alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone);
$alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task:
$alarm['allday'] = empty($alarm['time']) ? 1 : 0;
$p['alarms'][] = $alarm;
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by the calendar module
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'task:') === 0)
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']);
}
return $p;
}
/******* Attachment handling *******/
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
$this->lib->attachment_upload(self::SESSION_KEY);
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
// show loading page
if (!empty($_GET['_preload'])) {
return $this->lib->attachment_loading_page();
}
$task = get_input_value('_t', RCUBE_INPUT_GPC);
$list = get_input_value('_list', RCUBE_INPUT_GPC);
$id = get_input_value('_id', RCUBE_INPUT_GPC);
$task = array('id' => $task, 'list' => $list);
$attachment = $this->driver->get_attachment($id, $task);
// show part page
if (!empty($_GET['_frame'])) {
$this->lib->attachment = $attachment;
$this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
$this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
$this->rc->output->send('tasklist.attachment');
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
$this->lib->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/******* Email related function *******/
public function mail_message2task()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$task = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$task['title'] = trim($message->subject);
$task['description'] = trim($message->first_text_part());
$task['id'] = -$uid;
$this->load_driver();
// copy mail attachments to task
if ($message->attachments && $this->driver->attachments) {
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $task['id'];
$_SESSION[self::SESSION_KEY]['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $task['id'],
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
$attachment['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']);
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; # add prefix to consider it 'new'
$task['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2taskdialog', $task);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/******* Utility functions *******/
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Jun 9, 8:03 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196779
Default Alt Text
(154 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment