diff --git a/bootstrap.php b/bootstrap.php index 056c5f01dcd4098d7f8649be2211249a7b9f20f8..6977d3385f9321c1865f3f41222cdbbaceb004d3 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -102,6 +102,7 @@ class Bootstrap { define('THREAD_COLLABORATOR_TABLE', $prefix.'thread_collaborator'); define('TICKET_STATUS_TABLE', $prefix.'ticket_status'); define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); + define('EVENT_TABLE',$prefix.'event'); define('TASK_TABLE', $prefix.'task'); define('TASK_CDATA_TABLE', $prefix.'task__cdata'); diff --git a/include/class.i18n.php b/include/class.i18n.php index 17709bee89b112527e2158910641d18608c64555..ab692eb6292cd2a4181249920eadc04006054eeb 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -64,6 +64,7 @@ class Internationalization { 'ticket_status.yaml' => 'TicketStatus::__create', // Role 'role.yaml' => 'Role::__create', + 'event.yaml' => 'Event::__create', 'file.yaml' => 'AttachmentFile::__create', 'sequence.yaml' => 'Sequence::__create', 'queue_column.yaml' => 'QueueColumn::__create', diff --git a/include/class.report.php b/include/class.report.php index 12d7322d3dbc6665fed18b86d8b738d49eb091ba..dbff0ba38f68714692c091d9214b079223351360 100644 --- a/include/class.report.php +++ b/include/class.report.php @@ -77,26 +77,31 @@ class OverviewReport { function getPlotData() { list($start, $stop) = $this->getDateRange(); + $states = array("created", "closed", "reopened", "assigned", "overdue", "transferred"); + $event_ids = Event::getIds(); # Fetch all types of events over the timeframe - $res = db_query('SELECT DISTINCT(state) FROM '.THREAD_EVENT_TABLE + $res = db_query('SELECT DISTINCT(E.name) FROM '.THREAD_EVENT_TABLE + .' T JOIN '.EVENT_TABLE . ' E ON E.id = T.event_id' .' WHERE timestamp BETWEEN '.$start.' AND '.$stop - .' AND state IN ("created", "closed", "reopened", "assigned", "overdue", "transferred")' + .' AND T.event_id IN ('.implode(",",$event_ids).')' .' ORDER BY 1'); $events = array(); while ($row = db_fetch_row($res)) $events[] = $row[0]; # TODO: Handle user => db timezone offset # XXX: Implement annulled column from the %ticket_event table - $res = db_query('SELECT state, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), ' + $res = db_query('SELECT H.name, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), ' .'COUNT(DISTINCT T.id)' .' FROM '.THREAD_EVENT_TABLE. ' E ' + . ' LEFT JOIN '.EVENT_TABLE. ' H + ON (E.event_id = H.id)' .' JOIN '.THREAD_TABLE. ' T ON (T.id = E.thread_id AND T.object_type = "T") ' .' WHERE E.timestamp BETWEEN '.$start.' AND '.$stop .' AND NOT annulled' - .' AND E.state IN ("created", "closed", "reopened", "assigned", "overdue", "transferred")' - .' GROUP BY E.state, DATE_FORMAT(E.timestamp, \'%Y-%m-%d\')' + .' AND E.event_id IN ('.implode(",",$event_ids).')' + .' GROUP BY E.event_id, DATE_FORMAT(E.timestamp, \'%Y-%m-%d\')' .' ORDER BY 2, 1'); # Initialize array of plot values $plots = array(); @@ -139,6 +144,11 @@ class OverviewReport { function getTabularData($group='dept') { global $thisstaff; + $event_ids = Event::getIds(); + $event = function ($name) use ($event_ids) { + return $event_ids[$name]; + }; + list($start, $stop) = $this->getDateRange(); $times = ThreadEvent::objects() ->constrain(array( @@ -148,8 +158,8 @@ class OverviewReport { )) ->constrain(array( 'thread__events' => array( - 'thread__events__state' => 'created', - 'state' => 'closed', + 'thread__events__event_id' => $event('created'), + 'event_id' => $event('closed'), 'annulled' => 0, ), )) @@ -174,27 +184,27 @@ class OverviewReport { ->aggregate(array( 'Opened' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'created')), 1) + ->when(new Q(array('event_id' => $event('created'))), 1) ), 'Assigned' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'assigned')), 1) + ->when(new Q(array('event_id' => $event('assigned'))), 1) ), 'Overdue' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'overdue')), 1) + ->when(new Q(array('event_id' => $event('overdue'))), 1) ), 'Closed' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'closed')), 1) + ->when(new Q(array('event_id' => $event('closed'))), 1) ), 'Reopened' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'reopened')), 1) + ->when(new Q(array('event_id' => $event('reopened'))), 1) ), 'Deleted' => SqlAggregate::COUNT( SqlCase::N() - ->when(new Q(array('state' => 'deleted')), 1) + ->when(new Q(array('event_id' => $event('deleted'))), 1) ), )); diff --git a/include/class.thread.php b/include/class.thread.php index 3d367a902f496b69e54d4bc5fc3fd710a4ef3e0a..467ed6c0c46b2c981d662291a4bee88571b191de 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -2052,16 +2052,80 @@ class ThreadEvent extends VerySimpleModel { $subclasses[$class::$state] = $class; } } + $this->state = Event::getNameById($this->event_id); if (!($class = $subclasses[$this->state])) return $this; return new $class($this->ht); } } +class Event extends VerySimpleModel { + static $meta = array( + 'table' => EVENT_TABLE, + 'pk' => array('id'), + ); + + function getInfo() { + return $this->ht; + } + + function getId() { + return $this->id; + } + + function getName() { + return $this->name; + } + + function getNameById($id) { + return array_search($id, self::getIds()); + } + + function getIdByName($name) { + $ids = self::getIds(); + return $ids[$name] ?: 0; + } + + function getDescription() { + return $this->description; + } + + static function getIds() { + static $ids; + + if (!isset($ids)) { + $ids = array(); + $events = static::objects()->values_flat('id', 'name'); + foreach ($events as $row) { + list($id, $name) = $row; + $ids[$name] = $id; + } + } + + return $ids; + } + + static function create($vars=false, &$errors=array()) { + $event = new static($vars); + return $event; + } + + static function __create($vars, &$errors=array()) { + $event = self::create($vars); + $event->save(); + return $event; + } + + function save($refetch=false) { + return parent::save($refetch); + } +} + class ThreadEvents extends InstrumentedList { function annul($event) { + $event_id = Event::getIdByName($event); $this->queryset - ->filter(array('state' => $event)) + ->filter(array('event_id' => $event_id)) ->update(array('annulled' => 1)); } @@ -2116,7 +2180,7 @@ class ThreadEvents extends InstrumentedList { } } $event->username = $username; - $event->state = $state; + $event->event_id = Event::getIdByName($state); if ($data) { if (is_array($data)) diff --git a/include/class.user.php b/include/class.user.php index 0fa35e4a14d62514632bcf163f80fc25e216b49c..f2d3e2fd0e8c9e4cff961248ce1abe51a062a646 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -635,7 +635,8 @@ implements TemplateVariable, Searchable { } function deleteAllTickets() { - $deleted = TicketStatus::lookup(array('state' => 'deleted')); + $event_id = Event::getIdByName('deleted'); + $deleted = TicketStatus::lookup(array('event_id' => $event_id)); foreach($this->tickets as $ticket) { if (!$T = Ticket::lookup($ticket->getId())) continue; diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php index 93843210969e4e3d79aea05b4ad692805dc5f95f..8457698caf820605482ee839492d4001fc6fbb55 100644 --- a/include/client/templates/thread-entries.tmpl.php +++ b/include/client/templates/thread-entries.tmpl.php @@ -1,6 +1,12 @@ <?php +$states = array('created', 'closed', 'reopened', 'edited', 'collab'); +$event_ids = array(); +foreach ($states as $state) { + $eid = Event::getIdByName($state); + $event_ids[] = $eid; +} $events = $events - ->filter(array('state__in' => array('created', 'closed', 'reopened', 'edited', 'collab'))) + ->filter(array('event_id__in' => $event_ids)) ->order_by('id'); $events = new IteratorIterator($events->getIterator()); $events->rewind(); diff --git a/include/i18n/en_US/event.yaml b/include/i18n/en_US/event.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7c9c8ed5bb93d531a50a21f6b84f6a4caf0f07ee --- /dev/null +++ b/include/i18n/en_US/event.yaml @@ -0,0 +1,61 @@ +# +# event.yaml +# +# Events initially inserted into the system. +# +--- +- id: 1 + name: created + description: + +- id: 2 + name: closed + description: + +- id: 3 + name: reopened + description: + +- id: 4 + name: assigned + description: + +- id: 5 + name: released + description: + +- id: 6 + name: transferred + description: + +- id: 7 + name: referred + description: + +- id: 8 + name: overdue + description: + +- id: 9 + name: edited + description: + +- id: 10 + name: viewed + description: + +- id: 11 + name: error + description: + +- id: 12 + name: collab + description: + +- id: 13 + name: resent + description: + +- id: 14 + name: deleted + description: diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index fbebb45e63ee1f85f4388d1ead81fa6b7403dd4f..00b20916567f6418d4c640ae0895c26b3499d242 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -26fd79dc5443f37779f9d2c4108058f4 +00c949a623b82848baaf3480b51307e3 diff --git a/include/upgrader/streams/core/0ca85857-86707325.patch.sql b/include/upgrader/streams/core/0ca85857-86707325.patch.sql index 2962d23e40f284614bd877ee0317e1183f7a21bf..2daaf3b7cdab96ac80265205de51dbe6fc0926ef 100644 --- a/include/upgrader/streams/core/0ca85857-86707325.patch.sql +++ b/include/upgrader/streams/core/0ca85857-86707325.patch.sql @@ -17,10 +17,6 @@ CREATE TABLE `%TABLE_PREFIX%thread_referral` ( KEY `thread_id` (`thread_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -ALTER TABLE `%TABLE_PREFIX%thread_event` - CHANGE `state` `state` enum('created','closed','reopened','assigned','transferred', 'referred', 'overdue','edited','viewed','error','collab','resent', 'deleted') NOT NULL; - - -- Finished with patch UPDATE `%TABLE_PREFIX%config` SET `value` = '86707325fc571e56242fccc46fd24466' diff --git a/include/upgrader/streams/core/26fd79dc-00c949a6.cleanup.sql b/include/upgrader/streams/core/26fd79dc-00c949a6.cleanup.sql new file mode 100644 index 0000000000000000000000000000000000000000..50cad9d04f5cb4a2ee7b773c2841637a6659a2de --- /dev/null +++ b/include/upgrader/streams/core/26fd79dc-00c949a6.cleanup.sql @@ -0,0 +1,3 @@ +-- Drop the state field from thread_events +ALTER TABLE `%TABLE_PREFIX%thread_event` + DROP COLUMN `state`; diff --git a/include/upgrader/streams/core/26fd79dc-00c949a6.patch.sql b/include/upgrader/streams/core/26fd79dc-00c949a6.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..774e248b961eee5cf30ee8459e3ca76496e27059 --- /dev/null +++ b/include/upgrader/streams/core/26fd79dc-00c949a6.patch.sql @@ -0,0 +1,45 @@ +/** +* @signature 00c949a623b82848baaf3480b51307e3 +* @version v1.11.0 +* @title Database Optimization +* +* This patch is for optimizing our database to handle large amounts of data +* more smoothly. +* +* 1. remove states in thread_event table and add them to their own event table +*/ + +-- Create a new table to store events +CREATE TABLE `%TABLE_PREFIX%event` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(60) NOT NULL, + `description` varchar(60) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%event` (`id`, `name`, `description`) +VALUES + (1,'created',''), + (2,'closed',''), + (3,'reopened',''), + (4,'assigned',''), + (5,'released',''), + (6,'transferred',''), + (7,'referred',''), + (8,'overdue',''), + (9,'edited',''), + (10,'viewed',''), + (11,'error',''), + (12,'collab',''), + (13,'resent',''), + (14,'deleted',''); + +-- Add event_id column to thread_events +ALTER TABLE `%TABLE_PREFIX%thread_event` + ADD `event_id` int(11) unsigned AFTER `thread_id`; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '00c949a623b82848baaf3480b51307e3', `updated` = NOW() + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/include/upgrader/streams/core/26fd79dc-00c949a6.task.php b/include/upgrader/streams/core/26fd79dc-00c949a6.task.php new file mode 100644 index 0000000000000000000000000000000000000000..d3d6a2084ac31d8cab76465794c8992a3d6e69e3 --- /dev/null +++ b/include/upgrader/streams/core/26fd79dc-00c949a6.task.php @@ -0,0 +1,143 @@ +<?php + +class EventEnumRemoval extends MigrationTask { + var $description = "Remove the Enum 'state' field from ThreadEvents"; + var $queue; + var $skipList; + var $errorList = array(); + + function sleep() { + return array('queue'=>$this->queue, 'skipList'=>$this->skipList); + } + function wakeup($stuff) { + $this->queue = $stuff['queue']; + $this->skipList = $stuff['skipList']; + while (!$this->isFinished()) + $this->do_batch(30, 5000); + } + + function run($max_time) { + $this->do_batch($max_time * 0.9, 5000); + } + + function isFinished() { + return $this->getQueueLength() == 0; + } + + function do_batch($time=30, $max=0) { + if(!$this->queueEvents($max) || !$this->getQueueLength()) + return 0; + + $this->setStatus("{$this->getQueueLength()} events remaining"); + + $count = 0; + $start = Misc::micro_time(); + while ($this->getQueueLength() && (Misc::micro_time()-$start) < $time) { + if($this->next() && $max && ++$count>=$max) { + break; + } + } + + return $this->queueEvents($max); + } + + function queueEvents($limit=0){ + global $cfg, $ost; + + # Since the queue is persistent - we want to make sure we get to empty + # before we find more events. + if(($qc=$this->getQueueLength())) + return $qc; + + $sql = "SELECT this.id, this.state, that.id, that.name FROM ".THREAD_EVENT_TABLE. " this + INNER JOIN ( + SELECT id, name + FROM ". EVENT_TABLE. ") that + WHERE this.state = that.name + AND event_id IS NULL"; + + if(($skipList=$this->getSkipList())) + $sql.= ' AND this.id NOT IN('.implode(',', db_input($skipList)).')'; + + if($limit && is_numeric($limit)) + $sql.=' LIMIT '.$limit; + + //XXX: Do a hard fail or error querying the database? + if(!($res=db_query($sql))) + return $this->error('Unable to query DB for Thread Event migration!'); + + // Force the log message to the database + $ost->logDebug("Thread Event Migration", 'Found '.db_num_rows($res) + .' events to migrate', true); + + if(!db_num_rows($res)) + return 0; //Nothing else to do!! + + $this->queue = array(); + while (list($id, $state, $eventId, $eventName)=db_fetch_row($res)) { + $info=array( + 'id' => $id, + 'state' => $state, + 'eventId' => $eventId, + 'eventName' => $eventName, + ); + $this->enqueue($info); + } + + return $this->getQueueLength(); + } + + function skip($eventId, $error) { + $this->skipList[] = $eventId; + + return $this->error($error." (ID #$eventId)"); + } + + function error($what) { + global $ost; + + $this->errors++; + $this->errorList[] = $what; + // Log the error but don't send the alert email + $ost->logError('Upgrader: Thread Event Migrater', $what, false); + # Assist in returning FALSE for inline returns with this method + return false; + } + + function getErrors() { + return $this->errorList; + } + + function getSkipList() { + return $this->skipList; + } + + function enqueue($info) { + $this->queue[] = $info; + } + + function getQueueLength() { + return count($this->queue); + } + + function next() { + # Fetch next item -- use the last item so the array indices don't + # need to be recalculated for every shift() operation. + $info = array_pop($this->queue); + + if (!$info['state']) { + # Continue with next thread event + return $this->skip($info['eventId'], + sprintf('Thread Event ID %s: State is blank', $info['eventId'])); + } + + db_query('update '.THREAD_EVENT_TABLE + .' set event_id='.db_input($info['eventId']) + .' where state='.db_input($info['eventName']) + .' and id='.db_input($info['id'])); + + return true; + } +} +return 'EventEnumRemoval'; +?> diff --git a/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql b/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql index f3bc7e0b201ea76bb59c6ff4de8f1ed272740d82..fd5953f5b6870f0cd53ffd5e42f8e36ea0019670 100644 --- a/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql +++ b/include/upgrader/streams/core/70921d5c-26fd79dc.patch.sql @@ -5,10 +5,6 @@ * * This patch is for final revisions needed for v1.11 */ - -ALTER TABLE `%TABLE_PREFIX%thread_event` - CHANGE `state` `state` enum('created','closed','reopened','assigned', 'released', 'transferred', 'referred', 'overdue','edited','viewed','error','collab','resent', 'deleted') NOT NULL; - ALTER TABLE `%TABLE_PREFIX%attachment` ADD INDEX `file_object` (`file_id`,`object_id`); diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index fbbb6e45050facca4ad2c6390657073d14b23c96..d023e83ca506ac49e41578256a0a7ead0cb41766 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -711,15 +711,24 @@ CREATE TABLE `%TABLE_PREFIX%lock` ( KEY `staff_id` (`staff_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%event`; +CREATE TABLE `%TABLE_PREFIX%event` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(60) NOT NULL, + `description` varchar(60) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_event`; CREATE TABLE `%TABLE_PREFIX%thread_event` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `thread_id` int(11) unsigned NOT NULL default '0', + `event_id` int(11) unsigned DEFAULT NULL, `staff_id` int(11) unsigned NOT NULL, `team_id` int(11) unsigned NOT NULL, `dept_id` int(11) unsigned NOT NULL, `topic_id` int(11) unsigned NOT NULL, - `state` enum('created','closed','reopened','assigned','released','transferred', 'referred', 'overdue','edited','viewed','error','collab','resent', 'deleted') NOT NULL, `data` varchar(1024) DEFAULT NULL COMMENT 'Encoded differences', `username` varchar(128) NOT NULL default 'SYSTEM', `uid` int(11) unsigned DEFAULT NULL, @@ -727,8 +736,8 @@ CREATE TABLE `%TABLE_PREFIX%thread_event` ( `annulled` tinyint(1) unsigned NOT NULL default '0', `timestamp` datetime NOT NULL, PRIMARY KEY (`id`), - KEY `ticket_state` (`thread_id`, `state`, `timestamp`), - KEY `ticket_stats` (`timestamp`, `state`) + KEY `ticket_state` (`thread_id`, `event_id`, `timestamp`), + KEY `ticket_stats` (`timestamp`, `event_id`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_referral`;