diff --git a/bootstrap.php b/bootstrap.php
index 2703395faa6a5548847cfa4590cbbf0f4067bc19..8b7ea7eb9e20e2e4e74ee554242ac110afc861ee 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');
@@ -293,6 +294,10 @@ class Bootstrap {
         }
         if (extension_loaded('iconv'))
             iconv_set_encoding('internal_encoding', 'UTF-8');
+
+        function mb_str_wc($str) {
+            return count(preg_split('~[^\p{L}\p{N}\'].+~u', trim($str)));
+        }
     }
 
     function croak($message) {
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index 9ca601e33020d9c0f3ea1e669bbc4d405217df5e..c25570e095754f8e4237de353c817aae8259269e 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -32,7 +32,8 @@ class DynamicFormsAjaxAPI extends AjaxController {
             if (!$form->hasAnyVisibleFields())
                 continue;
             ob_start();
-            $form->getForm($_SESSION[':form-data'])->render(!$client);
+            $form->getForm($_SESSION[':form-data'])->render(!$client, false,
+                    array('mode' => 'create'));
             $html .= ob_get_clean();
             ob_start();
             print $form->getMedia();
diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php
index 9c2a2b207c11ba72e8b6c7fa259670d0b4b6a0d7..d3ffc8b9dec2b994ae95efd133104a4860d308dc 100644
--- a/include/ajax.orgs.php
+++ b/include/ajax.orgs.php
@@ -93,7 +93,7 @@ class OrgsAjaxAPI extends AjaxController {
 
         $errors = array();
         if($org->update($_POST, $errors))
-             Http::response(201, $org->to_json());
+             Http::response(201, $org->to_json(), 'application/json');
 
         $forms = $org->getForms();
 
@@ -162,7 +162,7 @@ class OrgsAjaxAPI extends AjaxController {
             }
 
             if (!$info['error'] && $user && $user->setOrganization($org))
-                Http::response(201, $user->to_json());
+                Http::response(201, $user->to_json(), 'application/json');
             elseif (!$info['error'])
                 $info['error'] = sprintf('%s - %s', __('Unable to add user to the organization'), __('Please try again!'));
 
@@ -230,7 +230,7 @@ class OrgsAjaxAPI extends AjaxController {
         if ($_POST) {
             $form = OrganizationForm::getDefaultForm()->getForm($_POST);
             if (($org = Organization::fromForm($form)))
-                Http::response(201, $org->to_json());
+                Http::response(201, $org->to_json(), 'application/json');
 
             $info = array('error' =>sprintf('%s - %s', __('Error adding organization'), __('Please try again!')));
         }
@@ -274,7 +274,7 @@ class OrgsAjaxAPI extends AjaxController {
             $info += array('title' => __('Organization Lookup'));
 
         if ($_POST && ($org = Organization::lookup($_POST['orgid']))) {
-            Http::response(201, $org->to_json());
+            Http::response(201, $org->to_json(), 'application/json');
         }
 
         ob_start();
diff --git a/include/ajax.search.php b/include/ajax.search.php
index 93da251e7d164e9548550dce3b35cf16051bbcf3..8ac463aa0e247f2f7ddef5c85915848494464731 100644
--- a/include/ajax.search.php
+++ b/include/ajax.search.php
@@ -135,7 +135,7 @@ class SearchAjaxAPI extends AjaxController {
             $_SESSION[$key] = $keep;
         }
     }
-    
+
     function _hashCriteria($criteria, $size=10) {
         $parts = array();
         foreach ($criteria as $C) {
@@ -394,9 +394,8 @@ class SearchAjaxAPI extends AjaxController {
         $criteria = array();
         if ($ids && is_array($ids))
             $criteria = array('id__in' => $ids);
-
-        $counts = SavedQueue::ticketsCount($thisstaff, $criteria, 'q');
+        $counts = SavedQueue::counts($thisstaff, true, $criteria);
         Http::response(200, false, 'application/json');
-        return $this->encode($counts);
+        return $this->encode(Format::number($counts));
     }
 }
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 2864efda74a09e4c5ceed50e699f15edbff6f5ef..3338ea4c536f334dcf73df470ecb62d2182b6e37 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -1256,6 +1256,70 @@ function refer($tid, $target=null) {
         return self::_changeSelectedTicketsStatus($state, $info, $errors);
     }
 
+    function markAs($tid, $action='') {
+        global $thisstaff;
+
+        // Standard validation
+        if (!($ticket=Ticket::lookup($tid)))
+            Http::response(404, __('No such ticket'));
+
+        if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_REPLY) && !$thisstaff->isManager())
+            Http::response(403, __('Permission denied'));
+
+        $errors = array();
+        $info = array(':title' => __('Please Confirm'));
+
+        // Instantiate form for comment field
+        $form = MarkAsForm::instantiate($_POST);
+
+        // Mark as answered or unanswered
+        if ($_POST) {
+            switch($action) {
+                case 'answered':
+                    if($ticket->isAnswered())
+                        $errors['err'] = __('Ticket is already marked as answered');
+                    elseif (!$ticket->markAnswered())
+                        $errors['err'] = __('Cannot mark ticket as answered');
+                    break;
+
+                case 'unanswered':
+                    if(!$ticket->isAnswered())
+                        $errors['err'] = __('Ticket is already marked as unanswered');
+                    elseif (!$ticket->markUnanswered())
+                        $errors['err'] - __('Cannot mark ticket as unanswered');
+                    break;
+
+                default:
+                    Http::response(404, __('Unknown action'));
+            }
+
+            // Retrun errors to form (if any)
+            if($errors) {
+                $info['error'] = $errors['err'] ?: sprintf(__('Unable to mark ticket as %s'), $action);
+                $form->addErrors($errors);
+            } else {
+                // Add comment (if provided)
+                $comments = $form->getComments();
+                if ($comments) {
+                    $title = __(sprintf('Ticket Marked %s', ucfirst($action)));
+                    $_errors = array();
+
+                    $ticket->postNote(
+                        array('note' => $comments, 'title' => $title),
+                        $_errors, $thisstaff, false);
+                }
+
+                // Add success messages and log activity
+                $_SESSION['::sysmsgs']['msg'] = sprintf(__('Ticket marked as %s successfully'), $action);
+                $msg = sprintf(__('Ticket flagged as %s by %s'), $action, $thisstaff->getName());
+                $ticket->logActivity(sprintf(__('Ticket Marked %s'), ucfirst($action)), $msg);
+                Http::response(201, $ticket->getId());
+            }
+        }
+
+        include STAFFINC_DIR . 'templates/mark-as.tmpl.php';
+    }
+
     function triggerThreadAction($ticket_id, $thread_id, $action) {
         $thread = ThreadEntry::lookup($thread_id);
         if (!$thread)
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 328b93cf0e1d1167797ad29fcda7a0d6a1b8da8b..459c4654286a5ad2bac46bcf1aba222caaf2f380 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -168,7 +168,7 @@ class UsersAjaxAPI extends AjaxController {
 
         $errors = array();
         if ($user->updateInfo($_POST, $errors, true) && !$errors)
-             Http::response(201, $user->to_json());
+             Http::response(201, $user->to_json(),  'application/json');
 
         $forms = $user->getForms();
         include(STAFFINC_DIR . 'templates/user.tmpl.php');
@@ -271,7 +271,7 @@ class UsersAjaxAPI extends AjaxController {
     function getUser($id=false) {
 
         if(($user=User::lookup(($id) ? $id : $_REQUEST['id'])))
-           Http::response(201, $user->to_json());
+           Http::response(201, $user->to_json(), 'application/json');
 
         $info = array('error' => sprintf(__('%s: Unknown or invalid ID.'), _N('end user', 'end users', 1)));
 
@@ -297,7 +297,7 @@ class UsersAjaxAPI extends AjaxController {
             $info['title'] = __('Add New User');
             $form = UserForm::getUserForm()->getForm($_POST);
             if (($user = User::fromForm($form)))
-                Http::response(201, $user->to_json());
+                Http::response(201, $user->to_json(), 'application/json');
 
             $info['error'] = sprintf('%s - %s', __('Error adding user'), __('Please try again!'));
         }
@@ -433,7 +433,7 @@ class UsersAjaxAPI extends AjaxController {
             }
 
             if ($org && $user->setOrganization($org))
-                Http::response(201, $org->to_json());
+                Http::response(201, $org->to_json(), 'application/json');
             elseif (! $info['error'])
                 $info['error'] = __('Unable to add user to organization.')
                     .' '.__('Correct any errors below and try again.');
diff --git a/include/api.tickets.php b/include/api.tickets.php
index c209086a9d57a682462f2d2966c5fd41db69b176..8d20e391cfa1aebb66ad5d154a16f2fd1510d1ef 100644
--- a/include/api.tickets.php
+++ b/include/api.tickets.php
@@ -14,7 +14,8 @@ class TicketApiController extends ApiController {
             "attachments" => array("*" =>
                 array("name", "type", "data", "encoding", "size")
             ),
-            "message", "ip", "priorityId"
+            "message", "ip", "priorityId",
+            "system_emails", "thread_entry_recipients"
         );
         # Fetch dynamic form field names for the given help topic and add
         # the names to the supported request structure
diff --git a/include/class.client.php b/include/class.client.php
index 11b500e4aef370bf60846024ada0cf8cf645df55..2d157ede502923d5e88fcbfa90f79924346c30b7 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -59,19 +59,20 @@ implements EmailContact, ITicketUser, TemplateVariable {
         case 'ticket_link':
             $qstr = array();
             if ($cfg && $cfg->isAuthTokenEnabled()
-                    && ($ticket=$this->getTicket())
-                    && !$ticket->getThread()->getNumCollaborators()) {
-                      $qstr['auth'] = $ticket->getAuthToken($this);
-                      return sprintf('%s/view.php?%s',
-                              $cfg->getBaseUrl(),
-                              Http::build_query($qstr, false)
-                              );
-                    }
-                    else {
-                      return sprintf('%s/tickets.php?id=%s',
-                              $cfg->getBaseUrl(),
-                              $ticket->getId()
-                              );
+                    && ($ticket=$this->getTicket())) {
+                      if (!$ticket->getThread()->getNumCollaborators()) {
+                          $qstr['auth'] = $ticket->getAuthToken($this);
+                          return sprintf('%s/view.php?%s',
+                               $cfg->getBaseUrl(),
+                               Http::build_query($qstr, false)
+                               );
+                      }
+                      else {
+                          return sprintf('%s/tickets.php?id=%s',
+                               $cfg->getBaseUrl(),
+                               $ticket->getId()
+                               );
+                      }
                     }
 
 
diff --git a/include/class.config.php b/include/class.config.php
index 004a1e8e8a44d2be9fdcc1cde54acd0a03300d73..8cdf808e8838c6e19f1d73d1c97f0dc7094df76c 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -30,10 +30,6 @@ class Config {
     # new settings and the corresponding default values.
     var $defaults = array();                # List of default values
 
-
-    # Items
-    var $items = null;
-
     function __construct($section=null, $defaults=array()) {
         if ($section)
             $this->section = $section;
@@ -133,18 +129,11 @@ class Config {
 
     function destroy() {
         unset($this->session);
-        if ($this->items)
-            $this->items->delete();
-
-        return true;
+        return $this->items()->delete() > 0;
     }
 
     function items() {
-
-        if (!isset($this->items))
-            $this->items = ConfigItem::items($this->section, $this->section_column);
-
-        return $this->items;
+        return ConfigItem::items($this->section, $this->section_column);
     }
 }
 
@@ -778,6 +767,10 @@ class OsticketConfig extends Config {
         return $sequence;
     }
 
+    function showTopLevelTicketCounts() {
+        return ($this->get('queue_bucket_counts'));
+    }
+
     function getDefaultTicketNumberFormat() {
         return $this->get('ticket_number_format');
     }
@@ -1288,6 +1281,7 @@ class OsticketConfig extends Config {
         return $this->updateAll(array(
             'ticket_number_format'=>$vars['ticket_number_format'] ?: '######',
             'ticket_sequence_id'=>$vars['ticket_sequence_id'] ?: 0,
+            'queue_bucket_counts'=>isset($vars['queue_bucket_counts'])?1:0,
             'default_priority_id'=>$vars['default_priority_id'],
             'default_help_topic'=>$vars['default_help_topic'],
             'default_ticket_status_id'=>$vars['default_ticket_status_id'],
diff --git a/include/class.dept.php b/include/class.dept.php
index fe8ded86e6aba9c5371c6ea6b46e1e861d7bf62e..102e87030e48f0debdbb40e311f29a45938768c4 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -962,7 +962,7 @@ extends Form {
         );
     }
 
-    function getClean() {
+    function getClean($validate = true) {
         $clean = parent::getClean();
 
         $clean['ispublic'] = !$clean['private'];
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 7d654ed053f2998185e27e89e1f055a11348d2fd..75c3e9904136dcab723426ed2159f6a07007fd23 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -1098,16 +1098,18 @@ class DynamicFormEntry extends VerySimpleModel {
         return !$this->_errors;
     }
 
-    function isValidForClient() {
-        $filter = function($f) {
-            return $f->isVisibleToUsers();
+    function isValidForClient($update=false) {
+        $filter = function($f) use($update) {
+            return $update ? $f->isEditableToUsers() :
+                $f->isVisibleToUsers();
         };
         return $this->isValid($filter);
     }
 
-    function isValidForStaff() {
-        $filter = function($f) {
-            return $f->isVisibleToStaff();
+    function isValidForStaff($update=false) {
+        $filter = function($f) use($update) {
+            return $update ? $f->isEditableToStaff() :
+                $f->isVisibleToStaff();
         };
         return $this->isValid($filter);
     }
@@ -1181,7 +1183,8 @@ class DynamicFormEntry extends VerySimpleModel {
     }
 
     function render($options=array()) {
-        $options += array('staff' => true);
+        if (is_array($options))
+            $options += array('staff' => true);
         return $this->getForm()->render($options);
     }
 
@@ -1238,6 +1241,15 @@ class DynamicFormEntry extends VerySimpleModel {
         }
     }
 
+    /**
+     * Save the form entry and all associated answers.
+     *
+     */
+
+    function save($refetch=false) {
+        return $this->saveAnswers(null, $refetch);
+    }
+
     /**
      * Save the form entry and all associated answers.
      *
@@ -1245,7 +1257,8 @@ class DynamicFormEntry extends VerySimpleModel {
      * (mixed) FALSE if updated failed, otherwise the number of dirty answers
      * which were save is returned (which may be ZERO).
      */
-    function save($refetch=false) {
+
+    function saveAnswers($isEditable=null, $refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
 
@@ -1255,12 +1268,12 @@ class DynamicFormEntry extends VerySimpleModel {
         $dirty = 0;
         foreach ($this->getAnswers() as $a) {
             $field = $a->getField();
-
             // Don't save answers for presentation-only fields or fields
-            // which are stored elsewhere
-            if (!$field->hasData() || !$field->isStorable()
-                || $field->isPresentationOnly()
-            ) {
+            // which are stored elsewhere or those which are not editable
+            if (!$field->hasData()
+                    || !$field->isStorable()
+                    || $field->isPresentationOnly()
+                    || ($isEditable && !$isEditable($field))) {
                 continue;
             }
             // Set the entry here so that $field->getClean() can use the
diff --git a/include/class.export.php b/include/class.export.php
index 4fc1bbf7bd1c2bcf00412ca74f1c45223fae4f2f..2ea048ae92996d72523d50f28849eb6a2cc85490 100644
--- a/include/class.export.php
+++ b/include/class.export.php
@@ -76,7 +76,7 @@ class Export {
                     ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id'))),
                 'reopen_count' => TicketThread::objects()
                     ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
-                    ->filter(array('events__annulled' => 0, 'events__state' => 'reopened'))
+                    ->filter(array('events__annulled' => 0, 'events__event_id' => Event::getIdByName('reopened')))
                     ->aggregate(array('count' => SqlAggregate::COUNT('events__id'))),
                 'thread_count' => TicketThread::objects()
                     ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
diff --git a/include/class.faq.php b/include/class.faq.php
index 555fd3aee7f6f01fb485259e338c13c394dc826b..38e20b55cfa14b176eeae22913f96448e8b850d3 100644
--- a/include/class.faq.php
+++ b/include/class.faq.php
@@ -405,7 +405,7 @@ class FAQ extends VerySimpleModel {
         }
 
         $images = Draft::getAttachmentIds($vars['answer']);
-        $images = array_map(function($i) { return $i['id']; }, $images);
+        $images = array_flip(array_map(function($i) { return $i['id']; }, $images));
         $this->getAttachments()->keepOnlyFileIds($images, true);
 
         // Handle language-specific attachments
diff --git a/include/class.file.php b/include/class.file.php
index e61b5afe76516fcecfd880359bb6193c13164b4e..419a2820922f6405f83d236b8cc47934f566dbc5 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -197,6 +197,18 @@ class AttachmentFile extends VerySimpleModel {
                 $options);
     }
 
+    // Generates full download URL for external sources.
+    // e.g. https://domain.tld/file.php?args=123
+    function getExternalDownloadUrl($options=array()) {
+        global $cfg;
+
+        $download = self::getDownloadUrl($options);
+        // Separate URL handle and args
+        list($handle, $args) = explode('file.php?', $download);
+
+        return (string) rtrim($cfg->getBaseUrl(), '/').'/file.php?'.$args;
+    }
+
     static function generateDownloadUrl($id, $key, $hash, $options = array()) {
 
         // Expire at the nearest midnight, allow at least12 hrs access
@@ -258,7 +270,9 @@ class AttachmentFile extends VerySimpleModel {
         $type = $this->getType() ?: 'application/octet-stream';
         if (isset($_REQUEST['overridetype']))
             $type = $_REQUEST['overridetype'];
-        Http::download($name ?: $this->getName(), $type, null, 'inline');
+        elseif (!strcasecmp($disposition, 'attachment'))
+            $type = 'application/octet-stream';
+        Http::download($name ?: $this->getName(), $type, null, $disposition);
         header('Content-Length: '.$this->getSize());
         $this->sendData(false);
         exit();
diff --git a/include/class.format.php b/include/class.format.php
index a2d72340dcf02e3cba45990e4e455ab8146604e3..216cb6e630ea42be0d8ca88bdca19a372c33b182 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -503,6 +503,22 @@ class Format {
         return implode( $separator, $string );
     }
 
+    function number($number, $locale=false) {
+        if (is_array($number))
+            return array_map(array('Format','number'), $number);
+
+        if (!is_numeric($number))
+            return $number;
+
+        if (extension_loaded('intl') && class_exists('NumberFormatter')) {
+            $nf = NumberFormatter::create($locale ?: Internationalization::getCurrentLocale(),
+                NumberFormatter::DECIMAL);
+            return $nf->format($number);
+        }
+
+        return number_format((int) $number);
+    }
+
     /* elapsed time */
     function elapsedTime($sec) {
 
@@ -667,20 +683,20 @@ class Format {
             '%x', $timezone ?: $cfg->getTimezone(), $user);
     }
 
-    function datetime($timestamp, $fromDb=true, $timezone=false, $user=false) {
+    function datetime($timestamp, $fromDb=true, $format=false,  $timezone=false, $user=false) {
         global $cfg;
 
         return self::__formatDate($timestamp,
-                $cfg->getDateTimeFormat(), $fromDb,
+                $format ?: $cfg->getDateTimeFormat(), $fromDb,
                 IDF_SHORT, IDF_SHORT,
                 '%x %X', $timezone ?: $cfg->getTimezone(), $user);
     }
 
-    function daydatetime($timestamp, $fromDb=true, $timezone=false, $user=false) {
+    function daydatetime($timestamp, $fromDb=true, $format=false,  $timezone=false, $user=false) {
         global $cfg;
 
         return self::__formatDate($timestamp,
-                $cfg->getDayDateTimeFormat(), $fromDb,
+                $format ?: $cfg->getDayDateTimeFormat(), $fromDb,
                 IDF_FULL, IDF_SHORT,
                 '%x %X', $timezone ?: $cfg->getTimezone(), $user);
     }
@@ -1001,11 +1017,11 @@ implements TemplateVariable {
         case 'short':
             return Format::date($this->date, $this->fromdb, false, $this->timezone, $this->user);
         case 'long':
-            return Format::datetime($this->date, $this->fromdb, $this->timezone, $this->user);
+            return Format::datetime($this->date, $this->fromdb, false, $this->timezone, $this->user);
         case 'time':
             return Format::time($this->date, $this->fromdb, false, $this->timezone, $this->user);
         case 'full':
-            return Format::daydatetime($this->date, $this->fromdb, $this->timezone, $this->user);
+            return Format::daydatetime($this->date, $this->fromdb, false, $this->timezone, $this->user);
         }
     }
 
diff --git a/include/class.forms.php b/include/class.forms.php
index 4fb4de4609802681c71b57d2c9b8009d66d0aa77..d199d22a64e07a7cb97b562139d8aab2317286df 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -43,7 +43,7 @@ class Form {
             $this->id = $options['id'];
 
         // Use POST data if source was not specified
-        $this->_source = ($source) ? $source : $_POST;
+        $this->_source = $source ?: $_POST;
     }
 
     function getFormId() {
@@ -89,6 +89,28 @@ class Form {
         return $this->getField($name);
     }
 
+    function hasAnyEnabledFields() {
+        return $this->hasAnyVisibleFields(false);
+    }
+
+    function hasAnyVisibleFields($user=false) {
+        $visible = 0;
+        $isstaff = $user instanceof Staff;
+        foreach ($this->getFields() as $F) {
+            if (!$user) {
+                // Assume hasAnyEnabledFields
+                if ($F->isEnabled())
+                    $visible++;
+            } elseif($isstaff) {
+                if ($F->isVisibleToStaff())
+                    $visible++;
+            } elseif ($F->isVisibleToUsers()) {
+                $visible++;
+            }
+        }
+        return $visible > 0;
+    }
+
     function getTitle() { return $this->title; }
     function getInstructions() { return $this->instructions; }
     function getSource() { return $this->_source; }
@@ -120,7 +142,7 @@ class Form {
         }
     }
 
-    function getClean() {
+    function getClean($validate=true) {
         if (!$this->_clean) {
             $this->_clean = array();
             foreach ($this->getFields() as $key=>$field) {
@@ -131,7 +153,7 @@ class Form {
                 if (is_int($key) && $field->get('id'))
                     $key = $field->get('id');
                 $this->_clean[$key] = $this->_clean[$field->get('name')]
-                    = $field->getClean();
+                    = $field->getClean($validate);
             }
             unset($this->_clean[""]);
         }
@@ -606,7 +628,7 @@ class FormField {
      * submitted via POST, in order to kick off parsing and validation of
      * user-entered data.
      */
-    function getClean() {
+    function getClean($validate=true) {
         if (!isset($this->_clean)) {
             $this->_clean = (isset($this->value))
                 // XXX: The widget value may be parsed already if this is
@@ -628,7 +650,7 @@ class FormField {
             if (!isset($this->_clean) && ($d = $this->get('default')))
                 $this->_clean = $d;
 
-            if ($this->isVisible())
+            if ($this->isVisible() && $validate)
                 $this->validateEntry($this->_clean);
         }
         return $this->_clean;
@@ -836,7 +858,7 @@ class FormField {
      * Returns an HTML friendly value for the data in the field.
      */
     function display($value) {
-        return Format::htmlchars($this->toString($value));
+        return Format::htmlchars($this->toString($value ?: $this->value));
     }
 
     /**
@@ -1960,6 +1982,40 @@ class ChoiceField extends FormField {
     }
 }
 
+class NumericField extends FormField {
+
+    function getSearchMethods() {
+        return array(
+            'equal' =>   __('Equal'),
+            'greater' =>  __('Greater Than'),
+            'less' =>  __('Less Than'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'equal' => array('TextboxField', array(
+                    'configuration' => array(
+                        'validator' => 'number',
+                        'size' => 6
+                        ),
+            )),
+            'greater' => array('TextboxField', array(
+                    'configuration' => array(
+                        'validator' => 'number',
+                        'size' => 6
+                        ),
+            )),
+            'less' => array('TextboxField', array(
+                    'configuration' => array(
+                        'validator' => 'number',
+                        'size' => 6
+                        ),
+            )),
+        );
+    }
+}
+
 class DatetimeField extends FormField {
     static $widget = 'DatetimePickerWidget';
 
@@ -2043,7 +2099,7 @@ class DatetimeField extends FormField {
 
     function to_php($value) {
 
-        if (strtotime($value) <= 0)
+        if (!is_numeric($value) && strtotime($value) <= 0)
             return 0;
 
         return $value;
@@ -2056,8 +2112,9 @@ class DatetimeField extends FormField {
             return '';
 
         $config = $this->getConfiguration();
+        $format = $config['format'] ?: false;
         if ($config['gmt'])
-            return $this->format((int) $datetime->format('U'));
+            return $this->format((int) $datetime->format('U'), $format);
 
         // Force timezone if field has one.
         if ($config['timezone']) {
@@ -2066,10 +2123,10 @@ class DatetimeField extends FormField {
         }
 
         $value = $this->format($datetime->format('U'),
-                $datetime->getTimezone()->getName());
-
+                $datetime->getTimezone()->getName(),
+                $format);
         // No need to show timezone
-        if (!$config['time'])
+        if (!$config['time'] || $format)
             return $value;
 
         // Display is NOT timezone aware show entry's timezone.
@@ -2083,16 +2140,16 @@ class DatetimeField extends FormField {
         return ($timestamp > 0) ? $timestamp : '';
     }
 
-    function format($timestamp, $timezone=false) {
+    function format($timestamp, $timezone=false, $format=false) {
 
         if (!$timestamp || $timestamp <= 0)
             return '';
 
         $config = $this->getConfiguration();
         if ($config['time'])
-            $formatted = Format::datetime($timestamp, false, $timezone);
+            $formatted = Format::datetime($timestamp, false, $format,  $timezone);
         else
-            $formatted = Format::date($timestamp, false, false, $timezone);
+            $formatted = Format::date($timestamp, false, $format, $timezone);
 
         return $formatted;
     }
@@ -3403,7 +3460,7 @@ class FileUploadField extends FormField {
                     $files[] = $f;
             }
 
-            foreach (@$this->getClean() as $key => $value)
+            foreach ($this->getClean(false) ?: array() as $key => $value)
                 $files[] = array('id' => $key, 'name' => $value);
 
             $this->files = $files;
@@ -4447,7 +4504,7 @@ class FileUploadWidget extends Widget {
         );
         $maxfilesize = ($config['size'] ?: 1048576) / 1048576;
         $files = array();
-        $new = $this->field->getClean();
+        $new = $this->field->getClean(false);
 
         foreach ($this->field->getAttachments() as $att) {
             unset($new[$att->file_id]);
@@ -4559,7 +4616,7 @@ class FileUploadWidget extends Widget {
                 continue;
 
             // Keep the values as the IDs
-            $ids[$id] = $name;
+            $ids[$id] = $name ?: $allowed[$id];
         }
 
         return $ids;
@@ -5047,6 +5104,49 @@ class ReleaseForm extends Form {
     }
 }
 
+class MarkAsForm extends Form {
+    static $id = 'markAs';
+
+    function getFields() {
+        if ($this->fields)
+            return $this->fields;
+
+        $fields = array(
+            'comments' => new TextareaField(array(
+                    'id' => 1, 'label'=> '', 'required'=>false, 'default'=>'',
+                    'configuration' => array(
+                        'html' => true,
+                        'size' => 'small',
+                        'placeholder' => __('Optional reason for marking ticket as (un)answered'),
+                        ),
+                    )
+                ),
+            );
+
+
+        $this->setFields($fields);
+
+        return $this->fields;
+    }
+
+    function getField($name) {
+        if (($fields = $this->getFields())
+                && isset($fields[$name]))
+            return $fields[$name];
+    }
+
+    function isValid($include=false) {
+        if (!parent::isValid($include))
+            return false;
+
+        return !$this->errors();
+    }
+
+    function getComments() {
+        return $this->getField('comments')->getClean();
+    }
+}
+
 class ReferralForm extends Form {
 
     static $id = 'refer';
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.list.php b/include/class.list.php
index e5c84fbd91b58cc2f8dc742bbec745bba1151882..f8055805c2e692eec4fb446db79c8585241260f8 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -1421,6 +1421,9 @@ implements CustomListItem, TemplateVariable, Searchable {
     }
 
     function display() {
+
+        return $this->getLocalName();
+
         return sprintf('<a class="preview" href="#"
                 data-preview="#list/%d/items/%d/preview">%s</a>',
                 $this->getListId(),
diff --git a/include/class.orm.php b/include/class.orm.php
index 106e11465db81e2093041086435f67bcbdb3a627..cd30ffa974c1ed1cb95137b0d357a25b1d1b7610 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -333,6 +333,11 @@ class VerySimpleModel {
         return static::getMeta()->newInstance($row);
     }
 
+    function __wakeup() {
+        // If a model is stashed in a session, refresh the model from the database
+        $this->refetch();
+    }
+
     function get($field, $default=false) {
         if (array_key_exists($field, $this->ht))
             return $this->ht[$field];
@@ -1142,6 +1147,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
     const OPT_NOSORT    = 'nosort';
     const OPT_NOCACHE   = 'nocache';
     const OPT_MYSQL_FOUND_ROWS = 'found_rows';
+    const OPT_INDEX_HINT = 'indexhint';
 
     const ITER_MODELS   = 1;
     const ITER_HASH     = 2;
@@ -1281,6 +1287,10 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         return $this;
     }
 
+    function addExtraJoin(array $join) {
+       return $this->extra(array('joins' => array($join)));
+    }
+
     function distinct() {
         foreach (func_get_args() as $D)
             $this->distinct[] = $D;
@@ -1477,6 +1487,18 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         return isset($this->options[$option]);
     }
 
+    function getOption($option) {
+        return @$this->options[$option] ?: false;
+    }
+
+    function setOption($option, $value) {
+        $this->options[$option] = $value;
+    }
+
+    function clearOption($option) {
+        unset($this->options[$option]);
+    }
+
     function countSelectFields() {
         $count = count($this->values) + count($this->annotations);
         if (isset($this->extra['select']))
@@ -2611,13 +2633,22 @@ class SqlCompiler {
             foreach ($queryset->extra['tables'] as $S) {
                 $join = ' JOIN ';
                 // Left joins require an ON () clause
-                if ($lastparen = strrpos($S, '(')) {
-                    if (preg_match('/\bon\b/i', substr($S, $lastparen - 4, 4)))
-                        $join = ' LEFT' . $join;
-                }
+                // TODO: Have a way to indicate a LEFT JOIN
                 $sql .= $join.$S;
             }
         }
+
+        // Add extra joins from QuerySet
+        if (isset($queryset->extra['joins'])) {
+            foreach ($queryset->extra['joins'] as $J) {
+                list($base, $constraints, $alias) = $J;
+                $join = $constraints ? ' LEFT JOIN ' : ' JOIN ';
+                $sql .= "{$join}{$base} $alias";
+                if ($constraints instanceof Q)
+                    $sql .= ' ON ('.$this->compileQ($constraints, $queryset->model).')';
+            }
+        }
+
         return $sql;
     }
 
@@ -2965,6 +2996,7 @@ class MySqlCompiler extends SqlCompiler {
         $meta = $model::getMeta();
         $table = $this->quote($meta['table']).' '.$rootAlias;
         // Handle related tables
+        $need_group_by = false;
         if ($queryset->related) {
             $count = 0;
             $fieldMap = $theseFields = array();
@@ -3010,13 +3042,16 @@ class MySqlCompiler extends SqlCompiler {
         }
         // Support retrieving only a list of values rather than a model
         elseif ($queryset->values) {
+            $additional_group_by = array();
             foreach ($queryset->values as $alias=>$v) {
                 list($f) = $this->getField($v, $model);
                 $unaliased = $f;
                 if ($f instanceof SqlFunction) {
                     $fields[$f->toSql($this, $model, $alias)] = true;
                     if ($f instanceof SqlAggregate) {
-                        // Don't group_by aggregate expressions
+                        // Don't group_by aggregate expressions, but if there is an
+                        // aggergate expression, then we need a GROUP BY clause.
+                        $need_group_by = true;
                         continue;
                     }
                 }
@@ -3028,8 +3063,10 @@ class MySqlCompiler extends SqlCompiler {
                 // If there are annotations, add in these fields to the
                 // GROUP BY clause
                 if ($queryset->annotations && !$queryset->distinct)
-                    $group_by[] = $unaliased;
+                    $additional_group_by[] = $unaliased;
             }
+            if ($need_group_by && $additional_group_by)
+                $group_by = array_merge($group_by, $additional_group_by);
         }
         // Simple selection from one table
         elseif (!$queryset->aggregated) {
@@ -3050,6 +3087,8 @@ class MySqlCompiler extends SqlCompiler {
             foreach ($queryset->annotations as $alias=>$A) {
                 // The root model will receive the annotations, add in the
                 // annotation after the root model's fields
+                if ($A instanceof SqlAggregate)
+                    $need_group_by = true;
                 $T = $A->toSql($this, $model, $alias);
                 if ($fieldMap) {
                     array_splice($fields, count($fieldMap[0][0]), 0, array($T));
@@ -3061,7 +3100,7 @@ class MySqlCompiler extends SqlCompiler {
                 }
             }
             // If no group by has been set yet, use the root model pk
-            if (!$group_by && !$queryset->aggregated && !$queryset->distinct) {
+            if (!$group_by && !$queryset->aggregated && !$queryset->distinct && $need_group_by) {
                 foreach ($meta['pk'] as $pk)
                     $group_by[] = $rootAlias .'.'. $pk;
             }
@@ -3083,12 +3122,15 @@ class MySqlCompiler extends SqlCompiler {
         $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : '';
 
         $joins = $this->getJoins($queryset);
+        if ($hint = $queryset->getOption(QuerySet::OPT_INDEX_HINT)) {
+            $hint = " USE INDEX ({$hint})";
+        }
 
         $sql = 'SELECT ';
         if ($queryset->hasOption(QuerySet::OPT_MYSQL_FOUND_ROWS))
             $sql .= 'SQL_CALC_FOUND_ROWS ';
         $sql .= implode(', ', $fields).' FROM '
-            .$table.$joins.$where.$group_by.$having.$sort;
+            .$table.$hint.$joins.$where.$group_by.$having.$sort;
         // UNIONS
         if ($queryset->chain) {
             // If the main query is sorted, it will need parentheses
diff --git a/include/class.page.php b/include/class.page.php
index a430fc2e66cd20da6b5a1711b1739094bbc8fdbc..1eb7387108f6ea98febbf1130cdc051cb8bf67f3 100644
--- a/include/class.page.php
+++ b/include/class.page.php
@@ -282,7 +282,7 @@ class Page extends VerySimpleModel {
 
         // Attach inline attachments from the editor
         $keepers = Draft::getAttachmentIds($vars['body']);
-        $keepers = array_map(function($i) { return $i['id']; }, $keepers);
+        $keepers = array_flip(array_map(function($i) { return $i['id']; }, $keepers));
         $this->attachments->keepOnlyFileIds($keepers, true);
 
         if ($rv)
diff --git a/include/class.pagenate.php b/include/class.pagenate.php
index b20ad52a5da542faa088d21d31045a338761c313..70d1ca1262d3111ed962bc54a5233d335fdd4f2a 100644
--- a/include/class.pagenate.php
+++ b/include/class.pagenate.php
@@ -22,6 +22,7 @@ class PageNate {
     var $total;
     var $page;
     var $pages;
+    var $approx=false;
 
 
     function __construct($total,$page,$limit=20,$url='') {
@@ -32,7 +33,7 @@ class PageNate {
         $this->setTotal($total);
     }
 
-    function setTotal($total) {
+    function setTotal($total, $approx=false) {
         $this->total = intval($total);
         $this->pages = ceil( $this->total / $this->limit );
 
@@ -42,6 +43,7 @@ class PageNate {
         if (($this->limit-1)*$this->start > $this->total) {
             $this->start -= $this->start % $this->limit;
         }
+        $this->approx = $approx;
     }
 
     function setURL($url='',$vars='') {
@@ -97,8 +99,12 @@ class PageNate {
         }
         $html=__('Showing')."&nbsp;";
         if ($this->total > 0) {
-            $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */),
-               $start, $end, $this->total);
+            if ($this->approx)
+                $html .= sprintf(__('%1$d - %2$d of about %3$d' /* Used in pagination output */),
+                   $start, $end, $this->total);
+            else
+                $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */),
+                   $start, $end, $this->total);
         }else{
             $html .= " 0 ";
         }
diff --git a/include/class.queue.php b/include/class.queue.php
index e5ae4aa556ff134a9fa8653fb0ebaeb2f1d80730..a7b84f47fe8a25589acf1b7b2bcb688e1abd40e6 100644
--- a/include/class.queue.php
+++ b/include/class.queue.php
@@ -174,10 +174,7 @@ class CustomQueue extends VerySimpleModel {
      */
     function getForm($source=null, $searchable=null) {
         $fields = array();
-        $validator = false;
         if (!isset($searchable)) {
-            $searchable = $this->getCurrentSearchFields($source);
-            $validator = true;
             $fields = array(
                 ':keywords' => new TextboxField(array(
                     'id' => 3001,
@@ -188,11 +185,17 @@ class CustomQueue extends VerySimpleModel {
                         'classes' => 'full-width headline',
                         'placeholder' => __('Keywords — Optional'),
                     ),
+                    'validators' => function($self, $v) {
+                        if (mb_str_wc($v) > 3)
+                            $self->addError(__('Search term cannot have more than 3 keywords'));
+                    },
                 )),
             );
+
+            $searchable = $this->getCurrentSearchFields($source);
         }
 
-        foreach ($searchable as $path=>$field)
+        foreach ($searchable ?: array() as $path => $field)
             $fields = array_merge($fields, static::getSearchField($field, $path));
 
         $form = new AdvancedSearchForm($fields, $source);
@@ -574,7 +577,7 @@ class CustomQueue extends VerySimpleModel {
                 continue;
 
             $name = $f->get('name') ?: 'field_'.$f->get('id');
-            $key = 'cdata.'.$name;
+            $key = 'cdata__'.$name;
             $cdata[$key] = $f->getLocal('label');
         }
 
@@ -582,22 +585,22 @@ class CustomQueue extends VerySimpleModel {
         $fields = array(
                 'number' =>         __('Ticket Number'),
                 'created' =>        __('Date Created'),
-                'cdata.subject' =>  __('Subject'),
-                'user.name' =>      __('From'),
-                'user.default_email.address' => __('From Email'),
-                'cdata.:priority.priority_desc' => __('Priority'),
-                'dept::getLocalName' => __('Department'),
-                'topic::getName' => __('Help Topic'),
+                'cdata__subject' =>  __('Subject'),
+                'user__name' =>      __('From'),
+                'user__emails__address' => __('From Email'),
+                'cdata__priority' => __('Priority'),
+                'dept_id' => __('Department'),
+                'topic_id' => __('Help Topic'),
                 'source' =>         __('Source'),
-                'status::getName' =>__('Current Status'),
+                'status__id' =>__('Current Status'),
                 'lastupdate' =>     __('Last Updated'),
                 'est_duedate' =>    __('SLA Due Date'),
                 'duedate' =>        __('Due Date'),
                 'closed' =>         __('Closed Date'),
                 'isoverdue' =>      __('Overdue'),
                 'isanswered' =>     __('Answered'),
-                'staff::getName' => __('Agent Assigned'),
-                'team::getName' =>  __('Team Assigned'),
+                'staff_id' => __('Agent Assigned'),
+                'team_id' =>  __('Team Assigned'),
                 'thread_count' =>   __('Thread Count'),
                 'reopen_count' =>   __('Reopen Count'),
                 'attachment_count' => __('Attachment Count'),
@@ -629,6 +632,22 @@ class CustomQueue extends VerySimpleModel {
         return $fields;
     }
 
+    function getExportColumns($fields=array()) {
+        $columns = array();
+        $fields = $fields ?: $this->getExportFields();
+        $i = 0;
+        foreach ($fields as $path => $label) {
+            $c = QueueColumn::placeholder(array(
+                        'id' => $i++,
+                        'heading' => $label,
+                        'primary' => $path,
+                        ));
+            $c->setQueue($this);
+            $columns[$path] = $c;
+        }
+        return $columns;
+    }
+
     function getStandardColumns() {
         return $this->getColumns();
     }
@@ -775,14 +794,13 @@ class CustomQueue extends VerySimpleModel {
     }
 
     function export($options=array()) {
+        global $thisstaff;
 
-        if (!($query=$this->getBasicQuery()))
-            return false;
-
-        if (!($fields=$this->getExportFields()))
+        if (!$thisstaff
+                || !($query=$this->getBasicQuery())
+                || !($fields=$this->getExportFields()))
             return false;
 
-
         $filename = sprintf('%s Tickets-%s.csv',
                 $this->getName(),
                 strftime('%Y%m%d'));
@@ -799,14 +817,45 @@ class CustomQueue extends VerySimpleModel {
                     $filename ="$filename.csv";
             }
 
-            if (isset($opts['delimiter']))
+            if (isset($opts['delimiter']) && !$options['delimiter'])
                 $options['delimiter'] = $opts['delimiter'];
 
         }
 
+        // Apply columns
+        $columns = $this->getExportColumns($fields);
+        $headers = array(); // Reset fields based on validity of columns
+        foreach ($columns as $column) {
+            $query = $column->mangleQuery($query, $this->getRoot());
+            $headers[] = $column->getHeading();
+        }
+
+        // Apply visibility
+        if (!$this->ignoreVisibilityConstraints($thisstaff))
+            $query->filter($thisstaff->getTicketsVisibility());
+
+        // Render Util
+        $render = function ($row) use($columns) {
+            if (!$row) return false;
+
+            $record = array();
+            foreach ($columns as $path => $column) {
+                $record[] = (string) $column->from_query($row) ?:
+                    $row[$path] ?: '';
+            }
+            return $record;
+        };
 
-        return Export::saveTickets($query, $fields, $filename, 'csv',
-                $options);
+        $delimiter = $options['delimiter'] ?:
+            Internationalization::getCSVDelimiter();
+        $output = fopen('php://output', 'w');
+        Http::download($filename, "text/csv");
+        fputs($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
+        fputcsv($output, $headers, $delimiter);
+        foreach ($query as $row)
+            fputcsv($output, $render($row), $delimiter);
+        fclose($output);
+        exit();
     }
 
     /**
@@ -1006,8 +1055,7 @@ class CustomQueue extends VerySimpleModel {
     }
 
     function isPrivate() {
-        return !$this->isAQueue() && !$this->isPublic() &&
-            $this->staff_id;
+        return !$this->isAQueue() && $this->staff_id;
     }
 
     function isPublic() {
@@ -1036,6 +1084,57 @@ class CustomQueue extends VerySimpleModel {
         $this->clearFlag(self::FLAG_DISABLED);
     }
 
+    function getRoughCount() {
+        if (($count = $this->getRoughCountAPC()) !== false)
+            return $count;
+
+        $query = Ticket::objects();
+        $Q = $this->getBasicQuery();
+        $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)),
+            new SqlField('ticket_id'));
+        $query = $query->aggregate(array(
+            "ticket_count" => SqlAggregate::COUNT($expr)
+        ));
+
+        $row = $query->values()->one();
+        return $row['ticket_count'];
+    }
+
+    function getRoughCountAPC() {
+        if (!function_exists('apcu_store'))
+            return false;
+
+        $key = "rough.counts.".SECRET_SALT;
+        $cached = false;
+        $counts = apcu_fetch($key, $cached);
+        if ($cached === true && isset($counts["q{$this->id}"]))
+            return $counts["q{$this->id}"];
+
+        // Fetch rough counts of all queues. That is, fetch a total of the
+        // counts based on the queue criteria alone. Do no consider agent
+        // access. This should be fast and "rought"
+        $queues = static::objects()
+            ->filter(['flags__hasbit' => CustomQueue::FLAG_PUBLIC])
+            ->exclude(['flags__hasbit' => CustomQueue::FLAG_DISABLED]);
+
+        $query = Ticket::objects();
+        $prefix = "";
+
+        foreach ($queues as $queue) {
+            $Q = $queue->getBasicQuery();
+            $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)),
+                new SqlField('ticket_id'));
+            $query = $query->aggregate(array(
+                "q{$queue->id}" => SqlAggregate::COUNT($expr)
+            ));
+        }
+
+        $counts = $query->values()->one();
+
+        apcu_store($key, $counts, 900);
+        return @$counts["q{$this->id}"];
+    }
+
     function updateExports($fields, $save=true) {
 
         if (!$fields)
@@ -1252,6 +1351,8 @@ class CustomQueue extends VerySimpleModel {
 
         if ($this->dirty)
             $this->updated = SqlFunction::NOW();
+
+        $clearCounts = ($this->dirty || $this->__new__);
         if (!($rv = parent::save($refetch || $this->dirty)))
             return $rv;
 
@@ -1270,6 +1371,11 @@ class CustomQueue extends VerySimpleModel {
             };
             $move_children($this);
         }
+
+        // Refetch the queue counts
+        if ($clearCounts)
+            SavedQueue::clearCounts();
+
         return $this->columns->saveAll()
             && $this->exports->saveAll()
             && $this->sorts->saveAll();
@@ -1290,23 +1396,35 @@ class CustomQueue extends VerySimpleModel {
      *      visible queues.
      * $pid - <int> parent_id of root queue. Default is zero (top-level)
      */
-    static function getHierarchicalQueues(Staff $staff, $pid=0) {
-        $all = static::objects()
+    static function getHierarchicalQueues(Staff $staff, $pid=0,
+            $primary=true) {
+        $query = static::objects()
+            ->annotate(array('_sort' =>  SqlCase::N()
+                        ->when(array('sort' => 0), 999)
+                        ->otherwise(new SqlField('sort'))))
             ->filter(Q::any(array(
                 'flags__hasbit' => self::FLAG_PUBLIC,
                 'flags__hasbit' => static::FLAG_QUEUE,
                 'staff_id' => $staff->getId(),
             )))
             ->exclude(['flags__hasbit' => self::FLAG_DISABLED])
-            ->asArray();
-
+            ->order_by('parent_id', '_sort', 'title');
+        $all = $query->asArray();
         // Find all the queues with a given parent
-        $for_parent = function($pid) use ($all, &$for_parent) {
+        $for_parent = function($pid) use ($primary, $all, &$for_parent) {
             $results = [];
             foreach (new \ArrayIterator($all) as $q) {
-                if ($q->parent_id == $pid)
-                    $results[] = [ $q, $for_parent($q->getId()) ];
+                if ($q->parent_id != $pid)
+                    continue;
+
+                if ($pid == 0 && (
+                            ($primary &&  !$q->isAQueue())
+                            || (!$primary && $q->isAQueue())))
+                    continue;
+
+                $results[] = [ $q, $for_parent($q->getId()) ];
             }
+
             return $results;
         };
 
@@ -1429,7 +1547,7 @@ abstract class QueueColumnAnnotation {
     }
 
     // Add the annotation to a QuerySet
-    abstract function annotate($query);
+    abstract function annotate($query, $name);
 
     // Fetch some HTML to render the decoration on the page. This function
     // can return boolean FALSE to indicate no decoration should be applied
@@ -1464,6 +1582,17 @@ abstract class QueueColumnAnnotation {
     function isVisible($row) {
         return true;
     }
+
+    static function addToQuery($query, $name=false) {
+        $name = $name ?: static::$qname;
+        $annotation = new Static(array());
+        return $annotation->annotate($query, $name);
+    }
+
+    static function from_query($row, $name=false) {
+        $name = $name ?: static::$qname;
+        return $row[$name];
+    }
 }
 
 class TicketThreadCount
@@ -1472,9 +1601,10 @@ extends QueueColumnAnnotation {
     static $qname = '_thread_count';
     static $desc = /* @trans */ 'Thread Count';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
+        $name = $name ?: static::$qname;
         return $query->annotate(array(
-        static::$qname => TicketThread::objects()
+            $name => TicketThread::objects()
             ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
             ->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))
             ->aggregate(array('count' => SqlAggregate::COUNT('entries__id')))
@@ -1502,11 +1632,12 @@ extends QueueColumnAnnotation {
     static $qname = '_reopen_count';
     static $desc = /* @trans */ 'Reopen Count';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
+        $name = $name ?: static::$qname;
         return $query->annotate(array(
-        static::$qname => TicketThread::objects()
+            $name => TicketThread::objects()
             ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
-            ->filter(array('events__annulled' => 0, 'events__state' => 'reopened'))
+            ->filter(array('events__annulled' => 0, 'events__event_id' => Event::getIdByName('reopened')))
             ->aggregate(array('count' => SqlAggregate::COUNT('events__id')))
         ));
     }
@@ -1533,10 +1664,11 @@ extends QueueColumnAnnotation {
     static $qname = '_att_count';
     static $desc = /* @trans */ 'Attachment Count';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
         // TODO: Convert to Thread attachments
+        $name = $name ?: static::$qname;
         return $query->annotate(array(
-        static::$qname => TicketThread::objects()
+            $name => TicketThread::objects()
             ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
             ->filter(array('entries__attachments__inline' => 0))
             ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id')))
@@ -1563,9 +1695,10 @@ extends QueueColumnAnnotation {
     static $qname = '_collabs';
     static $desc = /* @trans */ 'Collaborator Count';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
+        $name = $name ?: static::$qname;
         return $query->annotate(array(
-        static::$qname => TicketThread::objects()
+            $name => TicketThread::objects()
             ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
             ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id')))
         ));
@@ -1590,7 +1723,7 @@ extends QueueColumnAnnotation {
     static $icon = 'exclamation';
     static $desc = /* @trans */ 'Overdue Icon';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
         return $query->values('isoverdue');
     }
 
@@ -1609,7 +1742,7 @@ extends QueueColumnAnnotation {
     static $icon = 'phone';
     static $desc = /* @trans */ 'Ticket Source';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
         return $query->values('source');
     }
 
@@ -1624,7 +1757,7 @@ extends QueueColumnAnnotation {
     static $icon = "lock";
     static $desc = /* @trans */ 'Locked';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
         global $thisstaff;
 
         return $query
@@ -1651,7 +1784,7 @@ extends QueueColumnAnnotation {
     static $icon = "user";
     static $desc = /* @trans */ 'Assignee Avatar';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
         return $query->values('staff_id', 'team_id');
     }
 
@@ -1690,7 +1823,7 @@ extends QueueColumnAnnotation {
     static $icon = "user";
     static $desc = /* @trans */ 'User Avatar';
 
-    function annotate($query) {
+    function annotate($query, $name=false) {
         return $query->values('user_id');
     }
 
@@ -1986,6 +2119,7 @@ extends VerySimpleModel {
     var $_annotations;
     var $_conditions;
     var $_queue;            // Apparent queue if being inherited
+    var $_fields;
 
     function getId() {
         return $this->id;
@@ -2024,6 +2158,25 @@ extends VerySimpleModel {
         $this->_queue = $queue;
     }
 
+    function getFields() {
+        if (!isset($this->_fields)) {
+            $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket';
+            $fields = CustomQueue::getSearchableFields($root);
+            $primary = CustomQueue::getOrmPath($this->primary);
+            $secondary = CustomQueue::getOrmPath($this->secondary);
+            if (($F = $fields[$primary]) && (list(,$field) = $F))
+                $this->_fields[$primary] = $field;
+            if (($F = $fields[$secondary]) && (list(,$field) = $F))
+                $this->_fields[$secondary] = $field;
+        }
+        return $this->_fields;
+    }
+
+    function getField($path=null) {
+        $fields = $this->getFields();
+        return @$fields[$path ?: $this->primary];
+    }
+
     function getWidth() {
         return $this->width ?: 100;
     }
@@ -2091,29 +2244,36 @@ extends VerySimpleModel {
     }
 
     function renderBasicValue($row) {
-        $root = ($q = $this->getQueue()) ? $q->getRoot() : 'Ticket';
-        $fields = CustomQueue::getSearchableFields($root);
+        $fields = $this->getFields();
         $primary = CustomQueue::getOrmPath($this->primary);
         $secondary = CustomQueue::getOrmPath($this->secondary);
 
         // Return a lazily ::display()ed value so that the value to be
         // rendered by the field could be changed or display()ed when
         // converted to a string.
-
         if (($F = $fields[$primary])
-            && (list(,$field) = $F)
-            && ($T = $field->from_query($row, $primary))
+            && ($T = $F->from_query($row, $primary))
         ) {
-            return new LazyDisplayWrapper($field, $T);
+            return new LazyDisplayWrapper($F, $T);
         }
         if (($F = $fields[$secondary])
-            && (list(,$field) = $F)
-            && ($T = $field->from_query($row, $secondary))
+            && ($T = $F->from_query($row, $secondary))
         ) {
-            return new LazyDisplayWrapper($field, $T);
+            return new LazyDisplayWrapper($F, $T);
         }
 
-         return new LazyDisplayWrapper($field, '');
+         return new LazyDisplayWrapper($F, '');
+    }
+
+    function from_query($row) {
+        if (!($f = $this->getField($this->primary)))
+            return '';
+
+        $val = $f->to_php($f->from_query($row, $this->primary));
+        if (!is_string($val))
+            $val = $f->display($val);
+
+        return $val;
     }
 
     function applyTruncate($text, $row) {
@@ -2157,14 +2317,12 @@ extends VerySimpleModel {
 
     function mangleQuery($query, $root=null) {
         // Basic data
-        $fields = CustomQueue::getSearchableFields($root ?: $this->getQueue()->getRoot());
-        if ($primary = $fields[$this->primary]) {
-            list(,$field) = $primary;
+        $fields = $this->getFields();
+        if ($field = $fields[$this->primary]) {
             $query = $this->addToQuery($query, $field,
                 CustomQueue::getOrmPath($this->primary, $query));
         }
-        if ($secondary = $fields[$this->secondary]) {
-            list(,$field) = $secondary;
+        if ($field = $fields[$this->secondary]) {
             $query = $this->addToQuery($query, $field,
                 CustomQueue::getOrmPath($this->secondary, $query));
         }
@@ -2219,7 +2377,7 @@ extends VerySimpleModel {
 
             $reverse = $reverse ? '-' : '';
             $query = $query->order_by("{$reverse}{$alias}");
-        } else {
+        } elseif($keys[0]) {
             list($path, $field) = $keys[0];
             $query = $field->applyOrderBy($query, $reverse, $path);
         }
@@ -2559,6 +2717,7 @@ extends VerySimpleModel {
     );
 
     var $_columns;
+    var $_extra;
 
     function getRoot($hint=false) {
         switch ($hint ?: $this->root) {
@@ -2576,6 +2735,12 @@ extends VerySimpleModel {
         return $this->id;
     }
 
+    function getExtra() {
+        if (isset($this->extra) && !isset($this->_extra))
+            $this->_extra = JsonDataParser::decode($this->extra);
+        return $this->_extra;
+    }
+
     function applySort(QuerySet $query, $reverse=false, $root=false) {
         $fields = CustomQueue::getSearchableFields($this->getRoot($root));
         foreach ($this->getColumnPaths() as $path=>$descending) {
@@ -2586,6 +2751,10 @@ extends VerySimpleModel {
                     CustomQueue::getOrmPath($path, $query));
             }
         }
+        // Add index hint if defined
+        if (($extra = $this->getExtra()) && isset($extra['index'])) {
+            $query->setOption(QuerySet::OPT_INDEX_HINT, $extra['index']);
+        }
         return $query;
     }
 
@@ -2619,6 +2788,11 @@ extends VerySimpleModel {
             array('id' => $this->id));
     }
 
+    function getAdvancedConfigForm($source=false) {
+        return new QueueSortAdvancedConfigForm($source ?: $this->getExtra(),
+            array('id' => $this->id));
+    }
+
     static function forQueue(CustomQueue $queue) {
         return static::objects()->filter([
             'root' => $queue->root ?: 'T',
@@ -2654,6 +2828,11 @@ extends VerySimpleModel {
             $this->columns = JsonDataEncoder::encode($columns);
         }
 
+        if ($this->getExtra() !== null) {
+            $extra = $this->getAdvancedConfigForm($vars)->getClean();
+            $this->extra = JsonDataEncoder::encode($extra);
+        }
+
         if (count($errors))
             return false;
 
@@ -2913,3 +3092,24 @@ extends AbstractForm {
         );
     }
 }
+
+class QueueSortAdvancedConfigForm
+extends AbstractForm {
+    function getInstructions() {
+        return __('If unsure, leave these options blank and unset');
+    }
+
+    function buildFields() {
+        return array(
+            'index' => new TextboxField(array(
+                'label' => __('Database Index'),
+                'hint' => __('Use this index when sorting on this column'),
+                'required' => false,
+                'layout' => new GridFluidCell(12),
+                'configuration' => array(
+                    'placeholder' => __('Automatic'),
+                ),
+            )),
+        );
+    }
+}
diff --git a/include/class.report.php b/include/class.report.php
index 12d7322d3dbc6665fed18b86d8b738d49eb091ba..418eb407ee18982ca939b7f6f1884fafb5ba6847 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)
                     ),
                 ));
 
@@ -205,10 +215,12 @@ class OverviewReport {
             $pk = 'dept__id';
             $stats = $stats
                 ->filter(array('dept_id__in' => $thisstaff->getDepts()))
-                ->values('dept__id', 'dept__name', 'dept__flags');
+                ->values('dept__id', 'dept__name', 'dept__flags')
+                ->distinct('dept__id');
             $times = $times
                 ->filter(array('dept_id__in' => $thisstaff->getDepts()))
-                ->values('dept__id');
+                ->values('dept__id')
+                ->distinct('dept__id');
             break;
         case 'topic':
             $headers = array(__('Help Topic'));
@@ -217,10 +229,12 @@ class OverviewReport {
             $topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
             $stats = $stats
                 ->values('topic_id', 'topic__topic', 'topic__flags')
-                ->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0, 'topic_id__in' => array_keys($topics)));
+                ->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0, 'topic_id__in' => array_keys($topics)))
+                ->distinct('topic_id');
             $times = $times
                 ->values('topic_id')
-                ->filter(array('topic_id__gt' => 0));
+                ->filter(array('topic_id__gt' => 0))
+                ->distinct('topic_id');
             break;
         case 'staff':
             $headers = array(__('Agent'));
@@ -230,8 +244,9 @@ class OverviewReport {
             $staff = Staff::getStaffMembers();
             $stats = $stats
                 ->values('staff_id', 'staff__firstname', 'staff__lastname')
-                ->filter(array('staff_id__in' => array_keys($staff)));
-            $times = $times->values('staff_id');
+                ->filter(array('staff_id__in' => array_keys($staff)))
+                ->distinct('staff_id');
+            $times = $times->values('staff_id')->distinct('staff_id');
             $depts = $thisstaff->getManagedDepartments();
             if ($thisstaff->hasPerm(ReportModel::PERM_AGENTS))
                 $depts = array_merge($depts, $thisstaff->getDepts());
diff --git a/include/class.role.php b/include/class.role.php
index 7898bb9b67d424c3bbbb468c5a06bf44a7c24a2d..710c78da7ce79cd77ee6042872136a49e2eb4eb4 100644
--- a/include/class.role.php
+++ b/include/class.role.php
@@ -387,7 +387,7 @@ extends AbstractForm {
         );
     }
 
-    function getClean() {
+    function getClean($validate = true) {
         $clean = parent::getClean();
         // Index permissions as ['ticket.edit' => 1]
         $clean['perms'] = array_keys($clean['perms']);
diff --git a/include/class.search.php b/include/class.search.php
index 35e034c01920f16358192a8565009e4c00028333..f2a77955b536dd165f28a333ffbd7e261a314e0b 100755
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -384,9 +384,11 @@ class MysqlSearchBackend extends SearchBackend {
             $criteria->extra(array(
                 'tables' => array(
                     str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
-                    "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, SUM({}) AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {} GROUP BY `ticket_id`) Z1"),
-                )
+                    "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, Z1.relevance FROM (SELECT Z1.`object_id`, Z1.`object_type`, {} AS `relevance` FROM `:_search` Z1 WHERE {} ORDER BY relevance DESC) Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN `:ticket` Z8 ON (Z8.`user_id` = Z6.`id`)) Z1"),
+                ),
             ));
+            $criteria->extra(array('order_by' => array(array(new SqlCode('Z1.relevance', 'DESC')))));
+
             $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')));
             break;
 
@@ -481,7 +483,7 @@ class MysqlSearchBackend extends SearchBackend {
             LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H')
             WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM')
             AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0)
-            ORDER BY A1.`id` DESC LIMIT 500";
+            LIMIT 500";
         if (!($res = db_query_unbuffered($sql, $auto_create)))
             return false;
 
@@ -501,7 +503,7 @@ class MysqlSearchBackend extends SearchBackend {
         $sql = "SELECT A1.`ticket_id` FROM `".TICKET_TABLE."` A1
             LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`ticket_id` = A2.`object_id` AND A2.`object_type`='T')
             WHERE A2.`object_id` IS NULL
-            ORDER BY A1.`ticket_id` DESC LIMIT 300";
+            LIMIT 300";
         if (!($res = db_query_unbuffered($sql, $auto_create)))
             return false;
 
@@ -524,8 +526,7 @@ class MysqlSearchBackend extends SearchBackend {
 
         $sql = "SELECT A1.`id` FROM `".USER_TABLE."` A1
             LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='U')
-            WHERE A2.`object_id` IS NULL
-            ORDER BY A1.`id` DESC";
+            WHERE A2.`object_id` IS NULL";
         if (!($res = db_query_unbuffered($sql, $auto_create)))
             return false;
 
@@ -550,8 +551,7 @@ class MysqlSearchBackend extends SearchBackend {
 
         $sql = "SELECT A1.`id` FROM `".ORGANIZATION_TABLE."` A1
             LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='O')
-            WHERE A2.`object_id` IS NULL
-            ORDER BY A1.`id` DESC";
+            WHERE A2.`object_id` IS NULL";
         if (!($res = db_query_unbuffered($sql, $auto_create)))
             return false;
 
@@ -575,8 +575,7 @@ class MysqlSearchBackend extends SearchBackend {
         require_once INCLUDE_DIR . 'class.faq.php';
         $sql = "SELECT A1.`faq_id` FROM `".FAQ_TABLE."` A1
             LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`faq_id` = A2.`object_id` AND A2.`object_type`='K')
-            WHERE A2.`object_id` IS NULL
-            ORDER BY A1.`faq_id` DESC";
+            WHERE A2.`object_id` IS NULL";
         if (!($res = db_query_unbuffered($sql, $auto_create)))
             return false;
 
@@ -701,18 +700,20 @@ class SavedQueue extends CustomQueue {
         return $this->_columns;
     }
 
+    static function getHierarchicalQueues(Staff $staff, $pid=0,
+            $primary=true) {
+        return CustomQueue::getHierarchicalQueues($staff, 0, false);
+    }
+
     /**
      * Fetch an AdvancedSearchForm instance for use in displaying or
      * configuring this search in the user interface.
      *
      */
     function getForm($source=null, $searchable=array()) {
-        global $thisstaff;
-
-        if (!$this->isAQueue())
-            $searchable =  $this->getCurrentSearchFields($source,
-                     parent::getCriteria());
-        else // Only allow supplemental matches.
+        $searchable = null;
+        if ($this->isAQueue())
+            // Only allow supplemental matches.
             $searchable = array_intersect_key($this->getCurrentSearchFields($source),
                     $this->getSupplementalMatches());
 
@@ -847,19 +848,53 @@ class SavedQueue extends CustomQueue {
         return (!$errors);
     }
 
-    static function ticketsCount($agent, $criteria=array(),
-            $prefix='') {
+    function getTotal($agent=null) {
+        $query = $this->getQuery();
+        if ($agent)
+            $query = $agent->applyVisibility($query);
+        $query->limit(false)->offset(false)->order_by(false);
+        try {
+            return $query->count();
+        } catch (Exception $e) {
+            return null;
+        }
+    }
+
+    function getCount($agent, $cached=true) {
+        $count = null;
+        if ($cached && ($counts = self::counts($agent, $cached)))
+            $count = $counts["q{$this->getId()}"];
+
+        if ($count == null)
+            $count = $this->getTotal($agent);
+
+        return $count;
+    }
+
+    // Get ticket counts for queues the agent has acces to.
+    static function counts($agent, $cached=true, $criteria=array()) {
 
         if (!$agent instanceof Staff)
-            return array();
+            return null;
 
-        $queues = SavedQueue::objects()
+        // Cache TLS in seconds
+        $ttl = 5*60;
+        // Cache key based on agent and salt of the installation
+        $key = "counts.queues.{$agent->getId()}.".SECRET_SALT;
+        if ($criteria && is_array($criteria)) // Consider additional criteria.
+            $key .= '.'.md5(serialize($criteria));
+
+        // only consider cache if requesed
+        if ($cached && ($counts=self::getCounts($key, $ttl)))
+            return $counts;
+
+        $queues = static::objects()
             ->filter(Q::any(array(
                 'flags__hasbit' => CustomQueue::FLAG_QUEUE,
                 'staff_id' => $agent->getId(),
             )));
 
-        if ($criteria)
+        if ($criteria && is_array($criteria))
             $queues->filter($criteria);
 
         $query = Ticket::objects();
@@ -870,11 +905,71 @@ class SavedQueue extends CustomQueue {
             $Q = $queue->getBasicQuery();
             $expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id'));
             $query->aggregate(array(
-                "$prefix{$queue->id}" => SqlAggregate::COUNT($expr, true)
+                "q{$queue->id}" => SqlAggregate::COUNT($expr, true)
             ));
+
+            // Add extra tables joins  (if any)
+            if ($Q->extra && isset($Q->extra['tables'])) {
+                $contraints = array();
+                if ($Q->constraints)
+                     $constraints = new Q($Q->constraints);
+                foreach ($Q->extra['tables'] as $T)
+                    $query->addExtraJoin(array($T, $constraints, ''));
+            }
+        }
+
+        try {
+            $counts = $query->values()->one();
+        }  catch (Exception $ex) {
+            foreach ($queues as $q)
+                $counts['q'.$q->getId()] = $q->getTotal();
         }
 
-        return $query->values()->one();
+        // Always cache the results
+        self::storeCounts($key, $counts, $ttl);
+
+        return $counts;
+    }
+
+    static function getCounts($key, $ttl) {
+
+        if (!$key) {
+            return array();
+        } elseif (function_exists('apcu_store')) {
+            $found = false;
+            $counts = apcu_fetch($key, $found);
+            if ($found === true)
+                return $counts;
+        } elseif (isset($_SESSION['qcounts'][$key])
+                && (time() - $_SESSION['qcounts'][$key]['time']) < $ttl) {
+            return $_SESSION['qcounts'][$key]['counts'];
+        } else {
+            // Auto clear missed session cache (if any)
+            unset($_SESSION['qcounts'][$key]);
+        }
+    }
+
+    static function storeCounts($key, $counts, $ttl) {
+        if (function_exists('apcu_store')) {
+            apcu_store($key, $counts, $ttl);
+        } else {
+            // Poor man's cache
+            $_SESSION['qcounts'][$key]['counts'] = $counts;
+            $_SESSION['qcounts'][$key]['time'] = time();
+        }
+    }
+
+    static function clearCounts() {
+        if (function_exists('apcu_store')) {
+            if (class_exists('APCUIterator')) {
+                $regex = '/^counts.queues.\d+.' . preg_quote(SECRET_SALT, '/') . '$/';
+                foreach (new APCUIterator($regex, APC_ITER_KEY) as $key) {
+                    apcu_delete($key);
+                }
+            }
+            // Also clear rough counts
+            apcu_delete("rough.counts.".SECRET_SALT);
+        }
     }
 
     static function lookup($criteria) {
@@ -903,6 +998,10 @@ class SavedSearch extends SavedQueue {
     function isSaved() {
         return (!$this->__new__);
     }
+
+    function getCount($agent, $cached=true) {
+        return 500;
+    }
 }
 
 class AdhocSearch
@@ -1004,21 +1103,30 @@ class AdvancedSearchSelectionField extends ChoiceField {
 }
 
 class HelpTopicChoiceField extends AdvancedSearchSelectionField {
+    static $_topics;
+
     function hasIdValue() {
         return true;
     }
 
     function getChoices($verbose=false) {
-        return Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
+        if (!isset($this->_topics))
+            $this->_topics = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
+
+        return $this->_topics;
     }
 }
 
 require_once INCLUDE_DIR . 'class.dept.php';
 class DepartmentChoiceField extends AdvancedSearchSelectionField {
-    var $_choices = null;
+    static $_depts;
+    var $_choices;
 
     function getChoices($verbose=false) {
-        return Dept::getDepartments();
+        if (!isset($this->_depts))
+            $this->_depts = Dept::getDepartments();
+
+        return $this->_depts;
     }
 
     function getQuickFilterChoices() {
@@ -1241,13 +1349,23 @@ trait ZeroMeansUnset {
 class AgentSelectionField extends AdvancedSearchSelectionField {
     use ZeroMeansUnset;
 
+    static $_agents;
+
     function getChoices($verbose=false) {
-        return array('M' => __('Me')) + Staff::getStaffMembers();
+        if (!isset($this->_agents)) {
+            $this->_agents = array('M' => __('Me')) +
+                Staff::getStaffMembers();
+        }
+        return $this->_agents;
     }
 
     function toString($value) {
+
         $choices =  $this->getChoices();
         $selection = array();
+        if (!is_array($value))
+            $value = array($value => $value);
+
         foreach ($value as $k => $v)
             if (isset($choices[$k]))
                 $selection[] = $choices[$k];
@@ -1278,9 +1396,13 @@ class AgentSelectionField extends AdvancedSearchSelectionField {
 }
 
 class DepartmentManagerSelectionField extends AgentSelectionField {
+    static $_members;
 
     function getChoices($verbose=false) {
-        return Staff::getStaffMembers();
+        if (isset($this->_members))
+            $this->_members = Staff::getStaffMembers();
+
+        return $this->_members;
     }
 
     function getSearchQ($method, $value, $name=false) {
@@ -1289,9 +1411,14 @@ class DepartmentManagerSelectionField extends AgentSelectionField {
 }
 
 class TeamSelectionField extends AdvancedSearchSelectionField {
+    static $_teams;
 
     function getChoices($verbose=false) {
-        return array('T' => __('One of my teams')) + Team::getTeams();
+        if (!isset($this->_teams))
+            $this->_teams = array('T' => __('One of my teams')) +
+                Team::getTeams();
+
+        return $this->_teams;
     }
 
     function getSearchQ($method, $value, $name=false) {
@@ -1315,6 +1442,19 @@ class TeamSelectionField extends AdvancedSearchSelectionField {
         $reverse = $reverse ? '-' : '';
         return $query->order_by("{$reverse}team__name");
     }
+
+    function toString($value) {
+        $choices =  $this->getChoices();
+        $selection = array();
+        if (!is_array($value))
+            $value = array($value => $value);
+        foreach ($value as $k => $v)
+            if (isset($choices[$k]))
+                $selection[] = $choices[$k];
+        return $selection ?  implode(',', $selection) :
+            parent::toString($value);
+    }
+
 }
 
 class TicketStateChoiceField extends AdvancedSearchSelectionField {
@@ -1395,6 +1535,7 @@ class OpenClosedTicketStatusList extends TicketStatusList {
         return $rv;
     }
 }
+
 class TicketStatusChoiceField extends SelectionField {
     static $widget = 'ChoicesWidget';
 
@@ -1425,6 +1566,55 @@ class TicketStatusChoiceField extends SelectionField {
             return parent::getSearchQ($method, $value, $name);
         }
     }
+
+    function applyOrderBy($query, $reverse=false, $name=false) {
+        $reverse = $reverse ? '-' : '';
+        return $query->order_by("{$reverse}status__name");
+    }
+}
+
+class TicketThreadCountField extends NumericField {
+
+    function addToQuery($query, $name=false) {
+        return TicketThreadCount::addToQuery($query, $name);
+    }
+
+    function from_query($row, $name=false) {
+         return TicketThreadCount::from_query($row, $name);
+    }
+}
+
+class TicketReopenCountField extends NumericField {
+
+    function addToQuery($query, $name=false) {
+        return TicketReopenCount::addToQuery($query, $name);
+    }
+
+    function from_query($row, $name=false) {
+         return TicketReopenCount::from_query($row, $name);
+    }
+}
+
+class ThreadAttachmentCountField extends NumericField {
+
+    function addToQuery($query, $name=false) {
+        return ThreadAttachmentCount::addToQuery($query, $name);
+    }
+
+    function from_query($row, $name=false) {
+         return ThreadAttachmentCount::from_query($row, $name);
+    }
+}
+
+class ThreadCollaboratorCountField extends NumericField {
+
+    function addToQuery($query, $name=false) {
+        return ThreadCollaboratorCount::addToQuery($query, $name);
+    }
+
+    function from_query($row, $name=false) {
+         return ThreadCollaboratorCount::from_query($row, $name);
+    }
 }
 
 interface Searchable {
diff --git a/include/class.staff.php b/include/class.staff.php
index 3833c63533505f740f8ca1da3ed13d494e9fec96..c02bdb342b525f9ff851b7b9bd70c4e2052d8f70 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -1389,7 +1389,7 @@ extends AbstractForm {
         );
     }
 
-    function getClean() {
+    function getClean($validate = true) {
         $clean = parent::getClean();
         // Index permissions as ['ticket.edit' => 1]
         $clean['perms'] = array_keys($clean['perms']);
@@ -1441,7 +1441,7 @@ extends AbstractForm {
         return __('Change the primary department and primary role of the selected agents');
     }
 
-    function getClean() {
+    function getClean($validate = true) {
         $clean = parent::getClean();
         $clean['eavesdrop'] = $clean['eavesdrop'] ? 1 : 0;
         return $clean;
@@ -1536,7 +1536,7 @@ extends AbstractForm {
         );
     }
 
-    function getClean() {
+    function getClean($validate = true) {
         $clean = parent::getClean();
         list($clean['username'],) = preg_split('/[^\w.-]/u', $clean['email'], 2);
         if (mb_strlen($clean['username']) < 3 || Staff::lookup($clean['username']))
diff --git a/include/class.thread.php b/include/class.thread.php
index 481a34835aed3ce179a8c83bf8e07b3ed3b84596..343b3e9005689e01a11ef5611e3a00179b776f82 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -2054,16 +2054,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 getDescription() {
+        return $this->description;
+    }
+
+    static function getNameById($id) {
+        return array_search($id, self::getIds());
+    }
+
+    static function getIdByName($name) {
+         $ids =  self::getIds();
+         return $ids[$name] ?: 0;
+    }
+
+    static function getIds() {
+        static $ids;
+
+        if (!isset($ids)) {
+            $ids = array();
+            $events = self::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));
     }
 
@@ -2118,7 +2182,7 @@ class ThreadEvents extends InstrumentedList {
             }
         }
         $event->username = $username;
-        $event->state = $state;
+        $event->event_id = Event::getIdByName($state);
 
         if ($data) {
             if (is_array($data))
@@ -2900,6 +2964,14 @@ implements TemplateVariable {
         return $resp;
     }
 
+    function __toString() {
+        return $this->asVar();
+    }
+
+    function asVar() {
+        return $this->getVar('complete');
+    }
+
     function getVar($name) {
         switch ($name) {
         case 'original':
@@ -2919,12 +2991,23 @@ implements TemplateVariable {
             if ($entry)
                 return $entry->getBody();
 
+            break;
+        case 'complete':
+            $content = '';
+            $thread = $this;
+            ob_start();
+            include INCLUDE_DIR.'client/templates/thread-export.tmpl.php';
+            $content = ob_get_contents();
+            ob_end_clean();
+            return $content;
+
             break;
         }
     }
 
     static function getVarScope() {
       return array(
+        'complete' => __('Thread Correspondence'),
         'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')),
         'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')),
       );
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 43446e89e67ed43a885f61045ec3266f3f1efbc2..a3f33f0521c2bb510b7e6a24fd07145c6b14b577 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -2114,27 +2114,39 @@ implements RestrictedAccess, Threadable, Searchable {
             )),
             'created' => new DatetimeField(array(
                 'label' => __('Create Date'),
-                'configuration' => array('fromdb' => true),
+                'configuration' => array(
+                    'fromdb' => true, 'time' => true,
+                    'format' => 'y-MM-dd HH:mm:ss'),
             )),
             'duedate' => new DatetimeField(array(
                 'label' => __('Due Date'),
-                'configuration' => array('fromdb' => true),
+                'configuration' => array(
+                    'fromdb' => true, 'time' => true,
+                    'format' => 'y-MM-dd HH:mm:ss'),
             )),
             'est_duedate' => new DatetimeField(array(
                 'label' => __('SLA Due Date'),
-                'configuration' => array('fromdb' => true),
+                'configuration' => array(
+                    'fromdb' => true, 'time' => true,
+                    'format' => 'y-MM-dd HH:mm:ss'),
             )),
             'reopened' => new DatetimeField(array(
                 'label' => __('Reopen Date'),
-                'configuration' => array('fromdb' => true),
+                'configuration' => array(
+                    'fromdb' => true, 'time' => true,
+                    'format' => 'y-MM-dd HH:mm:ss'),
             )),
             'closed' => new DatetimeField(array(
                 'label' => __('Close Date'),
-                'configuration' => array('fromdb' => true),
+                'configuration' => array(
+                    'fromdb' => true, 'time' => true,
+                    'format' => 'y-MM-dd HH:mm:ss'),
             )),
             'lastupdate' => new DatetimeField(array(
                 'label' => __('Last Update'),
-                'configuration' => array('fromdb' => true),
+                'configuration' => array(
+                    'fromdb' => true, 'time' => true,
+                    'format' => 'y-MM-dd HH:mm:ss'),
             )),
             'assignee' => new AssigneeChoiceField(array(
                 'label' => __('Assignee'),
@@ -2171,6 +2183,18 @@ implements RestrictedAccess, Threadable, Searchable {
             'isassigned' => new AssignedField(array(
                         'label' => __('Assigned'),
             )),
+            'thread_count' => new TicketThreadCountField(array(
+                        'label' => __('Thread Count'),
+            )),
+            'attachment_count' => new ThreadAttachmentCountField(array(
+                        'label' => __('Attachment Count'),
+            )),
+            'collaborator_count' => new ThreadCollaboratorCountField(array(
+                        'label' => __('Collaborator Count'),
+            )),
+            'reopen_count' => new TicketReopenCountField(array(
+                        'label' => __('Reopen Count'),
+            )),
             'ip_address' => new TextboxField(array(
                 'label' => __('IP Address'),
                 'configuration' => array('validator' => 'ip'),
@@ -3176,6 +3200,9 @@ implements RestrictedAccess, Threadable, Searchable {
     function save($refetch=false) {
         if ($this->dirty) {
             $this->updated = SqlFunction::NOW();
+            if (isset($this->dirty['status_id']))
+                // Refetch the queue counts
+                SavedQueue::clearCounts();
         }
         return parent::save($this->dirty || $refetch);
     }
@@ -3273,22 +3300,25 @@ implements RestrictedAccess, Threadable, Searchable {
         if (!$this->save())
             return false;
 
-	$vars['note'] = ThreadEntryBody::clean($vars['note']);
+        $vars['note'] = ThreadEntryBody::clean($vars['note']);
         if ($vars['note'])
             $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff);
 
         // Update dynamic meta-data
-        foreach ($forms as $f) {
-            if ($C = $f->getChanges())
+        foreach ($forms as $form) {
+            if ($C = $form->getChanges())
                 $changes['fields'] = ($changes['fields'] ?: array()) + $C;
             // Drop deleted forms
-            $idx = array_search($f->getId(), $vars['forms']);
+            $idx = array_search($form->getId(), $vars['forms']);
             if ($idx === false) {
-                $f->delete();
+                $form->delete();
             }
             else {
-                $f->set('sort', $idx);
-                $f->save();
+                $form->set('sort', $idx);
+                $form->saveAnswers(function($f) {
+                        return $f->isVisibleToStaff()
+                        && $f->isEditableToStaff(); }
+                        );
             }
         }
 
@@ -3331,7 +3361,10 @@ implements RestrictedAccess, Threadable, Searchable {
                     __($field->getLabel()));
         else {
             if ($field->answer) {
-                if (!$field->save())
+                if (!$field->isEditableToStaff())
+                    $errors['field'] = sprintf(__('%s can not be edited'),
+                            __($field->getLabel()));
+                elseif (!$field->save())
                     $errors['field'] =  __('Unable to update field');
                 $changes['fields'] = array($field->getId() => $changes);
             } else {
@@ -4186,7 +4219,8 @@ implements RestrictedAccess, Threadable, Searchable {
          Punt for now
          */
 
-        $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 '
+        $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1'
+            .' USE INDEX (status_id)'
             .' INNER JOIN '.TICKET_STATUS_TABLE.' status
                 ON (status.id=T1.status_id AND status.state="open") '
             .' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.flags & 1 = 1) '
diff --git a/include/class.translation.php b/include/class.translation.php
index 2e864b39383fd0dc26b4244eb4f68e304eb532d8..6a60b0acf33952d90781eef1c61dda6a0b3df644 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -671,7 +671,7 @@ class Translation extends gettext_reader implements Serializable {
         list($this->charset, $this->encode, $this->cache_translations)
             = unserialize($what);
         $this->short_circuit = ! $this->enable_cache
-            = 0 < count($this->cache_translations);
+            = 0 < $this->cache_translations ? count($this->cache_translations) : 1;
     }
 }
 
diff --git a/include/class.user.php b/include/class.user.php
index 0fa35e4a14d62514632bcf163f80fc25e216b49c..36e1df54848c17b7e8de08268135820cb0079ad0 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -359,7 +359,7 @@ implements TemplateVariable, Searchable {
                 'email' => (string) $this->getEmail(),
                 'phone' => (string) $this->getPhoneNumber());
 
-        return JsonDataEncoder::encode($info);
+        return Format::json_encode($info);
     }
 
     function __toString() {
@@ -451,19 +451,20 @@ implements TemplateVariable, Searchable {
         return $vars;
     }
 
-    function getForms($data=null) {
+    function getForms($data=null, $cb=null) {
 
         if (!isset($this->_forms)) {
             $this->_forms = array();
+            $cb = $cb ?: function ($f) use($data) { return ($data); };
             foreach ($this->getDynamicData() as $entry) {
                 $entry->addMissingFields();
-                if(!$data
-                        && ($form = $entry->getDynamicForm())
+                if(($form = $entry->getDynamicForm())
                         && $form->get('type') == 'U' ) {
+
                     foreach ($entry->getFields() as $f) {
-                        if ($f->get('name') == 'name')
+                        if ($f->get('name') == 'name' && !$cb($f))
                             $f->value = $this->getFullName();
-                        elseif ($f->get('name') == 'email')
+                        elseif ($f->get('name') == 'email' && !$cb($f))
                             $f->value = $this->getEmail();
                     }
                 }
@@ -529,17 +530,23 @@ implements TemplateVariable, Searchable {
 
     function updateInfo($vars, &$errors, $staff=false) {
 
+
+        $isEditable = function ($f) use($staff) {
+            return ($staff ? $f->isEditableToStaff() :
+                    $f->isEditableToUsers());
+        };
         $valid = true;
-        $forms = $this->getForms($vars);
+        $forms = $this->getForms($vars, $isEditable);
         foreach ($forms as $entry) {
             $entry->setSource($vars);
-            if ($staff && !$entry->isValidForStaff())
+            if ($staff && !$entry->isValidForStaff(true))
                 $valid = false;
-            elseif (!$staff && !$entry->isValidForClient())
+            elseif (!$staff && !$entry->isValidForClient(true))
                 $valid = false;
             elseif ($entry->getDynamicForm()->get('type') == 'U'
                     && ($f=$entry->getField('email'))
-                    &&  $f->getClean()
+                    && $isEditable($f)
+                    && $f->getClean()
                     && ($u=User::lookup(array('emails__address'=>$f->getClean())))
                     && $u->id != $this->getId()) {
                 $valid = false;
@@ -558,7 +565,7 @@ implements TemplateVariable, Searchable {
         foreach ($forms as $entry) {
             if ($entry->getDynamicForm()->get('type') == 'U') {
                 //  Name field
-                if (($name = $entry->getField('name'))) {
+                if (($name = $entry->getField('name')) && $isEditable($name) ) {
                     $name = $name->getClean();
                     if (is_array($name))
                         $name = implode(', ', $name);
@@ -566,14 +573,15 @@ implements TemplateVariable, Searchable {
                 }
 
                 // Email address field
-                if (($email = $entry->getField('email'))) {
+                if (($email = $entry->getField('email'))
+                        && $isEditable($email)) {
                     $this->default_email->address = $email->getClean();
                     $this->default_email->save();
                 }
             }
 
-            // DynamicFormEntry::save returns the number of answers updated
-            if ($entry->save()) {
+            // DynamicFormEntry::saveAnswers returns the number of answers updated
+            if ($entry->saveAnswers($isEditable)) {
                 $this->updated = SqlFunction::NOW();
             }
         }
@@ -635,11 +643,11 @@ implements TemplateVariable, Searchable {
     }
 
     function deleteAllTickets() {
-        $deleted = TicketStatus::lookup(array('state' => 'deleted'));
+        $status_id = TicketStatus::lookup(array('state' => 'deleted'));
         foreach($this->tickets as $ticket) {
             if (!$T = Ticket::lookup($ticket->getId()))
                 continue;
-            if (!$T->setStatus($deleted))
+            if (!$T->setStatus($status_id))
                 return false;
         }
         $this->tickets->reset();
diff --git a/include/cli/modules/upgrade.php b/include/cli/modules/upgrade.php
index 297d6d56f84d36dcdc1ad23c42410a09d483b165..383c47f5c66226ac32f6ae102f502e27fcbe1645 100644
--- a/include/cli/modules/upgrade.php
+++ b/include/cli/modules/upgrade.php
@@ -51,12 +51,12 @@ class CliUpgrader extends Module {
         $cfg = $ost->getConfig();
 
         while (true) {
-            if ($upgrader->getTask()) {
-                // If there's anythin in the model cache (like a Staff
-                // object or something), ensure that changes to the database
-                // model won't cause crashes
-                ModelInstanceManager::flushCache();
+            // If there's anythin in the model cache (like a Staff
+            // object or something), ensure that changes to the database
+            // model won't cause crashes
+            ModelInstanceManager::flushCache();
 
+            if ($upgrader->getTask()) {
                 // More pending tasks - doTasks returns the number of pending tasks
                 $this->stdout->write("... {$upgrader->getNextAction()}\n");
                 $upgrader->doTask();
diff --git a/include/client/open.inc.php b/include/client/open.inc.php
index 4a4022c8df3cc8a98405e576942fd7a2d9599ba2..d0a96effc9dbfa94f840ea6ec5e14de4a7c7d881 100644
--- a/include/client/open.inc.php
+++ b/include/client/open.inc.php
@@ -42,7 +42,7 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
         if (!$thisclient) {
             $uform = UserForm::getUserForm()->getForm($_POST);
             if ($_POST) $uform->isValid();
-            $uform->render(array('staff' => false));
+            $uform->render(false, false, array('mode' => 'create'));
         }
         else { ?>
             <tr><td colspan="2"><hr /></td></tr>
@@ -89,7 +89,9 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
     </tr>
     </tbody>
     <tbody id="dynamic-form">
-        <?php foreach ($forms as $form) {
+        <?php
+        $options = array('mode' => 'create');
+        foreach ($forms as $form) {
             include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php');
         } ?>
     </tbody>
diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php
index cfde56a64e9d00c27adc7ed06290c51cb0b69346..f668f4320daf5e690a3095aed43aae96bcfb486e 100644
--- a/include/client/templates/dynamic-form.tmpl.php
+++ b/include/client/templates/dynamic-form.tmpl.php
@@ -1,8 +1,11 @@
 <?php
-    // Form headline and deck with a horizontal divider above and an extra
-    // space below.
-    // XXX: Would be nice to handle the decoration with a CSS class
-    ?>
+// Return if no visible fields
+global $thisclient;
+if (!$form->hasAnyVisibleFields($thisclient))
+    return;
+
+$isCreate = (isset($options['mode']) && $options['mode'] == 'create');
+?>
     <tr><td colspan="2"><hr />
     <div class="form-header" style="margin-bottom:0.5em">
     <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3>
@@ -12,13 +15,19 @@
     <?php
     // Form fields, each with corresponding errors follows. Fields marked
     // 'private' are not included in the output for clients
-    global $thisclient;
     foreach ($form->getFields() as $field) {
-        if (isset($options['mode']) && $options['mode'] == 'create') {
-            if (!$field->isVisibleToUsers() && !$field->isRequiredForUsers())
+        try {
+            if (!$field->isEnabled())
                 continue;
         }
-        elseif (!$field->isVisibleToUsers() && !$field->isEditableToUsers()) {
+        catch (Exception $e) {
+            // Not connected to a DynamicFormField
+        }
+
+        if ($isCreate) {
+            if (!$field->isVisibleToUsers() && !$field->isRequiredForUsers())
+                continue;
+        } elseif (!$field->isVisibleToUsers()) {
             continue;
         }
         ?>
@@ -28,7 +37,8 @@
                 <label for="<?php echo $field->getFormName(); ?>"><span class="<?php
                     if ($field->isRequiredForUsers()) echo 'required'; ?>">
                 <?php echo Format::htmlchars($field->getLocal('label')); ?>
-            <?php if ($field->isRequiredForUsers()) { ?>
+            <?php if ($field->isRequiredForUsers() &&
+                    ($field->isEditableToUsers() || $isCreate)) { ?>
                 <span class="error">*</span>
             <?php }
             ?></span><?php
@@ -40,12 +50,22 @@
             <br/>
             <?php
             }
-            $field->render(array('client'=>true));
-            ?></label><?php
-            foreach ($field->errors() as $e) { ?>
-                <div class="error"><?php echo $e; ?></div>
-            <?php }
-            $field->renderExtras(array('client'=>true));
+            if ($field->isEditableToUsers() || $isCreate) {
+                $field->render(array('client'=>true));
+                ?></label><?php
+                foreach ($field->errors() as $e) { ?>
+                    <div class="error"><?php echo $e; ?></div>
+                <?php }
+                $field->renderExtras(array('client'=>true));
+            } else {
+                $val = '';
+                if ($field->value)
+                    $val = $field->display($field->value);
+                elseif (($a=$field->getAnswer()))
+                    $val = $a->display();
+
+                echo sprintf('%s </label>', $val);
+            }
             ?>
             </td>
         </tr>
diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php
index f54fa4f6b228243fb3a1c20588fd127752929082..b3df17773728dbb2154d6c1707360e82f02fd2d1 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');
 $eventCount = count($events);
 $events = new IteratorIterator($events->getIterator());
diff --git a/include/client/templates/thread-export.tmpl.php b/include/client/templates/thread-export.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..5914a88da94a48a5da28c31dd1685a4e2f438ae4
--- /dev/null
+++ b/include/client/templates/thread-export.tmpl.php
@@ -0,0 +1,93 @@
+<?php
+global $cfg;
+
+$entryTypes = array(
+        'M' => array('color' => '#0088cc'),
+        'R' => array('color' => '#e65524'),
+        );
+
+AttachmentFile::objects()->filter(array(
+            'attachments__thread_entry__thread__id' => $thread->getId(),
+            'attachments__thread_entry__type__in' => array_keys($entryTypes)
+            ))->all();
+
+$entries = $thread->getEntries();
+$entries->filter(array('type__in' => array_keys($entryTypes)));
+?>
+<style type="text/css">
+    div {font-family: sans-serif;}
+</style>
+<div style="width: 100%; margin: 0; padding: 0;">
+    <div style="padding:10px;">
+    <p style="font-family: sans-serif; font-size:12px; color:#999;">&nbsp;</p>
+    </div>
+    <table width="100%" cellpadding="0" cellspacing="0" border="0">
+        <tbody>
+            <tr>
+                <td></td>
+            </tr>
+            <?php
+            foreach ($entries as $entry) {
+                $user = $entry->getUser() ?: $entry->getStaff();
+                $name = $user ? $user->getName() : $entry->poster;
+                $color = $entryTypes[$entry->type]['color'];
+                ?>
+                <tr>
+                    <td style=" border-top: 1px dashed #999;">
+                        <div style="background-color:#f7f7f7; padding:10px 20px;">
+                            <p style="font-family: sans-serif; padding:0; margin:0; color:<?php echo $color; ?>;">
+                                <strong><?php echo $name; ?></strong>
+                                <span style="color:#888; font-size:12px; padding-left: 20px;"><?php
+                                    echo $entry->title;
+                                ?>
+                                </span>
+                            </p>
+                            <p style="font-family: sans-serif; padding:0; margin:0; color:#888; font-size:12px;">
+                            <?php
+                            echo Format::daydatetime($entry->created);
+                            ?>
+                            </p>
+                        </div>
+                        <div style="padding:2px 20px;">
+                            <p style="font-family: sans-serif; font-size:14px; color:#555;">
+                                <?php
+                                echo $entry->getBody()->display('email');
+                                ?>
+                            </p>
+                            <?php
+                            if ($entry->has_attachments) { ?>
+                            <p style="font-family: sans-serif; font-size:12px; line-height:20px; color:#888;">
+                                <?php echo __('Attachments'); ?>
+                                <br />
+                                <?php
+                                foreach ($entry->attachments as $a) {
+                                    if ($a->inline) continue;
+                                    $size = '';
+                                    if ($a->file->size)
+                                        $size = sprintf('<small style="color:#ccc;">&nbsp;(%s)</small>',
+                                                Format::file_size($a->file->size));
+
+                                    $filename = Format::htmlchars($a->getFilename());
+                                    echo sprintf('<a href="%s" download="%s"
+                                            style="font-size:11px; color:#0088cc;"
+                                            target="_blank">%s</a>&nbsp;&nbsp;&nbsp;%s<br/>',
+                                            $a->file->getExternalDownloadUrl(),
+                                            $filename,
+                                            $filename,
+                                            $size);
+                                }
+                            ?>
+                            </p>
+                            <?php
+                            } ?>
+                        </div>
+                    </td>
+                </tr>
+            <?php
+            } ?>
+        </tbody>
+    </table>
+    <div style="font-family: sans-serif; margin: 2px 0 14px 0; padding: 10px ; border-top: 1px solid #999; font-size:12px; color:#888;">
+        &nbsp;
+    </div>
+</div>
diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml
index 731e51eb297b2e37469d40562c7cf23d4b28300e..e0c47f7149c49254782e2a020962e8ae4116c20a 100644
--- a/include/i18n/en_US/config.yaml
+++ b/include/i18n/en_US/config.yaml
@@ -77,6 +77,7 @@ core:
     email_attachments: 1
     ticket_number_format: '######'
     ticket_sequence_id: 0
+    queue_bucket_counts: 0
     task_number_format: '#'
     task_sequence_id: 2
     log_level: 2
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/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml
index 3b0c1cea9005b7fea87da7a97a7d9702fda6a8af..a736db8e0cc29871f04d5936c296c605bde495d2 100644
--- a/include/i18n/en_US/help/tips/settings.ticket.yaml
+++ b/include/i18n/en_US/help/tips/settings.ticket.yaml
@@ -32,6 +32,12 @@ sequence_id:
         in the <span class="doc-desc-title">Ticket Number
         Format</span> configuration for help topics.
 
+queue_bucket_counts:
+    title: Top-Level Ticket Counts
+    content: >
+        This setting is used to hide or show the ticket counts on Main-Level
+        queues. Get back to the way things used to be.
+
 default_ticket_status:
     title: Default Status for new Tickets
     content: >
diff --git a/include/i18n/en_US/queue.yaml b/include/i18n/en_US/queue.yaml
index b7325620c2df64ffb66322a400d05ca81879258e..83870a34706954d96c8af7e8038f813e137a4b72 100644
--- a/include/i18n/en_US/queue.yaml
+++ b/include/i18n/en_US/queue.yaml
@@ -32,6 +32,7 @@
   parent_id: 0
   flags: 0x03
   sort: 1
+  sort_id: 1
   root: T
   config: '[["status__state","includes",{"open":"Open"}]]'
   columns:
@@ -118,7 +119,7 @@
   parent_id: 1
   flags: 0x2b
   root: T
-  sort: 1
+  sort: 2
   sort_id: 4
   config: '{"criteria":[["isanswered","set",null]],"conditions":[]}'
   columns:
@@ -158,7 +159,7 @@
   parent_id: 1
   flags: 0x2b
   root: T
-  sort: 2
+  sort: 3
   sort_id: 4
   config: '{"criteria":[["isoverdue","set",null]],"conditions":[]}'
   columns:
@@ -204,6 +205,7 @@
   flags: 0x03
   root: T
   sort: 3
+  sort_id: 3
   config: '{"criteria":[["assignee","includes",{"M":"Me","T":"One of my teams"}],["status__state","includes",{"open":"Open"}]],"conditions":[]}'
   columns:
     - column_id: 1
diff --git a/include/i18n/en_US/queue_column.yaml b/include/i18n/en_US/queue_column.yaml
index 03250da19a83b3118bbd04eb4792218c232928dd..87b3c09c13bee6d5b4757ca6329f35a715ff6f28 100644
--- a/include/i18n/en_US/queue_column.yaml
+++ b/include/i18n/en_US/queue_column.yaml
@@ -69,8 +69,8 @@
   conditions: "[]"
 
 - id: 6
-  name: "Status Name"
-  primary: "status__name"
+  name: "Status"
+  primary: "status__id"
   truncate: "wrap"
   annotations: "[]"
   conditions: "[]"
diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php
index caab736fde8588f807d885fcaac6193819bd3451..b743aa5f62add15ab1e2e9cf42e937e04ccacdce 100644
--- a/include/staff/department.inc.php
+++ b/include/staff/department.inc.php
@@ -99,9 +99,9 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info);
             </td>
             <td>
                 <select name="status">
-                  <option value="active"<?php echo ($info['status'] == __('Active'))?'selected="selected"':'';?>><?php echo __('Active'); ?></option>
-                  <option value="disabled"<?php echo ($info['status'] == __('Disabled'))?'selected="selected"':'';?>><?php echo __('Disabled'); ?></option>
-                  <option value="archived"<?php echo ($info['status'] == __('Archived'))?'selected="selected"':'';?>><?php echo __('Archived'); ?></option>
+                  <option value="active"<?php echo (!strcasecmp($info['status'], 'active'))?'selected="selected"':'';?>><?php echo __('Active'); ?></option>
+                  <option value="disabled"<?php echo (!strcasecmp($info['status'], 'disabled'))?'selected="selected"':'';?>><?php echo __('Disabled'); ?></option>
+                  <option value="archived"<?php echo (!strcasecmp($info['status'], 'archived'))?'selected="selected"':'';?>><?php echo __('Archived'); ?></option>
                 </select>
                 &nbsp;<span class="error">&nbsp;</span> <i class="help-tip icon-question-sign" href="#status"></i>
             </td>
diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php
index 4fd4a34c9fe0870e6c006886bd5a23d88e68d5ec..bec901732d9052c03ae0a660abc956ebdd12177d 100644
--- a/include/staff/departments.inc.php
+++ b/include/staff/departments.inc.php
@@ -143,7 +143,7 @@ $showing = $pageNav->showing().' '._N('department', 'departments', $count);
                 echo Dept::getNameById($id); ?></a>&nbsp;<?php echo $default; ?>
                 </td>
                 <td><?php
-                  if($dept->getStatus() == __('Active'))
+                  if(!strcasecmp($dept->getStatus(), 'Active'))
                     echo $dept->getStatus();
                   else
                     echo '<b>'.$dept->getStatus();
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index 76db039c0ce14a36de83b1b9240e21e38f42f266..95636299e687419ee832990b63102ce511348cb3 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -65,6 +65,12 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <i class="help-tip icon-question-sign" href="#sequence_id"></i>
             </td>
         </tr>
+        <tr><td width="220"><?php echo __('Top-Level Ticket Counts'); ?>:</td>
+            <td>
+                <input type="checkbox" name="queue_bucket_counts" <?php echo $config['queue_bucket_counts']?'checked="checked"':''; ?>>
+                <?php echo __('Enable'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#queue_bucket_counts"></i>
+            </td>
+        </tr>
         <tr>
             <td width="180" class="required">
                 <?php echo __('Default Status'); ?>:
diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php
index ef18c8bd01c5a988f063d80caea823da0f857ca1..00abf34cbb93d08e8d73c1e36ed6beed2e3ef61a 100644
--- a/include/staff/templates/dynamic-form.tmpl.php
+++ b/include/staff/templates/dynamic-form.tmpl.php
@@ -1,6 +1,8 @@
 <?php
-// If the form was removed using the trashcan option, and there was some
-// other validation error, don't render the deleted form the second time
+global $thisstaff;
+
+$isCreate = (isset($options['mode']) && $options['mode'] == 'create');
+
 if (isset($options['entry']) && $options['mode'] == 'edit'
     && $_POST
     && ($_POST['forms'] && !in_array($options['entry']->getId(), $_POST['forms']))
@@ -42,8 +44,6 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
         try {
             if (!$field->isEnabled())
                 continue;
-            if ($options['mode'] == 'edit' && !$field->isEditableToStaff())
-                continue;
         }
         catch (Exception $e) {
             // Not connected to a DynamicFormField
@@ -60,41 +60,52 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
                 <?php echo Format::htmlchars($field->getLocal('label')); ?>:</td>
                 <td><div style="position:relative"><?php
             }
-            $field->render($options); ?>
-            <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?>
-                <span class="error">*</span>
-            <?php
-            }
-            if ($field->isStorable() && ($a = $field->getAnswer()) && $a->isDeleted()) {
-                ?><a class="action-button float-right danger overlay" title="Delete this data"
-                    href="#delete-answer"
-                    onclick="javascript:if (confirm('<?php echo __('You sure?'); ?>'))
-                        $.ajax({
-                            url: 'ajax.php/form/answer/'
-                                +$(this).data('entryId') + '/' + $(this).data('fieldId'),
-                            type: 'delete',
-                            success: $.proxy(function() {
-                                $(this).closest('tr').fadeOut();
-                            }, this)
-                        });"
-                    data-field-id="<?php echo $field->getAnswer()->get('field_id');
-                ?>" data-entry-id="<?php echo $field->getAnswer()->get('entry_id');
-                ?>"> <i class="icon-trash"></i> </a></div><?php
-            }
-            if ($a && !$a->getValue() && $field->isRequiredForClose()) {
-?><i class="icon-warning-sign help-tip warning"
-    data-title="<?php echo __('Required to close ticket'); ?>"
-    data-content="<?php echo __('Data is required in this field in order to close the related ticket'); ?>"
-/></i><?php
-            }
-            if ($field->get('hint') && !$field->isBlockLevel()) { ?>
-                <br /><em style="color:gray;display:inline-block"><?php
-                    echo Format::viewableImages($field->getLocal('hint')); ?></em>
-            <?php
-            }
-            foreach ($field->errors() as $e) { ?>
-                <div class="error"><?php echo Format::htmlchars($e); ?></div>
-            <?php } ?>
+
+            if ($field->isEditableToStaff() || $isCreate) {
+                $field->render($options); ?>
+                <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?>
+                    <span class="error">*</span>
+                <?php
+                }
+                if ($field->isStorable() && ($a = $field->getAnswer()) && $a->isDeleted()) {
+                    ?><a class="action-button float-right danger overlay" title="Delete this data"
+                        href="#delete-answer"
+                        onclick="javascript:if (confirm('<?php echo __('You sure?'); ?>'))
+                            $.ajax({
+                                url: 'ajax.php/form/answer/'
+                                    +$(this).data('entryId') + '/' + $(this).data('fieldId'),
+                                type: 'delete',
+                                success: $.proxy(function() {
+                                    $(this).closest('tr').fadeOut();
+                                }, this)
+                            });"
+                        data-field-id="<?php echo $field->getAnswer()->get('field_id');
+                    ?>" data-entry-id="<?php echo $field->getAnswer()->get('entry_id');
+                    ?>"> <i class="icon-trash"></i> </a></div><?php
+                }
+                if ($a && !$a->getValue() && $field->isRequiredForClose()) {
+    ?><i class="icon-warning-sign help-tip warning"
+        data-title="<?php echo __('Required to close ticket'); ?>"
+        data-content="<?php echo __('Data is required in this field in order to close the related ticket'); ?>"
+    /></i><?php
+                }
+                if ($field->get('hint') && !$field->isBlockLevel()) { ?>
+                    <br /><em style="color:gray;display:inline-block"><?php
+                        echo Format::viewableImages($field->getLocal('hint')); ?></em>
+                <?php
+                }
+                foreach ($field->errors() as $e) { ?>
+                    <div class="error"><?php echo Format::htmlchars($e); ?></div>
+                <?php }
+            } else {
+                $val = '';
+                if ($field->value)
+                    $val = $field->display($field->value);
+                elseif (($a= $field->getAnswer()))
+                    $val = $a->display();
+
+                echo $val;
+            }?>
             </div></td>
         </tr>
     <?php }
diff --git a/include/staff/templates/mark-as.tmpl.php b/include/staff/templates/mark-as.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..49c2d2c8664467dcf3ba6dfac34f575f8a471604
--- /dev/null
+++ b/include/staff/templates/mark-as.tmpl.php
@@ -0,0 +1,57 @@
+<?php
+global $cfg;
+
+$form = MarkAsForm::instantiate($_POST);
+?>
+<h3 class="drag-handle"><?php echo $info[':title'] ?:  __('Please Confirm'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} elseif ($info['notice']) {
+   echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>',
+           $info['notice']);
+}
+?>
+<form class="mass-action" method="post"
+    action="#tickets/<?php echo $ticket->getId(); ?>/mark/<?php echo $action; ?>"
+    name="markAs">
+    <table width="100%">
+        <tbody>
+            <tr><td>
+                <p>
+                <?php echo sprintf(
+                            __('Are you sure you want to mark ticket as <b>%s</b>?'),
+                            $action); ?>
+                </p>
+                <p>
+                <?php echo __('Please confirm to continue.'); ?>
+                </p>
+            </td></tr>
+            <tr><td>
+                <p>
+                <?php print $form->getField('comments')->render(); ?>
+                </p>
+            </td></tr>
+        </tbody>
+    </table>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+                value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php
+            echo __('OK'); ?>">
+        </span>
+    </p>
+</form>
+<div class="clear"></div>
diff --git a/include/staff/templates/queue-navigation.tmpl.php b/include/staff/templates/queue-navigation.tmpl.php
index 380e03af961d5f8f6901d9bf63eadb55bb7679a4..f408c21843b1ce40b11cf9f70f4465d005b5c84d 100644
--- a/include/staff/templates/queue-navigation.tmpl.php
+++ b/include/staff/templates/queue-navigation.tmpl.php
@@ -4,6 +4,7 @@
 // $q - <CustomQueue> object for this navigation entry
 // $selected - <bool> true if this queue is currently active
 // $child_selected - <bool> true if the selected queue is a descendent
+global $cfg;
 $childs = $children;
 $this_queue = $q;
 $selected = (!isset($_REQUEST['a'])  && $_REQUEST['queue'] == $this_queue->getId());
@@ -11,7 +12,15 @@ $selected = (!isset($_REQUEST['a'])  && $_REQUEST['queue'] == $this_queue->getId
 <li class="top-queue item <?php if ($child_selected) echo 'child active';
     elseif ($selected) echo 'active'; ?>">
   <a href="<?php echo $this_queue->getHref(); ?>"
-    class="Ticket"><i class="small icon-sort-down pull-right"></i><?php echo $this_queue->getName(); ?></a>
+    class="Ticket"><i class="small icon-sort-down pull-right"></i><?php echo $this_queue->getName(); ?>
+<?php if ($cfg->showTopLevelTicketCounts()) { ?>
+    <span id="queue-count-bucket" class="hidden">
+      (<span class="queue-count"
+        data-queue-id="<?php echo $this_queue->id; ?>"><span class="faded-more"></span>
+      </span>)
+    </span>
+<?php } ?>
+  </a>
   <div class="customQ-dropdown">
     <ul class="scroll-height">
       <!-- Add top-level queue (with count) -->
diff --git a/include/staff/templates/queue-savedsearches-nav.tmpl.php b/include/staff/templates/queue-savedsearches-nav.tmpl.php
index ef06cf06c91db2b6e5a32ea0395885cc8d05cd25..34a8aff168cb10326f1e8a927f506565d3de13fb 100644
--- a/include/staff/templates/queue-savedsearches-nav.tmpl.php
+++ b/include/staff/templates/queue-savedsearches-nav.tmpl.php
@@ -4,6 +4,10 @@
 // $searches = All visibile saved searches
 // $child_selected - <bool> true if the selected queue is a descendent
 // $adhoc - not FALSE if an adhoc advanced search exists
+
+$searches = SavedQueue::getHierarchicalQueues($thisstaff);
+if ($queue && !$queue->parent_id && $queue->staff_id)
+    $child_selected = true;
 ?>
 <li class="primary-only item <?php if ($child_selected) echo 'active'; ?>">
 <?php
@@ -16,14 +20,9 @@
   <div class="customQ-dropdown">
     <ul class="scroll-height">
       <!-- Start Dropdown and child queues -->
-      <?php foreach ($searches->findAll(array(
-            'staff_id' => $thisstaff->getId(),
-            'parent_id' => 0,
-            Q::not(array(
-                'flags__hasbit' => CustomQueue::FLAG_PUBLIC
-            ))
-      )) as $q) {
-        if ($q->checkAccess($thisstaff))
+      <?php foreach ($searches as $search) {
+          list($q, $children) = $search;
+          if ($q->checkAccess($thisstaff))
             include 'queue-subnavigation.tmpl.php';
       } ?>
      <?php
diff --git a/include/staff/templates/queue-sorting-edit.tmpl.php b/include/staff/templates/queue-sorting-edit.tmpl.php
index 0a2d98b0246433b59ea0f9cc1d575deb3a4ed41b..a001201a2beb94e6c3391f2965e6353882dfc8c3 100644
--- a/include/staff/templates/queue-sorting-edit.tmpl.php
+++ b/include/staff/templates/queue-sorting-edit.tmpl.php
@@ -5,6 +5,7 @@
  * $column - <QueueColumn> instance for this column
  */
 $sortid = $sort->getId();
+$advanced = in_array('extra', $sort::getMeta()->getFieldNames());
 ?>
 <h3 class="drag-handle"><?php echo __('Manage Sort Options'); ?> &mdash;
     <?php echo $sort->get('name') ?></h3>
@@ -14,10 +15,30 @@ $sortid = $sort->getId();
 <form method="post" action="#tickets/search/sort/edit/<?php
     echo $sortid; ?>">
 
+<?php if ($advanced) { ?>
+  <ul class="clean tabs">
+    <li class="active"><a href="#fields"><i class="icon-columns"></i>
+      <?php echo __('Fields'); ?></a></li>
+    <li><a href="#advanced"><i class="icon-cog"></i>
+      <?php echo __('Advanced'); ?></a></li>
+  </ul>
+
+  <div class="tab_content" id="fields">
+<?php } ?>
+
 <?php
 include 'queue-sorting.tmpl.php';
 ?>
 
+<?php if ($advanced) { ?>
+  </div>
+
+  <div class="hidden tab_content" id="advanced">
+    <?php echo $sort->getAdvancedConfigForm()->asTable(); ?>
+  </div>
+
+<?php } ?>
+
 <hr>
 <p class="full-width">
     <span class="buttons pull-left">
diff --git a/include/staff/templates/queue-tickets.tmpl.php b/include/staff/templates/queue-tickets.tmpl.php
index 37d7cced74789b85fd6aa1f6cf6b0dc720eacc30..699b28a351e98f547528d3ea2cbd9936603da4cf 100644
--- a/include/staff/templates/queue-tickets.tmpl.php
+++ b/include/staff/templates/queue-tickets.tmpl.php
@@ -76,8 +76,8 @@ if (!$sorted && isset($sort['queuesort'])) {
 $page = ($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav = new Pagenate(PHP_INT_MAX, $page, PAGE_LIMIT);
 $tickets = $pageNav->paginateSimple($tickets);
-$count = $tickets->total();
-$pageNav->setTotal($count);
+$count = $queue->getCount($thisstaff) ?: (PAGE_LIMIT*3);
+$pageNav->setTotal($count, true);
 $pageNav->setURL('tickets.php', $args);
 ?>
 
diff --git a/include/staff/templates/user-lookup.tmpl.php b/include/staff/templates/user-lookup.tmpl.php
index 62547c99bc7e1d8e59f6222d3859116b89437a97..f46fd7c99e9e4689183998212220730b6485973f 100644
--- a/include/staff/templates/user-lookup.tmpl.php
+++ b/include/staff/templates/user-lookup.tmpl.php
@@ -83,8 +83,8 @@ if ($user) { ?>
 <form method="post" class="user" action="<?php echo $info['action'] ?: '#users/lookup/form'; ?>">
     <table width="100%" class="fixed">
     <?php
-        if(!$form) $form = UserForm::getInstance();
-        $form->render(['staff' => true, 'title' => __('Create New User')]); ?>
+        $form = $form ?: UserForm::getInstance();
+        $form->render(true, __('Create New User'), array('mode' => 'create')); ?>
     </table>
     <hr>
     <p class="full-width">
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index 09bcfb21fdf95ee2a05366724b6ee921c63eaf48..267d927651833ba064e7bf38b1978b100433c752 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -348,6 +348,7 @@ if ($_POST)
         </tbody>
         <tbody id="dynamic-form">
         <?php
+            $options = array('mode' => 'create');
             foreach ($forms as $form) {
                 print $form->getForm($_SESSION[':form-data'])->getMedia();
                 include(STAFFINC_DIR .  'templates/dynamic-form.tmpl.php');
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 9fd5835b562c73b6ec5ce5e7e9f8b42b5cfc93f5..bc1573f354b5358d458d6962c8bca8b27d583680 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -22,6 +22,7 @@ $mylock = ($lock && $lock->getStaffId() == $thisstaff->getId()) ? $lock : null;
 $id    = $ticket->getId();    //Ticket ID.
 $isManager = $dept->isManager($thisstaff); //Check if Agent is Manager
 $canRelease = ($isManager || $role->hasPerm(Ticket::PERM_RELEASE)); //Check if Agent can release tickets
+$canAnswer = ($isManager || $role->hasPerm(Ticket::PERM_REPLY)); //Check if Agent can mark as answered/unanswered
 
 //Useful warnings and errors the user might want to know!
 if ($ticket->isClosed() && !$ticket->isReopenable())
@@ -161,13 +162,20 @@ if($ticket->isOverdue())
                             echo __('Mark as Overdue'); ?></a></li>
                     <?php
                     }
+                 } elseif($ticket->isOpen() && $canAnswer) {
 
                     if($ticket->isAnswered()) { ?>
-                    <li><a class="confirm-action" id="ticket-unanswered" href="#unanswered"><i class="icon-circle-arrow-left"></i> <?php
+                    <li><a href="#tickets/<?php echo $ticket->getId();
+                        ?>/mark/unanswered" class="ticket-action"
+                            data-redirect="tickets.php?id=<?php echo $ticket->getId(); ?>">
+                            <i class="icon-circle-arrow-left"></i> <?php
                             echo __('Mark as Unanswered'); ?></a></li>
                     <?php
                     } else { ?>
-                    <li><a class="confirm-action" id="ticket-answered" href="#answered"><i class="icon-circle-arrow-right"></i> <?php
+                    <li><a href="#tickets/<?php echo $ticket->getId();
+                        ?>/mark/answered" class="ticket-action"
+                            data-redirect="tickets.php?id=<?php echo $ticket->getId(); ?>">
+                            <i class="icon-circle-arrow-right"></i> <?php
                             echo __('Mark as Answered'); ?></a></li>
                     <?php
                     }
@@ -598,7 +606,9 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
     )));
     $displayed = array();
     foreach($answers as $a) {
-        $displayed[] = array($a->getLocal('label'), $a->display() ?: '<span class="faded">&mdash;' . __('Empty') . '&mdash; </span>', $a->getLocal('id'), ($a->getField() instanceof FileUploadField));
+        if (!$a->getField()->isVisibleToStaff())
+            continue;
+        $displayed[] = $a;
     }
     if (count($displayed) == 0)
         continue;
@@ -609,13 +619,18 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
     </thead>
     <tbody>
 <?php
-    foreach ($displayed as $stuff) {
-        list($label, $v, $id, $isFile) = $stuff;
+    foreach ($displayed as $a) {
+        $id =  $a->getLocal('id');
+        $label = $a->getLocal('label');
+        $v = $a->display() ?: '<span class="faded">&mdash;' . __('Empty') .  '&mdash; </span>';
+        $field = $a->getField();
+        $isFile = ($field instanceof FileUploadField);
 ?>
         <tr>
             <td width="200"><?php echo Format::htmlchars($label); ?>:</td>
             <td>
-            <?php if ($role->hasPerm(Ticket::PERM_EDIT)) {
+            <?php if ($role->hasPerm(Ticket::PERM_EDIT)
+                    && $field->isEditableToStaff()) {
                     $isEmpty = strpos($v, '&mdash;');
                     if ($isFile && !$isEmpty)
                         echo $v.'<br>'; ?>
@@ -633,7 +648,8 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
                       echo $v;
                   ?>
               </a>
-            <?php } else {
+            <?php
+            } else {
                 echo $v;
             } ?>
             </td>
diff --git a/include/upgrader/done.inc.php b/include/upgrader/done.inc.php
index 39c598d7f49a283ca0539f11f024ad57812eefd9..ff3bb9ffd1660205d2620cda33d4466aa7f7a68a 100644
--- a/include/upgrader/done.inc.php
+++ b/include/upgrader/done.inc.php
@@ -9,12 +9,17 @@ $_SESSION['ost_upgrader']=null;
         <div id="intro">
         <p><?php echo __('Congratulations! osTicket upgrade has been completed successfully.');?></p>
         <p><?php echo sprintf(__('Please refer to %s for more information about changes and/or new features.'),
-            sprintf('<a href="https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes" target="_blank">%s</a>',
-            __('Release Notes')
+            sprintf('<a href="%s" target="_blank">%s</a>',
+                'https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes',
+                __('Release Notes')
         ));?></p>
         </div>
         <p><?php echo __('Once again, thank you for choosing osTicket.');?></p>
-        <p><?php echo sprintf(__('Please feel free to %1$s let us know %2$s of any other improvements and features you would like to see in osTicket, so that we may add them in the future as we continue to develop better and better versions of osTicket.'), '<a target="_blank" href="http://osticket.com/support/">', '</a>');?></p>
+        <p><?php echo sprintf(__('Please feel free to %1$s let us know %2$s
+                    of any other improvements and features you would like to
+                    see in osTicket, so that we may add them in the future
+                    as we continue to develop better and better versions of
+                    osTicket.'), '<a target="_blank" href="https://osticket.com/support/">', '</a>');?></p>
         <p><?php echo __("We take user feedback seriously and we're dedicated to making changes based on your input.");?></p>
         <p><?php echo __('Good luck.');?><p>
         <p><?php echo __('osTicket Team.');?></p>
@@ -26,10 +31,15 @@ $_SESSION['ost_upgrader']=null;
             <p><b><?php echo __('Post-upgrade');?></b>: <?php
             echo sprintf(__('You can now go to %s to enable the system and explore the new features. For complete and up-to-date release notes see the %s'),
                 sprintf('<a href="'. ROOT_PATH . 'scp/settings.php" target="_blank">%s</a>', __('Admin Panel')),
-                sprintf('<a href="https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes" target="_blank">%s</a>', __('osTicket Docs')));?></p>
+                sprintf('<a href="%s" target="_blank">%s</a>',
+                    'https://docs.osticket.com/en/latest/Developer%20Documentation/Changelog.html?highlight=notes',
+                    __('osTicket Docs')));?></p>
             <p><b><?php echo __('Stay up to date');?></b>: <?php echo __("It's important to keep your osTicket installation up to date. Get announcements, security updates and alerts delivered directly to you!");?>
-            <?php echo sprintf(__('%1$s Get in the loop %2$s today and stay informed!'), '<a target="_blank" href="http://osticket.com/newsletter">', '</a>');?></p>
-            <p><b><?php echo __('Commercial Support Available');?></b>: <?php echo sprintf(__('Get guidance and hands-on expertise to address unique challenges and make sure your osTicket runs smoothly, efficiently, and securely. %1$s Learn More! %2$s'), '<a target="_blank" href="http://osticket.com/support">','</a>');?></p>
+            <?php echo sprintf(__('%1$s Get in the loop %2$s today and stay
+                        informed!'), '<a target="_blank" href="https://osticket.com/">', '</a>');?></p>
+            <p><b><?php echo __('Commercial Support Available');?></b>:
+            <?php echo sprintf(__('Get guidance and hands-on expertise to address unique challenges and make sure your osTicket runs smoothly, efficiently, and securely.  %1$s Learn More! %2$s'),
+                    '<a target="_blank" href="https://osticket.com/">','</a>');?></p>
    </div>
    <div class="clear"></div>
 </div>
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..01895a67dc5f85e7cbb0df73d05a0f2599dba64a
--- /dev/null
+++ b/include/upgrader/streams/core/26fd79dc-00c949a6.task.php
@@ -0,0 +1,134 @@
+<?php
+
+class EventEnumRemoval extends MigrationTask {
+    var $description = "Remove the Enum 'state' field from ThreadEvents";
+    var $queue;
+    var $skipList;
+    var $errorList = array();
+    var $limit = 20000;
+
+    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, $this->limit);
+    }
+
+    function run($max_time) {
+        $this->do_batch($max_time * 0.9, $this->limit);
+    }
+
+    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 COUNT(t.id) FROM ".THREAD_EVENT_TABLE. " t
+            INNER JOIN ".EVENT_TABLE. " e ON (e.name=t.state)
+            WHERE t.event_id IS NULL";
+
+        //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!');
+
+        $count = db_result($res);
+
+        // Force the log message to the database
+        $ost->logDebug("Thread Event Migration", 'Found '.$count
+            .' events to migrate', true);
+
+        if($count == 0)
+            return 0;  //Nothing else to do!!
+
+        $start = db_result(db_query("SELECT id FROM ".THREAD_EVENT_TABLE. "
+            WHERE event_id IS NULL
+            ORDER BY id ASC LIMIT 1"));
+
+        $this->queue = array();
+        $info=array(
+            'count'        => $count,
+            'start'        => $start,
+            'end'          => $start + $limit
+        );
+        $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);
+
+        $sql = "UPDATE ".THREAD_EVENT_TABLE. " t
+            INNER JOIN ".EVENT_TABLE. " e ON (e.name=t.state)
+            SET t.event_id = e.id
+            WHERE t.event_id IS NULL AND t.id <= ". $info['end'];
+
+        db_query($sql);
+
+        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/js/osticket.js b/js/osticket.js
index 4e0d512de80aaa87a35deb5a0165b4da86ce3b82..9584b92f8352666a4a57b2247c0a33221fad4b25 100644
--- a/js/osticket.js
+++ b/js/osticket.js
@@ -47,7 +47,15 @@ $(document).ready(function(){
         $(window).unbind('beforeunload');
         // Disable client-side Post Reply/Create Ticket buttons to help
         // prevent duplicate POST
-        $(':submit', $(this)).attr('disabled', true);
+        var form = $(this);
+        $(this).find('input[type="submit"]').each(function (index) {
+            // Clone original input
+            $(this).clone(false).removeAttr('id').prop('disabled', true).insertBefore($(this));
+
+            // Hide original input and add it to top of form
+            $(this).hide();
+            form.prepend($(this));
+        });
         $('#overlay, #loading').show();
         return true;
        });
diff --git a/scp/ajax.php b/scp/ajax.php
index dfdb7499e7ae093c1a55e20f8937772d6447002e..304fab05b0956ca0c0169d502a3c8d44faa90b6d 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -168,6 +168,7 @@ $dispatcher = patterns('',
         url('^(?P<tid>\d+)/field/(?P<field>\w+)/edit$', 'editField'),
         url('^(?P<tid>\d+)/assign(?:/(?P<to>\w+))?$', 'assign'),
         url('^(?P<tid>\d+)/release$', 'release'),
+        url('^(?P<tid>\d+)/mark/(?P<action>\w+)$', 'markAs'),
         url('^(?P<tid>\d+)/refer(?:/(?P<to>\w+))?$', 'refer'),
         url('^(?P<tid>\d+)/referrals$', 'referrals'),
         url('^(?P<tid>\d+)/claim$', 'claim'),
diff --git a/scp/autocron.php b/scp/autocron.php
index 170ab3a8b421286bdfe1db6a648a13e7b971946d..c19e1520f4a465512ca3cd30e28d3fc26d49aec3 100644
--- a/scp/autocron.php
+++ b/scp/autocron.php
@@ -45,6 +45,11 @@ if ($sec < 180 || !$ost || $ost->isUpgradePending())
 
 require_once(INCLUDE_DIR.'class.cron.php');
 
+// Run tickets count every 3rd run or so... force new count by skipping cached
+// results
+if ((mt_rand(1, 12) % 3) == 0)
+    SavedQueue::counts($thisstaff, false);
+
 // Clear staff obj to avoid false credit internal notes & auto-assignment
 $thisstaff = null;
 
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 878333317fb1d3e17f90f6d87dab46254a3b00f9..f255ae506c045beacf0499e528f31b4b5adee4b1 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -3525,6 +3525,12 @@ table.grid.form caption {
   margin-bottom: 5px;
 }
 
+.grid.form .field > .field-hint-text {
+  font-style: italic;
+  margin: 0 10px 5px 10px;
+  opacity: 0.8;
+}
+
 #basic_search {
   background-color: #f4f4f4;
   margin: -10px 0;
diff --git a/scp/departments.php b/scp/departments.php
index cb07697370fdefc681839ed7a59f81da10ab2daa..a4ca7252ec8c91edf44574daee009d0433567c5c 100644
--- a/scp/departments.php
+++ b/scp/departments.php
@@ -25,9 +25,6 @@ if($_REQUEST['id'] && !($dept=Dept::lookup($_REQUEST['id'])))
                 if(!$dept){
                     $errors['err']=sprintf(__('%s: Unknown or invalid'), __('department'));
                 }elseif($dept->update($_POST,$errors)){
-                    if ($_POST["status"] != __('Active'))
-                      Dept::clearInactiveDept($dept->getId());
-
                     $msg=sprintf(__('Successfully updated %s.'),
                         __('this department'));
                 }elseif(!$errors['err']){
@@ -133,8 +130,6 @@ if($_REQUEST['id'] && !($dept=Dept::lookup($_REQUEST['id'])))
                               FilterAction::setFilterFlag($filter_actions, 'dept', true);
                               if($d->save()) {
                                 $num++;
-                                //set dept_id to default for topics/emails using disabled dept
-                                Dept::clearInactiveDept($d->getId());
                               }
                             }
                             if ($num > 0) {
diff --git a/scp/js/scp.js b/scp/js/scp.js
index c83673cd3ce944df8139dcef8f3561e7f6707b0c..025db554c60208d11e7754b1fe758a86f68478c7 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -166,7 +166,15 @@ var scp_prep = function() {
         $.toggleOverlay(true);
         // Disable staff-side Post Reply/Open buttons to help prevent
         // duplicate POST
-        $(':submit', $(this)).attr('disabled', true);
+        var form = $(this);
+        $(this).find('input[type="submit"]').each(function (index) {
+            // Clone original input
+            $(this).clone(false).removeAttr('id').prop('disabled', true).insertBefore($(this));
+
+            // Hide original input and add it to top of form
+            $(this).hide();
+            form.prepend($(this));
+        });
         $('#overlay, #loading').show();
         return true;
      });
@@ -526,9 +534,10 @@ var scp_prep = function() {
         url: 'ajax.php/queue/counts',
         dataType: 'json',
         success: function(json) {
-          $('li > span.queue-count').each(function(i, e) {
+          $('li span.queue-count').each(function(i, e) {
             var $e = $(e);
             $e.text(json['q' + $e.data('queueId')]);
+            $(e).parents().find('#queue-count-bucket').show();
           });
         }
       });
@@ -826,8 +835,9 @@ $.confirm = function(message, title, options) {
 };
 
 $.userLookup = function (url, cb) {
-    $.dialog(url, 201, function (xhr) {
-        var user = $.parseJSON(xhr.responseText);
+    $.dialog(url, 201, function (xhr, user) {
+        if ($.type(user) == 'string')
+            user = $.parseJSON(user);
         if (cb) return cb(user);
     }, {
         onshow: function() { $('#user-search').focus(); }
@@ -835,8 +845,9 @@ $.userLookup = function (url, cb) {
 };
 
 $.orgLookup = function (url, cb) {
-    $.dialog(url, 201, function (xhr) {
-        var org = $.parseJSON(xhr.responseText);
+    $.dialog(url, 201, function (xhr, org) {
+        if ($.type(org) == 'string')
+            org = $.parseJSON(user);
         if (cb) cb(org);
     }, {
         onshow: function() { $('#org-search').focus(); }
@@ -1120,7 +1131,7 @@ if ($.support.pjax) {
     if (!$this.hasClass('no-pjax')
         && !$this.closest('.no-pjax').length
         && $this.attr('href').charAt(0) != '#')
-      $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 2000});
+      $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 30000});
   })
 }
 
diff --git a/scp/queues.php b/scp/queues.php
index 997fefa2678cae361a9654c6efecfaccf0199fe9..37fc13e3e2808fd252118ee10d20a12e0e983120 100644
--- a/scp/queues.php
+++ b/scp/queues.php
@@ -72,11 +72,13 @@ if ($_POST) {
                 if ($queue->save()) $updated++;
                 break;
             case 'delete':
-                if ($queue->delete()) $updated++;
+                if ($queue->getId() == $cfg->getDefaultTicketQueueId())
+                    $err = __('This queue is the default queue. Unable to delete. ');
+                elseif ($queue->delete()) $updated++;
             }
         }
         if (!$updated) {
-            Messages::error(__(
+            Messages::error($err ?: __(
                 'Unable to manage any of the selected queues'));
         }
         elseif ($_POST['count'] && $updated != $_POST['count']) {
diff --git a/scp/tickets.php b/scp/tickets.php
index d100f38d7b1af8e1505301922754e5925bc264dc..8881874e897217bbea32e02ba8735e482ee937ab 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -23,6 +23,11 @@ require_once(INCLUDE_DIR.'class.json.php');
 require_once(INCLUDE_DIR.'class.dynamic_forms.php');
 require_once(INCLUDE_DIR.'class.export.php');       // For paper sizes
 
+
+
+// Fetch ticket queues organized by root and sub-queues
+$queues = CustomQueue::getHierarchicalQueues($thisstaff);
+
 $page='';
 $ticket = $user = null; //clean start.
 $redirect = false;
@@ -79,16 +84,20 @@ if (!$ticket) {
     elseif (isset($_GET['a']) && $_GET['a'] === 'search'
         && ($_GET['query'])
     ) {
-        $key = substr(md5($_GET['query']), -10);
-        if ($_GET['search-type'] == 'typeahead') {
-            // Use a faster index
-            $criteria = ['user__emails__address', 'equal', $_GET['query']];
-        }
-        else {
-            $criteria = [':keywords', null, $_GET['query']];
+        $wc = mb_str_wc($_GET['query']);
+        if ($wc < 4) {
+            $key = substr(md5($_GET['query']), -10);
+            if ($_GET['search-type'] == 'typeahead') {
+                // Use a faster index
+                $criteria = ['user__emails__address', 'equal', $_GET['query']];
+            } else {
+                $criteria = [':keywords', null, $_GET['query']];
+            }
+            $_SESSION['advsearch'][$key] = [$criteria];
+            $queue_id = "adhoc,{$key}";
+        } else {
+            $errors['err'] = __('Search term cannot have more than 3 keywords');
         }
-        $_SESSION['advsearch'][$key] = [$criteria];
-        $queue_id = "adhoc,{$key}";
     }
 
     $queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TICKET);
@@ -109,18 +118,21 @@ if (!$ticket) {
         $queue = AdhocSearch::load($key);
     }
 
-    // Make the current queue sticky
-    $_SESSION[$queue_key] = $queue_id;
-
-    if ((int) $queue_id && !$queue) {
+    if ((int) $queue_id && !$queue)
         $queue = SavedQueue::lookup($queue_id);
-    }
-    if (!$queue) {
-        $queue = SavedQueue::lookup($cfg->getDefaultTicketQueueId());
-    }
 
-    // Set the queue_id for navigation to turn a top-level item bold
-    $_REQUEST['queue'] = $queue->getId();
+    if (!$queue && ($qid=$cfg->getDefaultTicketQueueId()))
+        $queue = SavedQueue::lookup($qid);
+
+    if (!$queue && $queues)
+        list($queue,) = $queues[0];
+
+    if ($queue) {
+        // Set the queue_id for navigation to turn a top-level item bold
+        $_REQUEST['queue'] = $queue->getId();
+        // Make the current queue sticky
+         $_SESSION[$queue_key] = $queue->getId();
+    }
 }
 
 // Configure form for file uploads
@@ -143,6 +155,8 @@ if($_POST && !$errors):
         $errors=array();
         $lock = $ticket->getLock(); //Ticket lock if any
         $role = $ticket->getRole($thisstaff);
+        $dept = $ticket->getDept();
+        $isManager = $dept->isManager($thisstaff); //Check if Agent is Manager
         switch(strtolower($_POST['a'])):
         case 'reply':
             if (!$role || !$role->hasPerm(Ticket::PERM_REPLY)) {
@@ -298,8 +312,7 @@ if($_POST && !$errors):
                     }
                     break;
                 case 'overdue':
-                    $dept = $ticket->getDept();
-                    if(!$dept || !$dept->isManager($thisstaff)) {
+                    if(!$dept || !$isManager) {
                         $errors['err']=__('Permission Denied. You are not allowed to flag tickets overdue');
                     } elseif($ticket->markOverdue()) {
                         $msg=sprintf(__('Ticket flagged as overdue by %s'),$thisstaff->getName());
@@ -308,28 +321,6 @@ if($_POST && !$errors):
                         $errors['err']=sprintf('%s %s', __('Problems marking the the ticket overdue.'), __('Please try again!'));
                     }
                     break;
-                case 'answered':
-                    $dept = $ticket->getDept();
-                    if(!$dept || !$dept->isManager($thisstaff)) {
-                        $errors['err']=__('Permission Denied. You are not allowed to flag tickets');
-                    } elseif($ticket->markAnswered()) {
-                        $msg=sprintf(__('Ticket flagged as answered by %s'),$thisstaff->getName());
-                        $ticket->logActivity(__('Ticket Marked Answered'),$msg);
-                    } else {
-                        $errors['err']=sprintf('%s %s', __('Problems marking the ticket answered.'), __('Please try again!'));
-                    }
-                    break;
-                case 'unanswered':
-                    $dept = $ticket->getDept();
-                    if(!$dept || !$dept->isManager($thisstaff)) {
-                        $errors['err']=__('Permission Denied. You are not allowed to flag tickets');
-                    } elseif($ticket->markUnAnswered()) {
-                        $msg=sprintf(__('Ticket flagged as unanswered by %s'),$thisstaff->getName());
-                        $ticket->logActivity(__('Ticket Marked Unanswered'),$msg);
-                    } else {
-                        $errors['err']=sprintf('%s %s', __('Problems marking the ticket unanswered.'), __('Please try again!'));
-                    }
-                    break;
                 case 'banemail':
                     if (!$thisstaff->hasPerm(Email::PERM_BANLIST)) {
                         $errors['err']=__('Permission Denied. You are not allowed to ban emails');
@@ -446,9 +437,6 @@ if (isset($_GET['clear_filter']))
 $nav->setTabActive('tickets');
 $nav->addSubNavInfo('jb-overflowmenu', 'customQ_nav');
 
-// Fetch ticket queues organized by root and sub-queues
-$queues = CustomQueue::getHierarchicalQueues($thisstaff);
-
 // Start with all the top-level (container) queues
 foreach ($queues as $_) {
     list($q, $children) = $_;
@@ -462,7 +450,7 @@ foreach ($queues as $_) {
                 || false !== strpos($queue->getPath(), "/{$q->getId()}/"));
         include STAFFINC_DIR . 'templates/queue-navigation.tmpl.php';
 
-        return ($child_selected || $selected);
+        return $child_selected;
     });
 }
 
@@ -473,10 +461,7 @@ $nav->addSubMenu(function() use ($queue) {
     // A queue is selected if it is the one being displayed. It is
     // "child" selected if its ID is in the path of the one selected
     $child_selected = $queue instanceof SavedSearch;
-    $searches = SavedSearch::forStaff($thisstaff)->getIterator();
-
     include STAFFINC_DIR . 'templates/queue-savedsearches-nav.tmpl.php';
-
     return ($child_selected || $selected);
 });
 
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`;
diff --git a/tickets.php b/tickets.php
index 560b16f3dc963008e05aa8c610607b82992fee95..93c7da3964d2460a8aadc2045b158c82cc959190 100644
--- a/tickets.php
+++ b/tickets.php
@@ -53,14 +53,17 @@ if ($_POST && is_object($ticket) && $ticket->getId()) {
             foreach ($forms as $form) {
                 $form->filterFields(function($f) { return !$f->isStorable(); });
                 $form->setSource($_POST);
-                if (!$form->isValid())
+                if (!$form->isValidForClient(true))
                     $errors = array_merge($errors, $form->errors());
             }
         }
         if (!$errors) {
-            foreach ($forms as $f) {
-                $changes += $f->getChanges();
-                $f->save();
+            foreach ($forms as $form) {
+                $changes += $form->getChanges();
+                $form->saveAnswers(function ($f) {
+                        return $f->isVisibleToUsers()
+                         && $f->isEditableToUsers(); });
+
             }
             if ($changes) {
               $user = User::lookup($thisclient->getId());
@@ -127,9 +130,9 @@ if($ticket && $ticket->checkUserAccess($thisclient)) {
         $inc = 'edit.inc.php';
         if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId());
         // Auto add new fields to the entries
-        foreach ($forms as $f) {
-            $f->filterFields(function($f) { return !$f->isStorable(); });
-            $f->addMissingFields();
+        foreach ($forms as $form) {
+            $form->filterFields(function($f) { return !$f->isStorable(); });
+            $form->addMissingFields();
         }
     }
     else