diff --git a/bootstrap.php b/bootstrap.php index d16f10feefc17f3e4c462a7f792f553208cf00fb..d2afa2243f7bfa2914da15428c3f8e2c9b197fcb 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -101,6 +101,8 @@ class Bootstrap { define('TICKET_STATUS_TABLE', $prefix.'ticket_status'); define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); + define('TASK_TABLE',$prefix.'task'); + define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE); diff --git a/include/ajax.draft.php b/include/ajax.draft.php index 8d264ff486639b94df5622028ea611c656f130a0..2687ad94b53fb28e074d856394489aeae2c435db 100644 --- a/include/ajax.draft.php +++ b/include/ajax.draft.php @@ -358,7 +358,7 @@ class DraftAjaxAPI extends AjaxController { } } $field_list = array('response', 'note', 'answer', 'body', - 'message', 'issue'); + 'message', 'issue', 'description'); foreach ($field_list as $field) { if (isset($vars[$field])) { return urldecode($vars[$field]); diff --git a/include/class.config.php b/include/class.config.php index eb0ffca76c6382939274120a346daada44147b0c..ab5d822fab63161344ae796386c8a13172d13b2b 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -673,6 +673,26 @@ class OsticketConfig extends Config { array('Ticket', 'isTicketNumberUnique')); } + // Task sequence + function getDefaultTaskSequence() { + if ($this->get('task_sequence_id')) + $sequence = Sequence::lookup($this->get('task_sequence_id')); + if (!$sequence) + $sequence = new RandomSequence(); + + return $sequence; + } + + function getDefaultTaskNumberFormat() { + return $this->get('task_number_format'); + } + + function getNewTaskNumber() { + $s = $this->getDefaultTaskSequence(); + return $s->next($this->getDefaultTaskNumberFormat(), + array('Task', 'isNumberUnique')); + } + /* autoresponders & Alerts */ function autoRespONNewTicket() { return ($this->get('ticket_autoresponder')); diff --git a/include/class.forms.php b/include/class.forms.php index 9b86046926406355a24d70ba72f6a1869cbe7c38..25ee52ce1a427caa6f514abf57dcd65daf710362 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1459,7 +1459,7 @@ class ThreadEntryField extends FormField { 'label'=>__('Enable Attachments'), 'default'=>$cfg->allowAttachments(), 'configuration'=>array( - 'desc'=>__('Enables attachments on tickets, regardless of channel'), + 'desc'=>__('Enables attachments, regardless of channel'), ), 'validators' => function($self, $value) { if (!ini_get('file_uploads')) diff --git a/include/class.model.php b/include/class.model.php index 7b8af26ab93d9056f3eb46fc8807e78b1658fc58..5d41a05d099684b943c9ad754f2b8653b443324d 100644 --- a/include/class.model.php +++ b/include/class.model.php @@ -21,6 +21,7 @@ class ObjectModel { const OBJECT_TYPE_ORG = 'O'; const OBJECT_TYPE_FAQ = 'K'; const OBJECT_TYPE_FILE = 'F'; + const OBJECT_TYPE_TASK = 'A'; private function objects() { static $objects = false; @@ -32,6 +33,7 @@ class ObjectModel { self::OBJECT_TYPE_ORG => 'Organization', self::OBJECT_TYPE_FAQ => 'FAQ', self::OBJECT_TYPE_FILE => 'AttachmentFile', + self::OBJECT_TYPE_TASK => 'Task', ); } diff --git a/include/class.task.php b/include/class.task.php new file mode 100644 index 0000000000000000000000000000000000000000..29f947a461e858f9a9fcd5f4fe2256fb73627293 --- /dev/null +++ b/include/class.task.php @@ -0,0 +1,358 @@ +<?php + +class TaskModel extends VerySimpleModel { + static $meta = array( + 'table' => TASK_TABLE, + 'pk' => array('id'), + 'joins' => array( + 'thread' => array( + 'reverse' => 'ThreadModel.object', + ), + ) + ); + + const ISOPEN = 0x0001; + const ISOVERDUE = 0x0002; + + + protected function hasFlag($flag) { + return ($this->get('flags') & $flag) !== 0; + } + + protected function clearFlag($flag) { + return $this->set('flags', $this->get('flags') & ~$flag); + } + + protected function setFlag($flag) { + return $this->set('flags', $this->get('flags') | $flag); + } + + function getId() { + return $this->id; + } + + function getNumber() { + return $this->number; + } + + function getStaffId() { + return $this->staff_id; + } + + function getTeamId() { + return $this->team_id; + } + + function getDeptId() { + return $this->dept_id; + } + + function getCreateDate() { + return $this->created; + } + + function getDueDate() { + return $this->duedate; + } + + function isOpen() { + return $this->hasFlag(self::ISOPEN); + } + + function isClosed() { + return !$this->isOpen(); + } + + function close() { + return $this->clearFlag(self::ISOPEN); + } + + function reopen() { + return $this->setFlag(self::ISOPEN); + } + + function isAssigned() { + return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId())); + } + + function isOverdue() { + return $this->hasFlag(self::ISOVERDUE); + } + +} + +class Task extends TaskModel { + var $form; + var $entry; + var $thread; + + + function getStatus() { + return $this->isOpen() ? _('Open') : _('Closed'); + } + + function getTitle() { + return $this->__cdata('title', ObjectModel::OBJECT_TYPE_TASK); + } + + + function getDept() { + return "Dept Object"; + } + + function getThread() { + + if (!$this->thread) + $this->thread = TaskThread::lookup(array( + 'object_id' => $this->getId(), + 'object_type' => ObjectModel::OBJECT_TYPE_TASK) + ); + + return $this->thread; + } + + function getThreadEntry($id) { + return $this->getThread()->getEntry($id); + } + + function getThreadEntries($type, $order='') { + return $this->getThread()->getEntries( + array('type' => $type, 'order' => $order)); + } + + function getForm() { + if (!isset($this->form)) { + // Look for the entry first + if ($this->form = DynamicFormEntry::lookup( + array('object_type' => ObjectModel::OBJECT_TYPE_TASK))) { + return $this->form; + } + // Make sure the form is in the database + elseif (!($this->form = DynamicForm::lookup( + array('type' => ObjectModel::OBJECT_TYPE_TASK)))) { + $this->__loadDefaultForm(); + return $this->getForm(); + } + // Create an entry to be saved later + $this->form = $this->form->instanciate(); + $this->form->object_type = ObjectModel::OBJECT_TYPE_TASK; + } + + return $this->form; + } + + function addDynamicData($data) { + + $tf = TaskForm::getInstance($this->id, true); + foreach ($tf->getFields() as $f) + if (isset($data[$f->get('name')])) + $tf->setAnswer($f->get('name'), $data[$f->get('name')]); + + $tf->save(); + + return $tf; + } + + function getDynamicData($create=true) { + if (!isset($this->_entries)) { + $this->_entries = DynamicFormEntry::forObject($this->id, + ObjectModel::OBJECT_TYPE_TASK)->all(); + if (!$this->_entries && $create) { + $f = TaskForm::getInstance($this->id, true); + $f->save(); + $this->_entries[] = $f; + } + } + + return $this->_entries ?: array(); + } + + + function to_json() { + + $info = array( + 'id' => $this->getId(), + 'title' => $this->getTitle() + ); + + return JsonDataEncoder::encode($info); + } + + function __cdata($field, $ftype=null) { + + foreach ($this->getDynamicData() as $e) { + // Make sure the form type matches + if (!$e->getForm() + || ($ftype && $ftype != $e->getForm()->get('type'))) + continue; + + // Get the named field and return the answer + if ($f = $e->getForm()->getField($field)) + return $f->getAnswer(); + } + + return null; + } + + function __toString() { + return (string) $this->getTitle(); + } + + /* util routines */ + function postNote($vars, &$errors, $poster='', $alert=true) { + global $cfg, $thisstaff; + + $vars['staffId'] = 0; + $vars['poster'] = 'SYSTEM'; + if ($poster && is_object($poster)) { + $vars['staffId'] = $poster->getId(); + $vars['poster'] = $poster->getName(); + } elseif ($poster) { //string + $vars['poster'] = $poster; + } + + if (!($note=$this->getThread()->addNote($vars, $errors))) + return null; + + if (isset($vars['task_status'])) { + if ($vars['task_status']) + $this->open(); + else + $this->close(); + + $this->save(true); + } + + return $note; + } + + static function lookupIdByNumber($number) { + $sql = 'SELECT id FROM '.TASK_TABLE + .' WHERE `number`='.db_input($number); + list($id) = db_fetch_row(db_query($sql)); + + return $id; + } + + static function isNumberUnique($number) { + return !self::lookupIdByNumber($number); + } + + static function create($form, $object) { + global $cfg, $thisstaff; + + if (!$thisstaff + || !$form + // TODO: Make sure it's an instance of ORM Model + || !$object) + return null; + + if (!$form->isValid()) + return false; + + try { + + $task = parent::create(array( + 'flags' => 1, + 'object_id' => $object->getId(), + 'object_type' => $object->getObjectType(), + 'number' => $cfg->getNewTaskNumber(), + 'created' => new SqlFunction('NOW'), + 'updated' => new SqlFunction('NOW'), + )); + $task->save(true); + } catch(OrmException $e) { + return null; + } + + $vars = $form->getClean(); + $task->addDynamicData($vars); + // Create a thread + message. + $thread = TaskThread::create($task); + $desc = $form->getField('description'); + if ($desc + && $desc->isAttachmentsEnabled() + && ($attachments=$desc->getWidget()->getAttachments())) + $vars['cannedattachments'] = $attachments->getClean(); + + $vars['staffId'] = $thisstaff->getId(); + $vars['poster'] = $thisstaff; + if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) + $vars['ip_address'] = $SERVER['REMOTE_ADDR']; + + $thread->addDescription($vars); + + Signal::send('model.created', $task); + + return $task; + } + + static function __loadDefaultForm() { + + require_once INCLUDE_DIR.'class.i18n.php'; + + $i18n = new Internationalization(); + $tpl = $i18n->getTemplate('form.yaml'); + foreach ($tpl->getData() as $f) { + if ($f['type'] == ObjectModel::OBJECT_TYPE_TASK) { + $form = DynamicForm::create($f); + $form->save(); + break; + } + } + } +} + +class TaskForm extends DynamicForm { + static $instance; + static $form; + + static function objects() { + $os = parent::objects(); + return $os->filter(array('type'=>ObjectModel::OBJECT_TYPE_TASK)); + } + + static function getDefaultForm() { + if (!isset(static::$form)) { + if (($o = static::objects()) && $o[0]) + static::$form = $o[0]; + } + + return static::$form; + } + + static function getInstance($object_id=0, $new=false) { + if ($new || !isset(static::$instance)) + static::$instance = static::getDefaultForm()->instanciate(); + + static::$instance->object_type = ObjectModel::OBJECT_TYPE_TASK; + + if ($object_id) + static::$instance->object_id = $object_id; + + return static::$instance; + } +} + +// Task thread class +class TaskThread extends ObjectThread { + + function addDescription($vars, &$errors=array()) { + + $vars['threadId'] = $this->getId(); + $vars['message'] = $vars['description']; + unset($vars['description']); + + return MessageThreadEntry::create($vars, $errors); + } + + static function create($task) { + $id = is_object($task) ? $task->getId() : $task; + return parent::create(array( + 'object_id' => $id, + 'object_type' => ObjectModel::OBJECT_TYPE_TASK + )); + } + +} +?> diff --git a/include/class.ticket.php b/include/class.ticket.php index 43c1ed87593ebfbd3acd76e592396288b3c6ae80..067167f64b2393f507189e51b29203a691ac0227 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -32,6 +32,7 @@ include_once(INCLUDE_DIR.'class.canned.php'); require_once(INCLUDE_DIR.'class.dynamic_forms.php'); require_once(INCLUDE_DIR.'class.user.php'); require_once(INCLUDE_DIR.'class.collaborator.php'); +require_once(INCLUDE_DIR.'class.task.php'); require_once(INCLUDE_DIR.'class.faq.php'); class TicketModel extends VerySimpleModel { @@ -188,11 +189,14 @@ class Ticket { $sql='SELECT ticket.*, thread.id as thread_id, lock_id, dept.name as dept_name ' .' ,count(distinct attach.attach_id) as attachments' + .' ,count(distinct task.id) as tasks' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.id) ' .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) ' .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON ( ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' + .' LEFT JOIN '.TASK_TABLE.' task + ON ( task.object_id = ticket.ticket_id AND task.object_type="T" ) ' .' LEFT JOIN '.THREAD_TABLE.' thread ON ( thread.object_id = ticket.ticket_id AND thread.object_type="T" ) ' .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry @@ -718,6 +722,10 @@ class Ticket { return $this->last_message; } + function getNumTasks() { + return $this->ht['tasks']; + } + function getThreadId() { return $this->ht['thread_id']; } diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml index 363b9abef8642f8587fa85bf4d19854a1c68a746..577dd5539694eea2cf66a02b493e13d56aa50660 100644 --- a/include/i18n/en_US/form.yaml +++ b/include/i18n/en_US/form.yaml @@ -181,3 +181,39 @@ configuration: rows: 4 cols: 40 +- type: A # notrans + title: Task Details + instructions: Please Describe The Issue + notes: | + This form is used to create tasks. + deletable: false + fields: + - type: text # notrans + name: title # notrans + label: Title + required: true + edit_mask: 15 + flags: 1 + sort: 1 + configuration: + size: 40 + length: 50 + - type: thread # notrans + name: description # notrans + label: Description + hint: Details on the reason(s) for creating the task. + required: true + edit_mask: 15 + flags: 3 + sort: 2 + - type: datetime # notrans + name: duedate # notrans + label: Due Date + required: false + edit_mask: 15 + flags: 3 + sort: 3 + configuration: + time: true + gmt: true + future: true diff --git a/include/i18n/en_US/sequence.yaml b/include/i18n/en_US/sequence.yaml index bb502c3526c287280a2fe664419989e26c9307ef..f67594aa91269309ddad0a470c3a3c07ac85e151 100644 --- a/include/i18n/en_US/sequence.yaml +++ b/include/i18n/en_US/sequence.yaml @@ -21,3 +21,10 @@ padding: '0' increment: 1 flags: 1 + +- id: 2 + name: "Tasks Sequence" + next: 1 + padding: '0' + increment: 1 + flags: 1 diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 252eb31ba78793be662ab22a616f2973246af4b4..73c2003f8c53fd3a92199db12c39bff375b68149 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -4b4daf9cf5e199673885f5ef58e743d1 +48b4499152a6c11e714d4a40fc33332a diff --git a/include/upgrader/streams/core/4b4daf9c-48b44991.patch.sql b/include/upgrader/streams/core/4b4daf9c-48b44991.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..7ceb7daed55117866ff95f206adb9eb513133a92 --- /dev/null +++ b/include/upgrader/streams/core/4b4daf9c-48b44991.patch.sql @@ -0,0 +1,61 @@ +/** + * @version v1.9.5 + * @signature 48b4499152a6c11e714d4a40fc33332a + * @title Add tasks + * + * This patch introduces the concept of tasks + * + */ + +-- create task task +CREATE TABLE `%TABLE_PREFIX%task` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) NOT NULL DEFAULT '0', + `object_type` char(1) NOT NULL, + `number` varchar(20) DEFAULT NULL, + `dept_id` int(10) unsigned NOT NULL DEFAULT '0', + `sla_id` int(10) unsigned NOT NULL DEFAULT '0', + `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `team_id` int(10) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `team_id` (`team_id`), + KEY `created` (`created`), + KEY `sla_id` (`sla_id`), + KEY `object` (`object_id`,`object_type`) +) DEFAULT CHARSET=utf8; + +-- Add flags field to form field +ALTER TABLE `%TABLE_PREFIX%form_field` + ADD `flags` INT UNSIGNED NOT NULL DEFAULT '1' AFTER `form_id`; + +-- Flag field stored in the system elsewhere as nonstorable locally. +UPDATE `%TABLE_PREFIX%form_field` A1 JOIN `%TABLE_PREFIX%form` A2 ON(A2.id=A1.form_id) + SET A1.`flags` = 3 + WHERE A2.`type` = 'U' AND A1.`name` IN('name','email'); + +UPDATE `%TABLE_PREFIX%form_field` A1 JOIN `%TABLE_PREFIX%form` A2 ON(A2.id=A1.form_id) + SET A1.`flags`=3 + WHERE A2.`type`='O' AND A1.`name` IN('name'); + +-- TODO: add ticket thread entry?? + + +-- rename ticket sequence numbering + +UPDATE `%TABLE_PREFIX%config` + SET `key` = 'ticket_number_format' + WHERE `key` = 'number_format' AND `namespace` = 'core'; + +UPDATE `%TABLE_PREFIX%config` + SET `key` = 'ticket_sequence_id' + WHERE `key` = 'sequence_id' AND `namespace` = 'core'; + +-- Set new schema signature +UPDATE `%TABLE_PREFIX%config` + SET `value` = '48b4499152a6c11e714d4a40fc33332a' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/include/upgrader/streams/core/4b4daf9c-48b44991.task.php b/include/upgrader/streams/core/4b4daf9c-48b44991.task.php new file mode 100644 index 0000000000000000000000000000000000000000..baf686b4d367228276c08710baa60916d55d47e1 --- /dev/null +++ b/include/upgrader/streams/core/4b4daf9c-48b44991.task.php @@ -0,0 +1,38 @@ +<?php +/* + * Import initial form for task + * + */ + +class TaskFormLoader extends MigrationTask { + var $description = "Loading initial data for tasks"; + + function run($max_time) { + global $cfg; + + // Load task form + require_once INCLUDE_DIR.'class.task.php'; + Task::__loadDefaultForm(); + // Load sequence for the task + $i18n = new Internationalization($cfg->get('system_language', 'en_US')); + $sequences = $i18n->getTemplate('sequence.yaml')->getData(); + foreach ($sequences as $s) { + if ($s['id'] != 2) continue; + unset($s['id']); + $sq=Sequence::create($s); + $sq->save(); + $sql= 'INSERT INTO '.CONFIG_TABLE + .' (`namespace`, `key`, `value`) ' + .' VALUES + ("core", "task_number_format", "###"), + ("core", "task_sequence_id",'.db_input($sq->id).')'; + db_query($sql); + break; + } + + } +} + +return 'TaskFormLoader'; + +?> diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index d91ce79ab86f5fed2f4d628a84c9a2b5ca1ec40a..b106d286f2a665cf2acc296ae222ffd2298d7aae 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -768,6 +768,28 @@ CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( UNIQUE KEY `collab` (`ticket_id`,`user_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%task`; +CREATE TABLE `%TABLE_PREFIX%task` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) NOT NULL DEFAULT '0', + `object_type` char(1) NOT NULL, + `number` varchar(20) DEFAULT NULL, + `dept_id` int(10) unsigned NOT NULL DEFAULT '0', + `sla_id` int(10) unsigned NOT NULL DEFAULT '0', + `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `team_id` int(10) unsigned NOT NULL DEFAULT '0', + `flags` int(10) unsigned NOT NULL DEFAULT '0', + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `dept_id` (`dept_id`), + KEY `staff_id` (`staff_id`), + KEY `team_id` (`team_id`), + KEY `created` (`created`), + KEY `sla_id` (`sla_id`), + KEY `object` (`object_id`,`object_type`) +) DEFAULT CHARSET=utf8; + -- pages CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%content` ( `id` int(10) unsigned NOT NULL auto_increment,