diff --git a/css/chosen-sprite.png b/css/chosen-sprite.png
new file mode 100644
index 0000000000000000000000000000000000000000..c57da70b4b5b1e08a6977ddde182677af0e5e1b8
Binary files /dev/null and b/css/chosen-sprite.png differ
diff --git a/css/chosen-sprite@2x.png b/css/chosen-sprite@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b50545202cb4770039362c55025b0b9824663ad
Binary files /dev/null and b/css/chosen-sprite@2x.png differ
diff --git a/css/chosen.min.css b/css/chosen.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..8bfa3c8b0bc07a7448e0659ebcc4f655c26e7a1f
--- /dev/null
+++ b/css/chosen.min.css
@@ -0,0 +1,3 @@
+/* Chosen v1.2.0 | (c) 2011-2014 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */
+
+.chosen-container{position:relative;display:inline-block;vertical-align:middle;font-size:13px;zoom:1;*display:inline;-webkit-user-select:none;-moz-user-select:none;user-select:none}.chosen-container *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.chosen-container .chosen-drop{position:absolute;top:100%;left:-9999px;z-index:1010;width:100%;border:1px solid #aaa;border-top:0;background:#fff;box-shadow:0 4px 5px rgba(0,0,0,.15)}.chosen-container.chosen-with-drop .chosen-drop{left:0}.chosen-container a{cursor:pointer}.chosen-container a:hover{text-decoration:none} .chosen-container-single .chosen-single{position:relative;display:block;overflow:hidden;padding:0 0 0 8px;height:25px;border:1px solid #aaa;border-radius:5px;background-color:#fff;background:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#fff),color-stop(50%,#f6f6f6),color-stop(52%,#eee),color-stop(100%,#f4f4f4));background:-webkit-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-moz-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:-o-linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background:linear-gradient(top,#fff 20%,#f6f6f6 50%,#eee 52%,#f4f4f4 100%);background-clip:padding-box;box-shadow:0 0 3px #fff inset,0 1px 1px rgba(0,0,0,.1);color:#444;text-decoration:none;white-space:nowrap;line-height:24px}.chosen-container-single .chosen-default{color:#999}.chosen-container-single .chosen-single span{display:block;overflow:hidden;margin-right:26px;text-overflow:ellipsis;white-space:nowrap}.chosen-container-single .chosen-single-with-deselect span{margin-right:38px}.chosen-container-single .chosen-single abbr{position:absolute;top:6px;right:26px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-single .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single.chosen-disabled .chosen-single abbr:hover{background-position:-42px -10px}.chosen-container-single .chosen-single div{position:absolute;top:0;right:0;display:block;width:18px;height:100%}.chosen-container-single .chosen-single div b{display:block;width:100%;height:100%;background:url(chosen-sprite.png) no-repeat 0 2px}.chosen-container-single .chosen-search{position:relative;z-index:1010;margin:0;padding:3px 4px;white-space:nowrap}.chosen-container-single .chosen-search input[type=text]{margin:1px 0;padding:4px 20px 4px 5px;width:100%;height:auto;outline:0;border:1px solid #aaa;background:#fff url(chosen-sprite.png) no-repeat 100% -20px;background:url(chosen-sprite.png) no-repeat 100% -20px;font-size:1em;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-single .chosen-drop{margin-top:-1px;border-radius:0 0 4px 4px;background-clip:padding-box}.chosen-container-single.chosen-container-single-nosearch .chosen-search{position:absolute;left:-9999px}.chosen-container .chosen-results{color:#444;position:relative;overflow-x:hidden;overflow-y:auto;margin:0 4px 4px 0;padding:0 0 0 4px;max-height:240px;-webkit-overflow-scrolling:touch}.chosen-container .chosen-results li{display:none;margin:0;padding:5px 6px;list-style:none;line-height:15px;word-wrap:break-word;-webkit-touch-callout:none}.chosen-container .chosen-results li.active-result{display:list-item;cursor:pointer}.chosen-container .chosen-results li.disabled-result{display:list-item;color:#ccc;cursor:default}.chosen-container .chosen-results li.highlighted{background-color:#3875d7;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#3875d7),color-stop(90%,#2a62bc));background-image:-webkit-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-moz-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:-o-linear-gradient(#3875d7 20%,#2a62bc 90%);background-image:linear-gradient(#3875d7 20%,#2a62bc 90%);color:#fff}.chosen-container .chosen-results li.no-results{color:#777;display:list-item;background:#f4f4f4}.chosen-container .chosen-results li.group-result{display:list-item;font-weight:700;cursor:default}.chosen-container .chosen-results li.group-option{padding-left:15px}.chosen-container .chosen-results li em{font-style:normal;text-decoration:underline}.chosen-container-multi .chosen-choices{position:relative;overflow:hidden;margin:0;padding:0 5px;width:100%;height:auto!important;height:1%;border:1px solid #aaa;background-color:#fff;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(1%,#eee),color-stop(15%,#fff));background-image:-webkit-linear-gradient(#eee 1%,#fff 15%);background-image:-moz-linear-gradient(#eee 1%,#fff 15%);background-image:-o-linear-gradient(#eee 1%,#fff 15%);background-image:linear-gradient(#eee 1%,#fff 15%);cursor:text}.chosen-container-multi .chosen-choices li{float:left;list-style:none}.chosen-container-multi .chosen-choices li.search-field{margin:0;padding:0;white-space:nowrap}.chosen-container-multi .chosen-choices li.search-field input[type=text]{margin:1px 0;padding:0;height:25px;outline:0;border:0!important;background:transparent!important;box-shadow:none;color:#999;font-size:100%;font-family:sans-serif;line-height:normal;border-radius:0}.chosen-container-multi .chosen-choices li.search-choice{position:relative;margin:3px 5px 3px 0;padding:3px 20px 3px 5px;border:1px solid #aaa;max-width:100%;border-radius:3px;background-color:#eee;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-size:100% 19px;background-repeat:repeat-x;background-clip:padding-box;box-shadow:0 0 2px #fff inset,0 1px 0 rgba(0,0,0,.05);color:#333;line-height:13px;cursor:default}.chosen-container-multi .chosen-choices li.search-choice span{word-wrap:break-word}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close{position:absolute;top:4px;right:3px;display:block;width:12px;height:12px;background:url(chosen-sprite.png) -42px 1px no-repeat;font-size:1px}.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover{background-position:-42px -10px}.chosen-container-multi .chosen-choices li.search-choice-disabled{padding-right:5px;border:1px solid #ccc;background-color:#e4e4e4;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#f4f4f4),color-stop(50%,#f0f0f0),color-stop(52%,#e8e8e8),color-stop(100%,#eee));background-image:-webkit-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-moz-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:-o-linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);background-image:linear-gradient(top,#f4f4f4 20%,#f0f0f0 50%,#e8e8e8 52%,#eee 100%);color:#666}.chosen-container-multi .chosen-choices li.search-choice-focus{background:#d4d4d4}.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close{background-position:-42px -10px}.chosen-container-multi .chosen-results{margin:0;padding:0}.chosen-container-multi .chosen-drop .result-selected{display:list-item;color:#ccc;cursor:default}.chosen-container-active .chosen-single{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active.chosen-with-drop .chosen-single{border:1px solid #aaa;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(20%,#eee),color-stop(80%,#fff));background-image:-webkit-linear-gradient(#eee 20%,#fff 80%);background-image:-moz-linear-gradient(#eee 20%,#fff 80%);background-image:-o-linear-gradient(#eee 20%,#fff 80%);background-image:linear-gradient(#eee 20%,#fff 80%);box-shadow:0 1px 0 #fff inset}.chosen-container-active.chosen-with-drop .chosen-single div{border-left:0;background:transparent}.chosen-container-active.chosen-with-drop .chosen-single div b{background-position:-18px 2px}.chosen-container-active .chosen-choices{border:1px solid #5897fb;box-shadow:0 0 5px rgba(0,0,0,.3)}.chosen-container-active .chosen-choices li.search-field input[type=text]{color:#222!important}.chosen-disabled{opacity:.5!important;cursor:default}.chosen-disabled .chosen-single{cursor:default}.chosen-disabled .chosen-choices .search-choice .search-choice-close{cursor:default}.chosen-rtl{text-align:right}.chosen-rtl .chosen-single{overflow:visible;padding:0 8px 0 0}.chosen-rtl .chosen-single span{margin-right:0;margin-left:26px;direction:rtl}.chosen-rtl .chosen-single-with-deselect span{margin-left:38px}.chosen-rtl .chosen-single div{right:auto;left:3px}.chosen-rtl .chosen-single abbr{right:auto;left:26px}.chosen-rtl .chosen-choices li{float:right}.chosen-rtl .chosen-choices li.search-field input[type=text]{direction:rtl}.chosen-rtl .chosen-choices li.search-choice{margin:3px 5px 3px 0;padding:3px 5px 3px 19px}.chosen-rtl .chosen-choices li.search-choice .search-choice-close{right:auto;left:4px}.chosen-rtl.chosen-container-single-nosearch .chosen-search,.chosen-rtl .chosen-drop{left:9999px}.chosen-rtl.chosen-container-single .chosen-results{margin:0 0 4px 4px;padding:0 4px 0 0}.chosen-rtl .chosen-results li.group-option{padding-right:15px;padding-left:0}.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div{border-right:0}.chosen-rtl .chosen-search input[type=text]{padding:4px 5px 4px 20px;background:#fff url(chosen-sprite.png) no-repeat -30px -20px;background:url(chosen-sprite.png) no-repeat -30px -20px;direction:rtl}.chosen-rtl.chosen-container-single .chosen-single div b{background-position:6px 2px}.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b{background-position:-12px 2px}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:144dpi){.chosen-rtl .chosen-search input[type=text],.chosen-container-single .chosen-single abbr,.chosen-container-single .chosen-single div b,.chosen-container-single .chosen-search input[type=text],.chosen-container-multi .chosen-choices .search-choice .search-choice-close,.chosen-container .chosen-results-scroll-down span,.chosen-container .chosen-results-scroll-up span{background-image:url(chosen-sprite@2x.png)!important;background-size:52px 37px!important;background-repeat:no-repeat!important}}
diff --git a/css/jquery.multiselect.css b/css/jquery.multiselect.css
deleted file mode 100644
index 8a08e22b75ba758290971dbadb1a903bddca534e..0000000000000000000000000000000000000000
--- a/css/jquery.multiselect.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.ui-multiselect { padding:2px 0 2px 4px; text-align:left }
-.ui-multiselect span.ui-icon { float:right }
-.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; }
-.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important }
-
-.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px }
-.ui-multiselect-header ul { font-size:0.9em }
-.ui-multiselect-header ul li { float:left; padding:0 10px 0 0 }
-.ui-multiselect-header a { text-decoration:none }
-.ui-multiselect-header a:hover { text-decoration:underline }
-.ui-multiselect-header span.ui-icon { float:left }
-.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 }
-
-.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; text-align: left }
-.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:auto }
-.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px }
-.ui-multiselect-checkboxes label input { position:relative; top:1px }
-.ui-multiselect-checkboxes li { clear:both; font-size:0.9em; padding-right:3px }
-.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid }
-.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none }
-
-/* remove label borders in IE6 because IE6 does not support transparency */
-* html .ui-multiselect-checkboxes label { border:none }
diff --git a/css/jquery.multiselect.filter.css b/css/jquery.multiselect.filter.css
deleted file mode 100644
index f8c323a7d2a49bcaab788fb81756fb1b5d4ab375..0000000000000000000000000000000000000000
--- a/css/jquery.multiselect.filter.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.ui-multiselect-hasfilter ul { position:relative; top:2px }
-.ui-multiselect-filter { float:left; margin-right:10px; font-size:11px }
-.ui-multiselect-filter input { width:100px; font-size:10px; margin-left:5px; height:15px; padding:2px; border:1px solid #292929; -webkit-appearance:textfield; -webkit-box-sizing:content-box; }
diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php
index 393c6dde48ba1d0a797bf34447d12ce28c526962..b8309a55edae5ecfadf028e155b7ca45bc27bac4 100644
--- a/include/ajax.orgs.php
+++ b/include/ajax.orgs.php
@@ -15,6 +15,7 @@
 
 if(!defined('INCLUDE_DIR')) die('403');
 
+require_once INCLUDE_DIR . 'class.organization.php';
 include_once(INCLUDE_DIR.'class.ticket.php');
 
 class OrgsAjaxAPI extends AjaxController {
diff --git a/include/ajax.search.php b/include/ajax.search.php
new file mode 100644
index 0000000000000000000000000000000000000000..d52d7ca4784db657856df3e351f1fc1967679522
--- /dev/null
+++ b/include/ajax.search.php
@@ -0,0 +1,233 @@
+<?php
+/*********************************************************************
+    ajax.search.php
+
+    AJAX interface for searches, queue management, etc.
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2014 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+if(!defined('INCLUDE_DIR')) die('403');
+
+include_once(INCLUDE_DIR.'class.ticket.php');
+require_once(INCLUDE_DIR.'class.ajax.php');
+
+class SearchAjaxAPI extends AjaxController {
+
+    static function ensureConsistentFormFieldIds($reset=false) {
+        // Maintain unique form field IDs over the life of the session
+        FormField::$uid = $reset ?: 1000;
+    }
+
+    function getAdvancedSearchDialog() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+
+        self::ensureConsistentFormFieldIds();
+        $search = SavedSearch::create();
+        // Don't send the state as the souce because it is not in the
+        // ::parse format (it's in ::to_php format)
+        $form = $search->getFormFromSession('advsearch');
+        $form->loadState($_SESSION['advsearch']);
+        $matches = self::_getSupportedTicketMatches();
+
+        include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
+    }
+
+    function addField($name) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+
+        list($type, $id) = explode('!', $name, 2);
+
+        switch (strtolower($type)) {
+        case ':ticket':
+        case ':user':
+        case ':organization':
+        case ':field':
+            // Support nested field ids for list properties and such
+            if (strpos($id, '.') !== false)
+                list(,$id) = explode('!', $id, 2);
+            if (!($field = DynamicFormField::lookup($id)))
+                Http::response(404, 'No such field: ', print_r($id, true));
+            break;
+        default:
+            Http::response(400, 'No such field type');
+        }
+
+        self::ensureConsistentFormFieldIds($_GET['ff_uid']);
+
+        $impl = $field->getImpl();
+        $impl->set('label', sprintf('%s / %s',
+            $field->form->getLocal('title'), $field->getLocal('label')
+        ));
+        $fields = SavedSearch::getSearchField($impl, $name);
+        $form = new Form($fields);
+        // Check the box to search the field by default
+        if ($F = $form->getField("{$name}+search"))
+            $F->value = true;
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/advanced-search-field.tmpl.php';
+        $html = ob_get_clean();
+
+        return $this->encode(array(
+            'success' => true,
+            'html' => $html,
+            // Send the current formfield UID to be resent with the next
+            // addField request and set above
+            'ff_uid' => FormField::$uid,
+        ));
+    }
+
+    function doSearch() {
+        global $thisstaff;
+
+        $search = SavedSearch::create();
+        self::ensureConsistentFormFieldIds();
+
+        $form = $search->getForm($_POST);
+        if (!$form->isValid()) {
+            $matches = self::_getSupportedTicketMatches();
+            include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
+            return;
+        }
+        $_SESSION['advsearch'] = $form->getState();
+
+        Http::response(200, $this->encode(array(
+            'redirect' => 'tickets.php?advanced',
+        )));
+    }
+
+    function saveSearch($id) {
+        global $thisstaff;
+
+        $search = SavedSearch::lookup($id);
+        if (!$search || !$search->checkAccess($thisstaff))
+            Http::response(404, 'No such saved search');
+        elseif (!$thisstaff)
+            Http::response(403, 'Agent login is required');
+
+        return self::_saveSearch($search);
+    }
+
+    function _saveSearch($search) {
+        $data = array();
+        foreach ($_POST['form'] as $id=>$info) {
+            $name = $info['name'];
+            if (substr($name, -2) == '[]')
+                $data[substr($name, 0, -2)][] = $info['value'];
+            else
+                $data[$name] = $info['value'];
+        }
+        self::ensureConsistentFormFieldIds();
+        $form = $search->getForm($data);
+        if (!$data || !$form->isValid()) {
+            Http::response(422, 'Validation errors exist on form');
+        }
+
+        $search->config = JsonDataEncoder::encode($form->getState());
+        if (isset($_POST['name']))
+            $search->title = $_POST['name'];
+        if (!$search->save()) {
+            Http::response(500, 'Internal error. Unable to update search');
+        }
+        Http::response(201, $this->encode(array(
+            'id' => $search->id,
+            'title' => $search->title,
+        )));
+    }
+
+    function _getSupportedTicketMatches() {
+        // User information
+        $matches = array();
+        foreach (array('ticket'=>'TicketForm', 'user'=>'UserForm', 'organization'=>'OrganizationForm') as $k=>$F) {
+            $form = $F::objects()->one();
+            $fields = &$matches[$form->getLocal('title')];
+            foreach ($form->getFields() as $f) {
+                if (!$f->hasData() || $f->isPresentationOnly())
+                    continue;
+                $fields[":$k!".$f->get('id')] = __(ucfirst($k)).' / '.$f->getLocal('label');
+                /* TODO: Support matches on list item properties
+                if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
+                    foreach ($fi->getSubFields() as $p) {
+                        $fields[":$k.".$f->get('id').'.'.$p->get('id')]
+                            = __(ucfirst($k)).' / '.$f->getLocal('label').' / '.$p->getLocal('label');
+                    }
+                }
+                */
+            }
+        }
+        $fields = &$matches[__('Custom Forms')];
+        foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $form) {
+            foreach ($form->getFields() as $f) {
+                if (!$f->hasData() || $f->isPresentationOnly())
+                    continue;
+                $key = sprintf(':field!%d', $f->get('id'), $f->get('id'));
+                $fields[$key] = $form->getLocal('title').' / '.$f->getLocal('label');
+            }
+        }
+        return $matches;
+    }
+
+    function createSearch() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login is required');
+
+        $search = SavedSearch::create();
+        $search->staff_id = $thisstaff->getId();
+        return self::_saveSearch($search);
+    }
+
+    function loadSearch($id) {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!($search = SavedSearch::lookup($id))) {
+            Http::response(404, 'No such saved search');
+        }
+
+        self::ensureConsistentFormFieldIds();
+        $form = $search->getForm();
+        if ($state = JsonDataParser::parse($search->config))
+            $form->loadState($state);
+
+        $matches = self::_getSupportedTicketMatches();
+        include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
+    }
+
+    function deleteSearch($id) {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!($search = SavedSearch::lookup($id))) {
+            Http::response(404, 'No such saved search');
+        }
+        elseif (!$search->delete()) {
+            Http::response(500, 'Unable to delete search');
+        }
+
+        Http::response(200, $this->encode(array(
+            'id' => $search->id,
+            'success' => true,
+        )));
+    }
+}
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 708bb6dbdc88b3f6adadba7205cc60efb5da8b4c..d5d12bea93f519e3b443038fbcdda61f35615566 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -97,184 +97,6 @@ class TicketsAjaxAPI extends AjaxController {
         return $this->json_encode($tickets);
     }
 
-    function _search($req) {
-        global $thisstaff, $cfg, $ost;
-
-        $result=array();
-        $criteria = array();
-
-        $select = 'SELECT ticket.ticket_id';
-        $from = ' FROM '.TICKET_TABLE.' ticket
-                  LEFT JOIN '.TICKET_STATUS_TABLE.' status
-                    ON (status.id = ticket.status_id) ';
-        //Access control.
-        $where = ' WHERE ( (ticket.staff_id='.db_input($thisstaff->getId())
-                    .' AND status.state="open" )';
-
-        if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-            $where.=' OR (ticket.team_id IN ('.implode(',', db_input(array_filter($teams)))
-                   .' ) AND status.state="open" )';
-
-        if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-            $where.=' OR ticket.dept_id IN ('.implode(',', db_input($depts)).')';
-
-        $where.=' ) ';
-
-        //Department
-        if ($req['deptId']) {
-            $where.=' AND ticket.dept_id='.db_input($req['deptId']);
-            $criteria['dept_id'] = $req['deptId'];
-        }
-
-        //Help topic
-        if($req['topicId']) {
-            $where.=' AND ticket.topic_id='.db_input($req['topicId']);
-            $criteria['topic_id'] = $req['topicId'];
-        }
-
-        // Status
-        if ($req['statusId']
-                && ($status=TicketStatus::lookup($req['statusId']))) {
-            $where .= sprintf(' AND status.id="%d" ',
-                    $status->getId());
-            $criteria['status_id'] = $status->getId();
-        }
-
-        // Flags
-        if ($req['flag']) {
-            switch (strtolower($req['flag'])) {
-                case 'answered':
-                    $where .= ' AND ticket.isanswered =1 ';
-                    $criteria['isanswered'] = 1;
-                    $criteria['state'] = 'open';
-                    $where .= ' AND status.state="open" ';
-                    break;
-                case 'overdue':
-                    $where .= ' AND ticket.isoverdue =1 ';
-                    $criteria['isoverdue'] = 1;
-                    $criteria['state'] = 'open';
-                    $where .= ' AND status.state="open" ';
-                    break;
-            }
-        }
-
-        //Assignee
-        if($req['assignee'] && strcasecmp($req['status'], 'closed'))  { # assigned-to
-            $id=preg_replace("/[^0-9]/", "", $req['assignee']);
-            $assignee = $req['assignee'];
-            $where.= ' AND ( ( status.state="open" ';
-            if($assignee[0]=='t') {
-                $where.=' AND ticket.team_id='.db_input($id);
-                $criteria['team_id'] = $id;
-            }
-            elseif($assignee[0]=='s' || is_numeric($id)) {
-                $where.=' AND ticket.staff_id='.db_input($id);
-                $criteria['staff_id'] = $id;
-            }
-
-            $where.=')';
-
-            if($req['staffId'] && !$req['status']) //Assigned TO + Closed By
-                $where.= ' OR (ticket.staff_id='.db_input($req['staffId']).
-                    ' AND status.state IN("closed")) ';
-            elseif($req['staffId']) // closed by any
-                $where.= ' OR status.state IN("closed") ';
-
-            $where.= ' ) ';
-        } elseif($req['staffId']) { # closed-by
-            $where.=' AND (ticket.staff_id='.db_input($req['staffId']).' AND
-                status.state IN("closed")) ';
-            $criteria['state__in'] = array('closed');
-            $criteria['staff_id'] = $req['staffId'];
-        }
-
-        //dates
-        $startTime  =($req['startDate'] && (strlen($req['startDate'])>=8))?strtotime($req['startDate']):0;
-        $endTime    =($req['endDate'] && (strlen($req['endDate'])>=8))?strtotime($req['endDate']):0;
-        if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0))
-            $startTime=$endTime=0;
-
-        if($startTime) {
-            $where.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')';
-            $criteria['created__gte'] = $startTime;
-        }
-
-        if($endTime) {
-            $where.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')';
-            $criteria['created__lte'] = $startTime;
-        }
-
-        // Dynamic fields
-        $cdata_search = false;
-        foreach (TicketForm::getInstance()->getFields() as $f) {
-            if (isset($req[$f->getFormName()])
-                    && ($val = $req[$f->getFormName()])) {
-                $name = $f->get('name') ? $f->get('name')
-                    : 'field_'.$f->get('id');
-                if (is_array($val)) {
-                    $cwhere = '(' . implode(' OR ', array_map(
-                        function($k) use ($name) {
-                            return sprintf('FIND_IN_SET(%s, `%s`)', db_input($k), $name);
-                        }, $val)
-                    ) . ')';
-                    $criteria["cdata.{$name}"] = $val;
-                }
-                else {
-                    $cwhere = "cdata.`$name` LIKE '%".db_real_escape($val)."%'";
-                    $criteria["cdata.{$name}"] = $val;
-                }
-                $where .= ' AND ('.$cwhere.')';
-                $cdata_search = true;
-            }
-        }
-        if ($cdata_search)
-            $from .= 'LEFT JOIN '.TABLE_PREFIX.'ticket__cdata '
-                    ." cdata ON (cdata.ticket_id = ticket.ticket_id)";
-
-        //Query
-        $joins = array();
-        if($req['query']) {
-            // Setup sets of joins and queries
-            if ($s = $ost->searcher)
-               return $s->find($req['query'], $criteria, 'Ticket');
-        }
-
-        $sections = array();
-        foreach ($joins as $j) {
-            $sections[] = "$select $from {$j['from']} $where AND ({$j['where']})";
-        }
-        if (!$joins)
-            $sections[] = "$select $from $where";
-
-        $sql=implode(' union ', $sections);
-        if (!($res = db_query($sql)))
-            return TicketForm::dropDynamicDataView();
-
-        $tickets = array();
-        while ($row = db_fetch_row($res))
-            $tickets[] = $row[0];
-
-        return $tickets;
-    }
-
-    function search() {
-        $tickets = self::_search($_REQUEST);
-        $result = array();
-
-        if (count($tickets)) {
-            $uid = md5($_SERVER['QUERY_STRING']);
-            $_SESSION["adv_$uid"] = $tickets;
-            $result['success'] = sprintf(__("Search criteria matched %s"),
-                    sprintf(_N('%d ticket', '%d tickets', count($tickets)), count($tickets)
-                ))
-                . " - <a href='tickets.php?advsid=$uid'>".__('view')."</a>";
-        } else {
-            $result['fail']=__('No tickets found matching your search criteria.');
-        }
-
-        return $this->json_encode($result);
-    }
-
     function acquireLock($tid) {
         global $cfg,$thisstaff;
 
diff --git a/include/class.auth.php b/include/class.auth.php
index 4cbbea0359ea071be9791ad9b8dba85eeb82ee5e..fb055a5c379a706682a48d38b0e5a54e5622ab6d 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -1,9 +1,29 @@
 <?php
-require(INCLUDE_DIR.'class.ostsession.php');
-require(INCLUDE_DIR.'class.usersession.php');
 
+interface AuthenticatedUser {
+    // Get basic information
+    function getId();
+    function getUsername();
+    function getRole();
+
+    //Backend used to authenticate the user
+    function getAuthBackend();
 
-abstract class AuthenticatedUser {
+    //Authentication key
+    function setAuthKey($key);
+
+    function getAuthKey();
+
+    // logOut the user
+    function logOut();
+
+    // Signal method to allow performing extra things when a user is logged
+    // into the sysem
+    function onLogin($bk);
+}
+
+abstract class BaseAuthenticatedUser
+implements AuthenticatedUser {
     //Authorization key returned by the backend used to authorize the user
     private $authkey;
 
@@ -38,6 +58,9 @@ abstract class AuthenticatedUser {
     function onLogin($bk) {}
 }
 
+require_once(INCLUDE_DIR.'class.ostsession.php');
+require_once(INCLUDE_DIR.'class.usersession.php');
+
 interface AuthDirectorySearch {
     /**
      * Indicates if the backend can be used to search for user information.
@@ -509,7 +532,7 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
 
     protected function validate($authkey) {
 
-        if (($staff = new StaffSession($authkey)) && $staff->getId())
+        if (($staff = StaffSession::lookup($authkey)) && $staff->getId())
             return $staff;
     }
 }
@@ -909,7 +932,7 @@ class osTicketAuthentication extends StaffAuthenticationBackend {
     static $id = "local";
 
     function authenticate($username, $password) {
-        if (($user = new StaffSession($username)) && $user->getId() &&
+        if (($user = StaffSession::lookup($username)) && $user->getId() &&
                 $user->check_passwd($password)) {
 
             //update last login && password reset stuff.
@@ -940,7 +963,7 @@ class PasswordResetTokenBackend extends StaffAuthenticationBackend {
             return false;
         elseif (!($_config = new Config('pwreset')))
             return false;
-        elseif (($staff = new StaffSession($_POST['userid'])) &&
+        elseif (($staff = StaffSession::lookup($_POST['userid'])) &&
                 !$staff->getId())
             $errors['msg'] = __('Invalid user-id given');
         elseif (!($id = $_config->get($_POST['token']))
diff --git a/include/class.client.php b/include/class.client.php
index e29f570b77e2c94b5ec8b6fb270165d9eef206a7..a8b39d1e5135bc69e01fccf9ef711bfbf32b613b 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -182,7 +182,7 @@ class TicketOwner extends  TicketUser {
  *
  */
 
-class  EndUser extends AuthenticatedUser {
+class  EndUser extends BaseAuthenticatedUser {
 
     protected $user;
     protected $_account = false;
diff --git a/include/class.dept.php b/include/class.dept.php
index d12216f5611f26a26ac2c22c814b1f6e82cfb029..d6d1fd9248d2aac08dd6bc6f2de3f786c6610dad 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -14,56 +14,40 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Dept implements Translatable {
+class Dept extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => DEPT_TABLE,
+        'pk' => array('dept_id'),
+        'joins' => array(
+            'sla' => array(
+                'constraint' => array('sla_id' => 'SLA.sla_id'),
+                'null' => true,
+            ),
+            'manager' => array(
+                'constraint' => array('manager_id' => 'Staff.staff_id'),
+            ),
+            'groups' => array(
+                'reverse' => 'GroupDeptAccess.dept'
+            ),
+        ),
+    );
 
-    var $id;
-
-    var $email;
-    var $sla;
-    var $manager;
     var $members;
-    var $groups;
+    var $config;
 
-    var $ht;
+    var $template;
+    var $email;
+    var $autorespEmail;
 
     const ALERTS_DISABLED = 2;
     const ALERTS_DEPT_AND_GROUPS = 1;
     const ALERTS_DEPT_ONLY = 0;
 
-    function Dept($id) {
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id=0) {
-        global $cfg;
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT dept.*,dept.dept_id as id,dept.dept_name as name, dept.dept_signature as signature, count(staff.staff_id) as users '
-            .' FROM '.DEPT_TABLE.' dept '
-            .' LEFT JOIN '.STAFF_TABLE.' staff ON (dept.dept_id=staff.dept_id) '
-            .' WHERE dept.dept_id='.db_input($id)
-            .' GROUP BY dept.dept_id';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['dept_id'];
-        $this->email=$this->sla=$this->manager=null;
-        $this->getEmail(); //Auto load email struct.
-        $this->config = new Config('dept.'.$this->id);
-        $this->members=$this->groups=array();
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
+    function getConfig() {
+        if (!isset($this->config))
+            $this->config = new Config('dept.'. $this->getId());
+        return $this->config;
     }
 
     function asVar() {
@@ -71,27 +55,33 @@ class Dept implements Translatable {
     }
 
     function getId() {
-        return $this->id;
+        return $this->dept_id;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->dept_name;
     }
 
     function getLocalName($locale=false) {
-        return CustomDataTranslation::translate($this->getTranslationTag(), $locale);
+        $tag = $this->getTranslateTag();
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->dept_name;
     }
-    function getTranslationTag() {
-        return _H('dept.name.' . $this->getId());
+    static function getLocalById($id, $subtag, $default) {
+        $tag = _H(sprintf('dept.%s.%s', $subtag, $id));
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $default;
+    }
+
+    function getTranslateTag($subtag='name') {
+        return _H(sprintf('dept.%s.%s', $subtag, $this->getId()));
     }
 
     function getEmailId() {
-        return $this->ht['email_id'];
+        return $this->email_id;
     }
 
     function getEmail() {
-        global $cfg;
-
         if(!$this->email)
             if(!($this->email = Email::lookup($this->getEmailId())) && $cfg)
                 $this->email = $cfg->getDefaultEmail();
@@ -99,54 +89,36 @@ class Dept implements Translatable {
         return $this->email;
     }
 
-    function getNumStaff() {
-        return $this->ht['users'];
-    }
-
-
-    function getNumUsers() {
-        return $this->getNumStaff();
-    }
-
     function getNumMembers() {
         return count($this->getMembers());
     }
 
     function getMembers($criteria=null) {
-
-        if(!$this->members || $criteria) {
-            $members = array();
-            $sql='SELECT DISTINCT s.staff_id FROM '.STAFF_TABLE.' s '
-                .' LEFT JOIN '.GROUP_TABLE.' g ON (g.group_id=s.group_id) '
-                .' LEFT JOIN '.GROUP_DEPT_TABLE.' gd ON(s.group_id=gd.group_id) '
-                .' INNER JOIN '.DEPT_TABLE.' d
-                       ON ( d.dept_id=s.dept_id
-                            OR d.manager_id=s.staff_id
-                            OR (d.dept_id=gd.dept_id AND d.group_membership='.
-                                self::ALERTS_DEPT_AND_GROUPS.')
-                        ) '
-                .' WHERE d.dept_id='.db_input($this->getId());
+        if (!$this->members || $criteria) {
+            $members = Staff::objects()
+                ->filter(Q::any(array(
+                    'dept_id' => $this->getId(),
+                    new Q(array(
+                        'group__depts__dept_id' => $this->getId(),
+                        'group__depts__group_membership' => self::ALERTS_DEPT_AND_GROUPS,
+                    )),
+                    'staff_id' => $this->manager_id
+                )));
 
             if ($criteria && $criteria['available'])
-                $sql .= ' AND
-                        ( g.group_enabled=1
-                          AND s.isactive=1
-                          AND s.onvacation=0 ) ';
-
-            $sql.=' ORDER BY s.lastname, s.firstname';
+                $members->filter(array(
+                    'group__group_enabled' => 1,
+                    'isactive' => 1,
+                    'onvacation' => 0,
+                ));
 
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    $members[$id] = Staff::lookup($id);
-            }
+            $qs->order_by('lastname', 'firstname');
 
             if ($criteria)
-                return $members;
-
-            $this->members = $members;
+                return $qs->all();
 
+            $this->members = $qs->all();
         }
-
         return $this->members;
     }
 
@@ -166,19 +138,15 @@ class Dept implements Translatable {
     }
 
     function getSLAId() {
-        return $this->ht['sla_id'];
+        return $this->sla_id;
     }
 
     function getSLA() {
-
-        if(!$this->sla && $this->getSLAId())
-            $this->sla=SLA::lookup($this->getSLAId());
-
         return $this->sla;
     }
 
     function getTemplateId() {
-         return $this->ht['tpl_id'];
+         return $this->tpl_id;
     }
 
     function getTemplate() {
@@ -195,8 +163,8 @@ class Dept implements Translatable {
     function getAutoRespEmail() {
 
         if (!$this->autorespEmail) {
-            if (!$this->ht['autoresp_email_id']
-                    || !($this->autorespEmail = Email::lookup($this->ht['autoresp_email_id'])))
+            if (!$this->autoresp_email_id
+                    || !($this->autorespEmail = Email::lookup($this->autoresp_email_id)))
                 $this->autorespEmail = $this->getEmail();
         }
 
@@ -209,7 +177,7 @@ class Dept implements Translatable {
     }
 
     function getSignature() {
-        return $this->ht['signature'];
+        return $this->dept_signature;
     }
 
     function canAppendSignature() {
@@ -217,14 +185,10 @@ class Dept implements Translatable {
     }
 
     function getManagerId() {
-        return $this->ht['manager_id'];
+        return $this->manager_id;
     }
 
     function getManager() {
-
-        if(!$this->manager && $this->getManagerId())
-            $this->manager=Staff::lookup($this->getManagerId());
-
         return $this->manager;
     }
 
@@ -237,27 +201,27 @@ class Dept implements Translatable {
 
 
     function isPublic() {
-         return ($this->ht['ispublic']);
+         return $this->ispublic;
     }
 
     function autoRespONNewTicket() {
-        return ($this->ht['ticket_auto_response']);
+        return $this->ticket_auto_response;
     }
 
     function autoRespONNewMessage() {
-        return ($this->ht['message_auto_response']);
+        return $this->message_auto_response;
     }
 
     function noreplyAutoResp() {
-         return ($this->ht['noreply_autoresp']);
+         return $this->noreply_autoresp;
     }
 
     function assignMembersOnly() {
-        return ($this->config->get('assign_members_only', 0));
+        return $this->getConfig()->get('assign_members_only', 0);
     }
 
     function isGroupMembershipEnabled() {
-        return ($this->ht['group_membership']);
+        return $this->group_membership;
     }
 
     function getHashtable() {
@@ -265,21 +229,21 @@ class Dept implements Translatable {
     }
 
     function getInfo() {
-        return $this->config->getInfo() + $this->getHashtable();
+        return $this->getConfig()->getInfo() + $this->getHashtable();
     }
 
     function getAllowedGroups() {
+        if ($this->groups)
+            return $this->groups;
 
-        if($this->groups) return $this->groups;
-
-        $sql='SELECT group_id FROM '.GROUP_DEPT_TABLE
-            .' WHERE dept_id='.db_input($this->getId());
+        $groups = GroupDept::object()
+            ->filter(array('dept_id' => $this->getId()))
+            ->values_flat('group_id');
 
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id)=db_fetch_row($res))
-                $this->groups[] = $id;
+        foreach ($groups as $row) {
+            list($id) = $row;
+            $this->groups[] = $id;
         }
-
         return $this->groups;
     }
 
@@ -287,31 +251,23 @@ class Dept implements Translatable {
 
         // Groups allowes to access department
         if($vars['groups'] && is_array($vars['groups'])) {
-            foreach($vars['groups'] as $k=>$id) {
-                $sql='INSERT IGNORE INTO '.GROUP_DEPT_TABLE
-                    .' SET dept_id='.db_input($this->getId()).', group_id='.db_input($id);
-                db_query($sql);
+            $groups = GroupDept::object()
+                ->filter(array('dept_id' => $this->getId()));
+            foreach ($groups as $group) {
+                if ($idx = array_search($group->group_id, $vars['groups']))
+                    unset($vars['groups'][$idx]);
+                else
+                    $group->delete();
+            }
+            foreach ($vars['groups'] as $id) {
+                GroupDept::create(array(
+                    'dept_id'=>$this->getId(), 'group_id'=>$id
+                ))->save();
             }
         }
-        $sql='DELETE FROM '.GROUP_DEPT_TABLE.' WHERE dept_id='.db_input($this->getId());
-        if($vars['groups'] && is_array($vars['groups']))
-            $sql.=' AND group_id NOT IN ('.implode(',', db_input($vars['groups'])).')';
-
-        db_query($sql);
 
         // Misc. config settings
-        $this->config->set('assign_members_only', $vars['assign_members_only']);
-
-        return true;
-    }
-
-    function update($vars, &$errors) {
-
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->updateSettings($vars);
-        $this->reload();
+        $this->getConfig()->set('assign_members_only', $vars['assign_members_only']);
 
         return true;
     }
@@ -319,14 +275,19 @@ class Dept implements Translatable {
     function delete() {
         global $cfg;
 
-        if(!$cfg
-                // Default department cannot be deleted
-                || $this->getId()==$cfg->getDefaultDeptId()
-                // Department  with users cannot be deleted
-                || $this->getNumUsers())
+        if (!$cfg
+            // Default department cannot be deleted
+            || $this->getId()==$cfg->getDefaultDeptId()
+            // Department  with users cannot be deleted
+            || Staff::objects()
+                ->filter(array('dept_id'=>$this->getId()))
+                ->count()
+        ) {
             return 0;
+        }
 
-        $id=$this->getId();
+        parent::delete();
+        $id = $this->getId();
         $sql='DELETE FROM '.DEPT_TABLE.' WHERE dept_id='.db_input($id).' LIMIT 1';
         if(db_query($sql) && ($num=db_affected_rows())) {
             // DO SOME HOUSE CLEANING
@@ -344,7 +305,7 @@ class Dept implements Translatable {
             db_query('DELETE FROM '.GROUP_DEPT_TABLE.' WHERE dept_id='.db_input($id));
 
             // Destrory config settings
-            $this->config->destroy();
+            $this->getConfig()->destroy();
         }
 
         return $num;
@@ -355,22 +316,18 @@ class Dept implements Translatable {
     }
 
     /*----Static functions-------*/
-	function getIdByName($name) {
-        $id=0;
-        $sql ='SELECT dept_id FROM '.DEPT_TABLE.' WHERE dept_name='.db_input($name);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
+	static function getIdByName($name) {
+        $row = static::objects()
+            ->filter(array('dept_name'=>$name))
+            ->values_flat('dept_id')
+            ->first();
 
-        return $id;
-    }
-
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($dept = new Dept($id)) && $dept->getId()==$id)?$dept:null;
+        return $row ? $row[0] : 0;
     }
 
     function getNameById($id) {
 
-        if($id && ($dept=Dept::lookup($id)))
+        if($id && ($dept=static::lookup($id)))
             $name= $dept->getName();
 
         return $name;
@@ -381,106 +338,109 @@ class Dept implements Translatable {
         return ($cfg && $cfg->getDefaultDeptId() && ($name=Dept::getNameById($cfg->getDefaultDeptId())))?$name:null;
     }
 
-    function getDepartments( $criteria=null) {
+    static function getDepartments( $criteria=null) {
 
-        $depts=array();
-        $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' WHERE 1';
-        if($criteria['publiconly'])
-            $sql.=' AND  ispublic=1';
+        $depts = self::objects();
+        if ($criteria['publiconly'])
+            $depts->filter(array('public' => 1));
 
-        if(($manager=$criteria['manager']))
-            $sql.=' AND manager_id='.db_input(is_object($manager)?$manager->getId():$manager);
+        if ($manager=$criteria['manager'])
+            $depts->filter(array('manager_id' => is_object($manager)?$manager->getId():$manager));
 
-        $sql.=' ORDER BY dept_name';
+        $depts->order_by('dept_name')
+            ->values_flat('dept_id', 'dept_name');
 
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name)=db_fetch_row($res))
-                $depts[$id] = $name;
+        $names = array();
+        foreach ($depts as $row) {
+            list($id, $name) = $row;
+            $names[$id] = $name;
         }
 
         // Fetch local names
-        foreach (CustomDataTranslation::getDepartmentNames(array_keys($depts)) as $id=>$name) {
+        foreach (CustomDataTranslation::getDepartmentNames(array_keys($names)) as $id=>$name) {
             // Translate the department
-            $depts[$id] = $name;
+            $names[$id] = $name;
         }
-
-        return $depts;
+        return $names;
     }
 
     function getPublicDepartments() {
         return self::getDepartments(array('publiconly'=>true));
     }
 
-    function create($vars, &$errors) {
-
-        if(!($id=self::save(0, $vars, $errors)))
-            return null;
-
-        if (($dept=self::lookup($id)))
-            $dept->updateSettings($vars);
+    static function create($vars, &$errors) {
+        $dept = parent::create($vars);
+        $dept->create = SqlFunction::NOW();
+        return $dept;
+    }
 
-        return $id;
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 
-    function save($id, $vars, &$errors) {
+    function update($vars, &$errors) {
         global $cfg;
 
-        if($id && $id!=$vars['id'])
+        if (isset($this->dept_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Missing or invalid Dept ID (internal error).');
 
-        if(!$vars['name']) {
+        if (!$vars['name']) {
             $errors['name']=__('Name required');
-        } elseif(strlen($vars['name'])<4) {
+        } elseif (strlen($vars['name'])<4) {
             $errors['name']=__('Name is too short.');
-        } elseif(($did=Dept::getIdByName($vars['name'])) && $did!=$id) {
+        } elseif (($did=static::getIdByName($vars['name']))
+                && (!isset($this->dept_id) || $did!=$this->getId())) {
             $errors['name']=__('Department already exists');
         }
 
-        if(!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId()))
+        if (!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId()))
             $errors['ispublic']=__('System default department cannot be private');
 
-        if($errors) return false;
-
-
-        $sql='SET updated=NOW() '
-            .' ,ispublic='.db_input(isset($vars['ispublic'])?$vars['ispublic']:0)
-            .' ,email_id='.db_input(isset($vars['email_id'])?$vars['email_id']:0)
-            .' ,tpl_id='.db_input(isset($vars['tpl_id'])?$vars['tpl_id']:0)
-            .' ,sla_id='.db_input(isset($vars['sla_id'])?$vars['sla_id']:0)
-            .' ,autoresp_email_id='.db_input(isset($vars['autoresp_email_id'])?$vars['autoresp_email_id']:0)
-            .' ,manager_id='.db_input($vars['manager_id']?$vars['manager_id']:0)
-            .' ,dept_name='.db_input(Format::striptags($vars['name']))
-            .' ,dept_signature='.db_input(Format::sanitize($vars['signature']))
-            .' ,group_membership='.db_input($vars['group_membership'])
-            .' ,ticket_auto_response='.db_input(isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1)
-            .' ,message_auto_response='.db_input(isset($vars['message_auto_response'])?$vars['message_auto_response']:1);
-
-
-        if($id) {
-            $sql='UPDATE '.DEPT_TABLE.' '.$sql.' WHERE dept_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+        if ($errors)
+            return false;
 
+        $this->updated = SqlFunction::NOW();
+        $this->ispublic = isset($vars['ispublic'])?$vars['ispublic']:0;
+        $this->email_id = isset($vars['email_id'])?$vars['email_id']:0;
+        $this->tpl_id = isset($vars['tpl_id'])?$vars['tpl_id']:0;
+        $this->sla_id = isset($vars['sla_id'])?$vars['sla_id']:0;
+        $this->autoresp_email_id = isset($vars['autoresp_email_id'])?$vars['autoresp_email_id']:0;
+        $this->manager_id = $vars['manager_id']?$vars['manager_id']:0;
+        $this->dept_name = Format::striptags($vars['name']);
+        $this->dept_signature = Format::sanitize($vars['signature']);
+        $this->group_membership = $vars['group_membership'];
+        $this->ticket_auto_response = isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1;
+        $this->message_auto_response = isset($vars['message_auto_response'])?$vars['message_auto_response']:1;
+
+        if ($this->save())
+            return $this->updateSettings($vars);
+
+        if (isset($this->dept_id))
             $errors['err']=sprintf(__('Unable to update %s.'), __('this department'))
                .' '.__('Internal error occurred');
-
-        } else {
-            if (isset($vars['id']))
-                $sql .= ', dept_id='.db_input($vars['id']);
-
-            $sql='INSERT INTO '.DEPT_TABLE.' '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
-
-
+        else
             $errors['err']=sprintf(__('Unable to create %s.'), __('this department'))
                .' '.__('Internal error occurred');
 
-        }
-
-
         return false;
     }
 
 }
+
+class GroupDeptAccess extends VerySimpleModel {
+    static $meta = array(
+        'table' => GROUP_DEPT_TABLE,
+        'pk' => array('dept_id', 'group_id'),
+        'joins' => array(
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.dept_id'),
+            ),
+            'group' => array(
+                'constraint' => array('group_id' => 'Group.group_id'),
+            ),
+        ),
+    );
+}
 ?>
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index e2a282e826813720e70208584b26af1554e10142..1ff552bd0d80edd5d56242312e1cc8ff4dab00e2 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -412,6 +412,7 @@ class DynamicFormField extends VerySimpleModel {
         'table' => FORM_FIELD_TABLE,
         'ordering' => array('sort'),
         'pk' => array('id'),
+        'select_related' => array('form'),
         'joins' => array(
             'form' => array(
                 'null' => true,
@@ -1107,7 +1108,7 @@ class SelectionField extends FormField {
     function getWidget() {
         $config = $this->getConfiguration();
         $widgetClass = false;
-        if ($config['widget'] == 'typeahead')
+        if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
             $widgetClass = 'TypeaheadSelectionWidget';
         return parent::getWidget($widgetClass);
     }
@@ -1191,6 +1192,13 @@ class SelectionField extends FormField {
 
     function getConfigurationOptions() {
         return array(
+            'multiselect' => new BooleanField(array(
+                'id'=>2,
+                'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
+                'required'=>false, 'default'=>false,
+                'configuration'=>array(
+                    'desc'=>__('Allow multiple selections')),
+            )),
             'widget' => new ChoiceField(array(
                 'id'=>1,
                 'label'=>__('Widget'),
@@ -1202,18 +1210,11 @@ class SelectionField extends FormField {
                 'configuration'=>array(
                     'multiselect' => false,
                 ),
-                'hint'=>__('Typeahead will work better for large lists')
-            )),
-            'multiselect' => new BooleanField(array(
-                'id'=>2,
-                'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
-                'required'=>false, 'default'=>false,
-                'configuration'=>array(
-                    'desc'=>__('Allow multiple selections')),
                 'visibility' => new VisibilityConstraint(
-                    new Q(array('widget__eq'=>'dropdown')),
+                    new Q(array('multiselect__eq'=>false)),
                     VisibilityConstraint::HIDDEN
                 ),
+                'hint'=>__('Typeahead will work better for large lists')
             )),
             'prompt' => new TextboxField(array(
                 'id'=>3,
@@ -1237,7 +1238,7 @@ class SelectionField extends FormField {
         if ($config['widget'])
             $config['typeahead'] = $config['widget'] == 'typeahead';
 
-        //Typeahed doesn't support multiselect for now  TODO: Add!
+        // Drop down list does not support multiple selections
         if ($config['typeahead'])
             $config['multiselect'] = false;
 
@@ -1290,6 +1291,42 @@ class SelectionField extends FormField {
         }
         return $data;
     }
+
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'notset' =>     __('does not have a value'),
+            'includes' =>   __('includes'),
+            '!includes' =>  __('does not include'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'notset' => null,
+            'includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+            '!includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case '!includes':
+            return Q::not(array("{$name}__intersect" => array_keys($value)));
+        case 'includes':
+            return new Q(array("{$name}__intersect" => array_keys($value)));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
 }
 
 class TypeaheadSelectionWidget extends ChoicesWidget {
diff --git a/include/class.export.php b/include/class.export.php
index 521eb7cefbdb89605ebe87e04ea2d644f359d60c..6f8ec713f3437a2cb21a264a80aa4637966ddd7e 100644
--- a/include/class.export.php
+++ b/include/class.export.php
@@ -42,48 +42,40 @@ class Export {
     #      attachments associated with each, ...
     static function dumpTickets($sql, $how='csv') {
         // Add custom fields to the $sql statement
-        $cdata = $fields = $select = array();
+        $cdata = $fields = array();
         foreach (TicketForm::getInstance()->getFields() as $f) {
             // Ignore core fields
-            if (in_array($f->get('name'), array('subject','priority')))
+            if (in_array($f->get('name'), array('priority')))
                 continue;
             // Ignore non-data fields
             elseif (!$f->hasData() || $f->isPresentationOnly())
                 continue;
 
-            $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id');
-            $key = '__field_'.$f->get('id');
-            // Fetch ID values for ID-based data
-            if ($f->hasIdValue()) {
-                $name .= '_id';
-            }
-            $cdata[$key] = $f->get('label');
+            $name = $f->get('name') ?: 'field_'.$f->get('id');
+            $key = 'cdata.'.$name;
             $fields[$key] = $f;
-            $select[] = "cdata.`$name` AS __field_".$f->get('id');
+            $cdata[$key] = $f->getLocal('label');
         }
-        if ($select)
-            $sql = str_replace(' FROM ', ',' . implode(',', $select) . ' FROM ', $sql);
         return self::dumpQuery($sql,
             array(
                 'number' =>         __('Ticket Number'),
-                'ticket_created' => __('Date'),
-                'subject' =>        __('Subject'),
-                'name' =>           __('From'),
-                'email' =>          __('From Email'),
-                'priority_desc' =>  __('Priority'),
-                'dept_name' =>      __('Department'),
-                'helptopic' =>      __('Help Topic'),
+                'created' =>        __('Date'),
+                'cdata.subject' =>  __('Subject'),
+                'user.name' =>      __('From'),
+                'user.default_email.address' => __('From Email'),
+                'cdata.:priority.priority_desc' => __('Priority'),
+                'dept::getLocalName' => __('Department'),
+                'topic::getName' => __('Help Topic'),
                 'source' =>         __('Source'),
-                'status' =>         __('Current Status'),
-                'effective_date' => __('Last Updated'),
+                'status::getName' =>__('Current Status'),
+                '::getEffectiveDate' => __('Last Updated'),
                 'duedate' =>        __('Due Date'),
                 'isoverdue' =>      __('Overdue'),
                 'isanswered' =>     __('Answered'),
-                'assigned' =>       __('Assigned To'),
-                'staff' =>          __('Agent Assigned'),
-                'team' =>           __('Team Assigned'),
-                'thread_count' =>   __('Thread Count'),
-                'attachments' =>    __('Attachment Count'),
+                'staff::getName' => __('Agent Assigned'),
+                'team::getName' =>  __('Team Assigned'),
+                #'thread_count' =>   __('Thread Count'),
+                #'attachments' =>    __('Attachment Count'),
             ) + $cdata,
             $how,
             array('modify' => function(&$record, $keys) use ($fields) {
@@ -213,32 +205,22 @@ class Export {
 class ResultSetExporter {
     var $output;
 
-    function ResultSetExporter($sql, $headers, $options=array()) {
+    function __construct($sql, $headers, $options=array()) {
         $this->headers = array_values($headers);
-        if ($s = strpos(strtoupper($sql), ' LIMIT '))
-            $sql = substr($sql, 0, $s);
+        // Remove limit and offset
+        $sql->limit(null)->offset(null);
         # TODO: If $filter, add different LIMIT clause to query
         $this->options = $options;
         $this->output = $options['output'] ?: fopen('php://output', 'w');
 
-        $this->_res = db_query($sql, true, true);
-        if ($row = db_fetch_array($this->_res)) {
-            $query_fields = array_keys($row);
-            $this->headers = array();
-            $this->keys = array();
-            $this->lookups = array();
-            foreach ($headers as $field=>$name) {
-                if (array_key_exists($field, $row)) {
-                    $this->headers[] = $name;
-                    $this->keys[] = $field;
-                    # Remember the location of this header in the query results
-                    # (column-wise) so we don't have to do hashtable lookups for every
-                    # column of every row.
-                    $this->lookups[] = array_search($field, $query_fields);
-                }
-            }
-            db_data_reset($this->_res);
+        $this->headers = array();
+        $this->keys = array();
+        foreach ($headers as $field=>$name) {
+            $this->headers[] = $name;
+            $this->keys[] = $field;
         }
+        $this->_res = $sql->getIterator();
+        $this->_res->rewind();
     }
 
     function getHeaders() {
@@ -246,12 +228,30 @@ class ResultSetExporter {
     }
 
     function next() {
-        if (!($row = db_fetch_row($this->_res)))
+        if (!$this->_res->valid())
             return false;
 
+        $object = $this->_res->current();
+        $this->_res->next();
+
         $record = array();
-        foreach ($this->lookups as $idx)
-            $record[] = $row[$idx];
+
+        foreach ($this->keys as $field) {
+            list($field, $func) = explode('::', $field);
+            $path = explode('.', $field);
+            $current = $object;
+            // Evaluate dotted ORM path
+            if ($field) {
+                foreach ($path as $P) {
+                    $current = $current->{$P};
+                }
+            }
+            // Evalutate :: function call on target current
+            if ($func && method_exists($current, $func)) {
+                $current = $current->{$func}();
+            }
+            $record[] = (string) $current;
+        }
 
         if (isset($this->options['modify']) && is_callable($this->options['modify']))
             $record = $this->options['modify']($record, $this->keys);
diff --git a/include/class.forms.php b/include/class.forms.php
index fb5b3cf6f32136e8005a7a04256f30e9642bc10e..e744f172cc41cd4b3cd073f659da8b2c1b888540 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -23,6 +23,8 @@ class Form {
     var $title = '';
     var $instructions = '';
 
+    var $validators = array();
+
     var $_errors = null;
     var $_source = false;
 
@@ -75,6 +77,11 @@ class Form {
         if (!isset($this->_errors)) {
             $this->_errors = array();
             $this->getClean();
+            // Validate the whole form so that errors can be added to the
+            // individual fields and collected below.
+            foreach ($this->validators as $V) {
+                $V($this);
+            }
             foreach ($this->getFields() as $field)
                 if ($field->errors() && (!$include || $include($field)))
                     $this->_errors[$field->get('id')] = $field->errors();
@@ -96,8 +103,18 @@ class Form {
         return $this->_clean;
     }
 
-    function errors() {
-        return $this->_errors;
+    function errors($formOnly=false) {
+        return ($formOnly) ? $this->_errors['form'] : $this->_errors;
+    }
+
+    function addError($message) {
+        $this->_errors['form'][] = $message;
+    }
+
+    function addValidator($function) {
+        if (!is_callable($function))
+            throw new Exception('Form validator must be callable');
+        $this->validators[] = $function;
     }
 
     function render($staff=true, $title=false, $options=array()) {
@@ -145,6 +162,51 @@ class Form {
             break;
         }
     }
+
+    /**
+     * getState
+     *
+     * Retrieves an array of information which can be passed to the
+     * ::loadState method later to recreate the current state of the form
+     * fields and values.
+     */
+    function getState() {
+        $info = array();
+        foreach ($this->getFields() as $f) {
+            // Skip invisible fields
+            if (!$f->isVisible())
+                continue;
+
+            // Skip fields set to default values
+            $v = $f->getClean();
+            $d = $f->get('default');
+            if ($v == $d)
+                continue;
+
+            // Skip empty values
+            if (!$v)
+                continue;
+
+            $info[$f->get('name') ?: $f->get('id')] = $f->to_database($v);
+        }
+        return $info;
+    }
+
+    /**
+     * loadState
+     *
+     * Reset this form to the state previously recorded by the ::getState()
+     * method
+     */
+    function loadState($state) {
+        foreach ($this->getFields() as $f) {
+            $name = $f->get('name');
+            $f->reset();
+            if (isset($state[$name])) {
+                $f->value = $f->to_php($state[$name]);
+            }
+        }
+    }
 }
 
 require_once(INCLUDE_DIR . "class.json.php");
@@ -153,7 +215,7 @@ class FormField {
     static $widget = false;
 
     var $ht = array(
-        'label' => 'Unlabeled',
+        'label' => false,
         'required' => false,
         'default' => false,
         'configuration' => array(),
@@ -183,7 +245,7 @@ class FormField {
         ),
     );
     static $more_types = array();
-    static $uid = 100;
+    static $uid = null;
 
     function __construct($options=array()) {
         $this->ht = array_merge($this->ht, $options);
@@ -218,8 +280,10 @@ class FormField {
                 return $types[$type];
     }
 
-    function get($what) {
-        return $this->ht[$what];
+    function get($what, $default=null) {
+        return array_key_exists($what, $this->ht)
+            ? $this->ht[$what]
+            : $default;
     }
     function set($field, $value) {
         $this->ht[$field] = $value;
@@ -252,6 +316,9 @@ class FormField {
 
             if ($this->isVisible())
                 $this->validateEntry($this->_clean);
+
+            if (!isset($this->_clean) && ($d = $this->get('default')))
+                $this->_clean = $d;
         }
         return $this->_clean;
     }
@@ -420,10 +487,93 @@ class FormField {
         return $this->toString($this->getClean());
     }
 
+    /**
+     * Fetches a value that represents this content in a consistent,
+     * searchable format. This is used by the search engine system and
+     * backend.
+     */
     function searchable($value) {
         return Format::searchable($this->toString($value));
     }
 
+    /**
+     * Fetches a list of options for searching. The values returned from
+     * this method are passed to the widget's `::render()` method so that
+     * the widget can be affected by this setting. For instance, date fields
+     * might have a 'between' search option which should trigger rendering
+     * of two date widgets for search results.
+     */
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'notset' =>     __('does not have a value'),
+            'equal' =>      __('is'),
+            'equal.not' =>  __('is not'),
+            'contains' =>   __('contains'),
+            'match' =>      __('matches'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'notset' => null,
+            'equal' => array('TextboxField', array()),
+            'equal.not' => array('TextboxField', array()),
+            'contains' => array('TextboxField', array()),
+            'match' => array('TextboxField', array(
+                'placeholder' => __('Valid regular expression'),
+                'configuration' => array('size'=>30),
+                'validators' => function($self, $v) {
+                    if (false === @preg_match($v, ' '))
+                        $self->addError(__('Cannot compile this regular expression'));
+                })),
+        );
+    }
+
+    /**
+     * This is used by the searching system to build a query for the search
+     * engine. The function should return a criteria listing to match
+     * content saved by the field by the `::to_database()` function.
+     */
+    function getSearchQ($method, $value, $name=false) {
+        $criteria = array();
+        $Q = new Q();
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+            case 'notset':
+                $Q->negate();
+            case 'set':
+                $criteria[$name . '__isnull'] = false;
+                break;
+
+            case 'equal.not':
+                $Q->negate();
+            case 'equal':
+                $criteria[$name . '__eq'] = $value;
+                break;
+
+            case 'contains':
+                $criteria[$name . '__contains'] = $value;
+                break;
+
+            case 'match':
+                $criteria[$name . '__regex'] = $value;
+                break;
+        }
+        return $Q->add($criteria);
+    }
+
+    function getSearchWidget($method) {
+        $methods = $this->getSearchMethodWidgets();
+        $info = $methods[$method];
+        if (is_array($info)) {
+            $class = $info[0];
+            return new $class($info[1]);
+        }
+        return $info;
+    }
+
     function getLabel() { return $this->get('label'); }
 
     /**
@@ -649,7 +799,8 @@ class FormField {
     }
 
     function getTranslateTag($subtag) {
-        return _H(sprintf('field.%s.%s', $subtag, $this->get('id')));
+        return _H(sprintf('field.%s.%s.%s', $subtag, $this->get('id'),
+            $this->get('form_id', '*internal*')));
     }
     function getLocal($subtag, $default=false) {
         $tag = $this->getTranslateTag($subtag);
@@ -912,6 +1063,20 @@ class BooleanField extends FormField {
     function toString($value) {
         return ($value) ? __('Yes') : __('No');
     }
+
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('checked'),
+            'set.not' =>    __('unchecked'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'set.not' => null,
+        );
+    }
 }
 
 class ChoiceField extends FormField {
@@ -1029,7 +1194,43 @@ class ChoiceField extends FormField {
             }
         }
         return $this->_choices;
-     }
+    }
+
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'notset' =>     __('does not have a value'),
+            'includes' =>   __('includes'),
+            '!includes' =>  __('does not include'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'notset' => null,
+            'includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+            '!includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case '!includes':
+            return Q::not(array("{$name}__in" => array_keys($value)));
+        case 'includes':
+            return new Q(array("{$name}__in" => array_keys($value)));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
 }
 
 class DatetimeField extends FormField {
@@ -1102,6 +1303,97 @@ class DatetimeField extends FormField {
         elseif ($value === -1 or $value === false)
             $this->_errors[] = __('Enter a valid date');
     }
+
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'notset' =>     __('does not have a value'),
+            'equal' =>      __('on'),
+            'notequal' =>   __('not on'),
+            'before' =>     __('before'),
+            'after' =>      __('after'),
+            'between' =>    __('between'),
+            'ndaysago' =>   __('in the last n days'),
+            'ndays' =>      __('in the next n days'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        $config_notime = $config = $this->getConfiguration();
+        $config_notime['time'] = false;
+        return array(
+            'set' => null,
+            'notset' => null,
+            'equal' => array('DatetimeField', array(
+                'configuration' => $config_notime,
+            )),
+            'notequal' => array('DatetimeField', array(
+                'configuration' => $config_notime,
+            )),
+            'before' => array('DatetimeField', array(
+                'configuration' => $config,
+            )),
+            'after' => array('DatetimeField', array(
+                'configuration' => $config,
+            )),
+            'between' => array('InlineformField', array(
+                'form' => array(
+                    'left' => new DatetimeField(),
+                    'text' => new FreeTextField(array(
+                        'configuration' => array('content' => 'and'))
+                    ),
+                    'right' => new DatetimeField(),
+                ),
+            )),
+            'ndaysago' => array('InlineformField', array(
+                'form' => array(
+                    'until' => new TextboxField(array(
+                        'configuration' => array('validator'=>'number', 'size'=>4))
+                    ),
+                    'text' => new FreeTextField(array(
+                        'configuration' => array('content' => 'days'))
+                    ),
+                ),
+            )),
+            'ndays' => array('InlineformField', array(
+                'form' => array(
+                    'until' => new TextboxField(array(
+                        'configuration' => array('validator'=>'number', 'size'=>4))
+                    ),
+                    'text' => new FreeTextField(array(
+                        'configuration' => array('content' => 'days'))
+                    ),
+                ),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case 'after':
+            return new Q(array("{$name}__gte" => $value));
+        case 'before':
+            return new Q(array("{$name}__lt" => $value));
+        case 'between':
+            return new Q(array(
+                "{$name}__gte" => $value['left'],
+                "{$name}__lte" => $value['right'],
+            ));
+        case 'ndaysago':
+            return new Q(array(
+                "{$name}__lt" => SqlFunction::NOW(),
+                "{$name}__gte" => SqlExpression::minus(SqlFunction::NOW(), SqlInterval::DAY($value['until'])),
+            ));
+        case 'ndays':
+            return new Q(array(
+                "{$name}__gt" => SqlFunction::NOW(),
+                "{$name}__lte" => SqlExpression::plus(SqlFunction::NOW(), SqlInterval::DAY($value['until'])),
+            ));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
 }
 
 /**
@@ -1661,6 +1953,125 @@ class FileUploadField extends FormField {
     }
 }
 
+class InlineFormData extends ArrayObject {
+    var $_form;
+
+    function __construct($form, array $data=array()) {
+        parent::__construct($data);
+        $this->_form = $form;
+    }
+
+    function getVar($tag) {
+        foreach ($this->_form->getFields() as $f) {
+            if ($f->get('name') == $tag)
+                return $this[$f->get('id')];
+        }
+    }
+}
+
+
+class InlineFormField extends FormField {
+    static $widget = 'InlineFormWidget';
+
+    var $_iform = null;
+
+    function validateEntry($value) {
+        if (!$this->getInlineForm()->isValid()) {
+            $this->_errors[] = __('Correct errors in the inline form');
+        }
+    }
+
+    function parse($value) {
+        // The InlineFieldWidget returns an array of cleaned data
+        return $value;
+    }
+
+    function to_database($value) {
+        return JsonDataEncoder::encode($value);
+    }
+
+    function to_php($value) {
+        $data = JsonDataParser::decode($value);
+        // The InlineFormData helps with the variable replacer API
+        return new InlineFormData($this->getInlineForm(), $data);
+    }
+
+    function display($data) {
+        $form = $this->getInlineForm();
+        ob_start(); ?>
+        <div><?php
+        foreach ($form->getFields() as $field) { ?>
+            <span style="display:inline-block;padding:0 5px;vertical-align:top">
+                <strong><?php echo Format::htmlchars($field->get('label')); ?></strong>
+                <div><?php
+                    $value = $data[$field->get('id')];
+                    echo $field->display($value); ?></div>
+            </span><?php
+        } ?>
+        </div><?php
+        return ob_get_clean();
+    }
+
+    function getInlineForm($data=false) {
+        $form = $this->get('form');
+        if (is_array($form)) {
+            $form = new Form($form, $data ?: $this->value ?: $this->getSource());
+        }
+        return $form;
+    }
+}
+
+class InlineDynamicFormField extends FormField {
+    function getInlineForm($data=false) {
+        if (!isset($this->_iform) || $data) {
+            $config = $this->getConfiguration();
+            $this->_iform = DynamicForm::lookup($config['form']);
+            if ($data)
+                $this->_iform = $this->_iform->getForm($data);
+        }
+        return $this->_iform;
+    }
+
+    function getConfigurationOptions() {
+        $forms = DynamicForm::objects()->filter(array('type'=>'G'))
+            ->values_flat('id', 'title');
+        $choices = array();
+        foreach ($forms as $row) {
+            list($id, $title) = $row;
+            $choices[$id] = $title;
+        }
+        return array(
+            'form' => new ChoiceField(array(
+                'id'=>2, 'label'=>'Inline Form', 'required'=>true,
+                'default'=>'', 'choices'=>$choices
+            )),
+        );
+    }
+}
+
+class InlineFormWidget extends Widget {
+    function render($mode=false) {
+        $form = $this->field->getInlineForm();
+        if (!$form)
+            return;
+        // Handle first-step edits -- load data from $this->value
+        if ($form instanceof DynamicForm && !$form->getSource())
+            $form = $form->getForm($this->value);
+        $inc = ($mode == 'client') ? CLIENTINC_DIR : STAFFINC_DIR;
+        include $inc . 'templates/inline-form.tmpl.php';
+    }
+
+    function getValue() {
+        $data = $this->field->getSource();
+        if (!$data)
+            return null;
+        $form = $this->field->getInlineForm($data);
+        if (!$form)
+            return null;
+        return $form->getClean();
+    }
+}
+
 class Widget {
     static $media = null;
 
@@ -1685,6 +2096,8 @@ class Widget {
             return $data[$this->name];
         elseif (isset($data[$this->field->get('name')]))
             return $data[$this->field->get('name')];
+        elseif (isset($data[$this->field->get('id')]))
+            return $data[$this->field->get('id')];
         return null;
     }
 
@@ -1721,7 +2134,6 @@ class TextboxWidget extends Widget {
         $placeholder = sprintf('placeholder="%s"', $this->field->getLocal('placeholder',
             $config['placeholder']));
         ?>
-        <span style="display:inline-block">
         <input type="<?php echo static::$input_type; ?>"
             id="<?php echo $this->id; ?>"
             <?php echo implode(' ', array_filter(array(
@@ -1729,7 +2141,6 @@ class TextboxWidget extends Widget {
                 $translatable, $placeholder))); ?>
             name="<?php echo $this->name; ?>"
             value="<?php echo Format::htmlchars($this->value); ?>"/>
-        </span>
         <?php
     }
 }
@@ -1805,12 +2216,6 @@ class PhoneNumberWidget extends Widget {
 }
 
 class ChoicesWidget extends Widget {
-    static $media = array(
-        'css' => array(
-            '/css/jquery.multiselect.css',
-        ),
-    );
-
     function render($mode=false) {
 
         if ($mode == 'view') {
@@ -1859,9 +2264,9 @@ class ChoicesWidget extends Widget {
         ?>
         <select name="<?php echo $this->name; ?>[]"
             id="<?php echo $this->id; ?>"
-            data-prompt="<?php echo $prompt; ?>"
+            data-placeholder="<?php echo $prompt; ?>"
             <?php if ($config['multiselect'])
-                echo ' multiple="multiple" class="multiselect"'; ?>>
+                echo ' multiple="multiple" class="chosen-select"'; ?>>
             <?php if (!$have_def && !$config['multiselect']) { ?>
             <option value="<?php echo $def_key; ?>">&mdash; <?php
                 echo $def_val; ?> &mdash;</option>
@@ -1880,7 +2285,7 @@ class ChoicesWidget extends Widget {
         <script type="text/javascript">
         $(function() {
             $("#<?php echo $this->id; ?>")
-            .multiselect({'noneSelectedText':'<?php echo $prompt; ?>'});
+            .chosen({'disable_search_threshold':10, 'width': '250px'});
         });
         </script>
        <?php
@@ -2261,7 +2666,9 @@ class VisibilityConstraint {
     }
 
     function compileQPhp(Q $Q, $field) {
-        $form = $field->getForm();
+        if (!($form = $field->getForm())) {
+            return $this->initial == self::VISIBLE;
+        }
         $expr = array();
         foreach ($Q->constraints as $c=>$value) {
             if ($value instanceof Q) {
diff --git a/include/class.group.php b/include/class.group.php
index 87a93994036ae559c4609b43382d43f13480d21a..6352289749de218a7841c40fa1cc4f8daf966d37 100644
--- a/include/class.group.php
+++ b/include/class.group.php
@@ -14,67 +14,41 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Group {
+class Group extends VerySimpleModel {
 
-    var $id;
-    var $ht;
+    static $meta = array(
+        'table' => GROUP_TABLE,
+        'pk' => array('group_id'),
+    );
 
     var $members;
     var $departments;
 
-    function Group($id){
-
-        $this->id=0;
-        return $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT grp.*,grp.group_name as name, grp.group_enabled as isactive, count(staff.staff_id) as users '
-            .'FROM '.GROUP_TABLE.' grp '
-            .'LEFT JOIN '.STAFF_TABLE.' staff USING(group_id) '
-            .'WHERE grp.group_id='.db_input($id).' GROUP BY grp.group_id ';
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['group_id'];
-        $this->members=array();
-        $this->departments = array();
-
-        return $this->id;
-    }
-
-    function reload(){
-        return $this->load();
-    }
-
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        $base['name'] = $base['group_name'];
+        $base['isactive'] = $base['group_enabled'];
+        return $base;
     }
 
     function getInfo(){
-        return  $this->getHashtable();
+        return $this->getHashtable();
     }
 
     function getId(){
-        return $this->id;
+        return $this->group_id;
     }
 
     function getName(){
-        return $this->ht['name'];
+        return $this->group_name;
     }
 
     function getNumUsers(){
-        return $this->ht['users'];
+        return Staff::objects()->filter(array('group_id'=>$this->getId()))->count();
     }
 
-
     function isEnabled(){
-        return ($this->ht['isactive']);
+        return $this->group_enabled;
     }
 
     function isActive(){
@@ -82,7 +56,7 @@ class Group {
     }
 
     function getTranslateTag($subtag) {
-        return _H(sprintf('group.%s.%s', $subtag, $this->id));
+        return _H(sprintf('group.%s.%s', $subtag, $this->getId()));
     }
     function getLocal($subtag) {
         $tag = $this->getTranslateTag($subtag);
@@ -98,169 +72,147 @@ class Group {
     //Get members of the group.
     function getMembers() {
 
-        if(!$this->members && $this->getNumUsers()) {
-            $sql='SELECT staff_id FROM '.STAFF_TABLE
-                .' WHERE group_id='.db_input($this->getId())
-                .' ORDER BY lastname, firstname';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    if(($staff=Staff::lookup($id)))
-                        $this->members[]= $staff;
-            }
+        if (!$this->members) {
+            $this->members = Staff::objects()
+                ->filter(array('group_id'=>$this->getId()))
+                ->order_by('lastname', 'firstname')
+                ->all();
         }
-
         return $this->members;
     }
 
     //Get departments the group is allowed to access.
     function getDepartments() {
-
-        if(!$this->departments) {
-            $sql='SELECT dept_id FROM '.GROUP_DEPT_TABLE
-                .' WHERE group_id='.db_input($this->getId());
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    $this->departments[]= $id;
+        if (!isset($this->departments)) {
+            $this->departments = array();
+            foreach (GroupDeptAccess::objects()
+                ->filter(array('group_id'=>$this->getId()))
+                ->values_flat('dept_id') as $gda
+            ) {
+                $this->departments[] = $gda[0];
             }
         }
-
         return $this->departments;
     }
 
 
-    function updateDeptAccess($depts) {
-
-
-        if($depts && is_array($depts)) {
-            foreach($depts as $k=>$id) {
-                $sql='INSERT IGNORE INTO '.GROUP_DEPT_TABLE
-                    .' SET group_id='.db_input($this->getId())
-                    .', dept_id='.db_input($id);
-                db_query($sql);
+    function updateDeptAccess($dept_ids) {
+        if (is_array($dept_ids)) {
+            $groups = GroupDeptAccess::objects()
+                ->filter(array('group_id' => $this->getId()));
+            foreach ($groups as $group) {
+                if ($idx = array_search($group->dept_id, $dept_ids))
+                    unset($dept_ids[$idx]);
+                else
+                    $group->delete();
+            }
+            foreach ($dept_ids as $id) {
+                GroupDeptAccess::create(array(
+                    'group_id'=>$this->getId(), 'dept_id'=>$id
+                ))->save();
             }
         }
-
-        $sql='DELETE FROM '.GROUP_DEPT_TABLE.' WHERE group_id='.db_input($this->getId());
-        if($depts && is_array($depts)) // just inserted departments IF any.
-            $sql.=' AND dept_id NOT IN('.implode(',', db_input($depts)).')';
-
-        db_query($sql);
-
-        return true;
-    }
-
-    function update($vars,&$errors) {
-
-        if(!Group::save($this->getId(),$vars,$errors))
-            return false;
-
-        $this->updateDeptAccess($vars['depts']);
-        $this->reload();
-        
         return true;
     }
 
     function delete() {
 
-        //Can't delete with members
-        if($this->getNumUsers())
+        // Can't delete with members
+        if ($this->getNumUsers())
             return false;
 
-        $res = db_query('DELETE FROM '.GROUP_TABLE.' WHERE group_id='.db_input($this->getId()).' LIMIT 1');
-        if(!$res || !db_affected_rows($res))
+        if (!parent::delete())
             return false;
 
-        //Remove dept access entry.
-        db_query('DELETE FROM '.GROUP_DEPT_TABLE.' WHERE group_id='.db_input($this->getId()));
+        // Remove dept access entries
+        GroupDeptAccess::objects()
+            ->filter(array('group_id'=>$this->getId()))
+            ->delete();
 
         return true;
     }
 
     /*** Static functions ***/
-    function getIdByName($name){
-        $sql='SELECT group_id FROM '.GROUP_TABLE.' WHERE group_name='.db_input(trim($name));
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
+    static function getIdByName($name){
+        $id = static::objects()->filter(array('group_name'=>trim($name)))
+            ->values_flat('group_id')->first();
 
-        return $id;
+        return $id ? $id[0] : 0;
     }
 
     static function getGroupNames($localize=true) {
         static $groups=array();
 
         if (!$groups) {
-            $sql='SELECT group_id, group_name, group_enabled as isactive FROM '.GROUP_TABLE.' ORDER BY group_name';
-            if (($res=db_query($sql)) && db_num_rows($res)) {
-                while (list($id, $name, $enabled) = db_fetch_row($res)) {
-                    $groups[$id] = sprintf('%s%s',
-                        self::getLocalById($id, 'name', $name),
-                        $enabled ? '' : ' ' . __('(disabled)'));
-                }
+            $query = static::objects()
+                ->values_flat('group_id', 'group_name', 'group_enabled')
+                ->order_by('group_name');
+            foreach ($query as $row) {
+                list($id, $name, $enabled) = $row;
+                $groups[$id] = sprintf('%s%s',
+                    self::getLocalById($id, 'name', $name),
+                    $enabled ? '' : ' ' . __('(disabled)'));
             }
         }
         // TODO: Sort groups if $localize
         return $groups;
     }
 
-    function lookup($id){
-        return ($id && is_numeric($id) && ($g= new Group($id)) && $g->getId()==$id)?$g:null;
+    static function create($vars=false) {
+        $group = parent::create($vars);
+        $group->created = SqlFunction::NOW();
+        return $group;
     }
 
-    function create($vars, &$errors) {
-        if(($id=self::save(0,$vars,$errors)) && ($group=self::lookup($id)))
-            $group->updateDeptAccess($vars['depts']);
-
-        return $id;
+    function save($refetch=false) {
+        if ($this->dirty) {
+            $this->updated = SqlFunction::NOW();
+        }
+        return parent::save($refetch || $this->dirty);
     }
 
-    function save($id,$vars,&$errors) {
-        if($id && $vars['id']!=$id)
+    function update($vars,&$errors) {
+        if (isset($this->group_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Missing or invalid group ID');
-            
-        if(!$vars['name']) {
+
+        if (!$vars['name']) {
             $errors['name']=__('Group name required');
-        }elseif(strlen($vars['name'])<3) {
+        } elseif(strlen($vars['name'])<3) {
             $errors['name']=__('Group name must be at least 3 chars.');
-        }elseif(($gid=Group::getIdByName($vars['name'])) && $gid!=$id){
+        } elseif (($gid=static::getIdByName($vars['name']))
+                && (!isset($this->group_id) || $gid!=$this->getId())) {
             $errors['name']=__('Group name already exists');
         }
-        
-        if($errors) return false;
-            
-        $sql=' SET updated=NOW() '
-            .', group_name='.db_input(Format::striptags($vars['name']))
-            .', group_enabled='.db_input($vars['isactive'])
-            .', can_create_tickets='.db_input($vars['can_create_tickets'])
-            .', can_delete_tickets='.db_input($vars['can_delete_tickets'])
-            .', can_edit_tickets='.db_input($vars['can_edit_tickets'])
-            .', can_assign_tickets='.db_input($vars['can_assign_tickets'])
-            .', can_transfer_tickets='.db_input($vars['can_transfer_tickets'])
-            .', can_close_tickets='.db_input($vars['can_close_tickets'])
-            .', can_ban_emails='.db_input($vars['can_ban_emails'])
-            .', can_manage_premade='.db_input($vars['can_manage_premade'])
-            .', can_manage_faq='.db_input($vars['can_manage_faq'])
-            .', can_post_ticket_reply='.db_input($vars['can_post_ticket_reply'])
-            .', can_view_staff_stats='.db_input($vars['can_view_staff_stats'])
-            .', notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($id) {
-            
-            $sql='UPDATE '.GROUP_TABLE.' '.$sql.' WHERE group_id='.db_input($id);
-            if(($res=db_query($sql)))
-                return true;
 
+        if ($errors)
+            return false;
+
+        $this->group_name=Format::striptags($vars['name']);
+        $this->group_enabled=$vars['isactive'];
+        $this->can_create_tickets=$vars['can_create_tickets'];
+        $this->can_delete_tickets=$vars['can_delete_tickets'];
+        $this->can_edit_tickets=$vars['can_edit_tickets'];
+        $this->can_assign_tickets=$vars['can_assign_tickets'];
+        $this->can_transfer_tickets=$vars['can_transfer_tickets'];
+        $this->can_close_tickets=$vars['can_close_tickets'];
+        $this->can_ban_emails=$vars['can_ban_emails'];
+        $this->can_manage_premade=$vars['can_manage_premade'];
+        $this->can_manage_faq=$vars['can_manage_faq'];
+        $this->can_post_ticket_reply=$vars['can_post_ticket_reply'];
+        $this->can_view_staff_stats=$vars['can_view_staff_stats'];
+        $this->notes=Format::sanitize($vars['notes']);
+
+        if ($this->save())
+            return $this->updateDeptAccess($vars['depts'] ?: array());
+
+        if (isset($this->group_id)) {
             $errors['err']=sprintf(__('Unable to update %s.'), __('this group'))
                .' '.__('Internal error occurred');
-
-        }else{
-            $sql='INSERT INTO '.GROUP_TABLE.' '.$sql.',created=NOW()';
-            if(($res=db_query($sql)) && ($id=db_insert_id()))
-                return $id;
-
+        }
+        else {
             $errors['err']=sprintf(__('Unable to create %s.'), __('this group'))
                .' '.__('Internal error occurred');
         }
-
         return false;
     }
 }
diff --git a/include/class.list.php b/include/class.list.php
index 8a6e4667558eb2733dfdd8cc3e9e50145bab08e9..a95429a6f2c5e2425f2c85612c75d2ae29bae964 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -1176,6 +1176,4 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         include(STAFFINC_DIR . 'templates/status-options.tmpl.php');
     }
 }
-
-TicketStatus::_inspect();
 ?>
diff --git a/include/class.lock.php b/include/class.lock.php
index d6bcbad9dc66105803fbb49a267252d068423242..c61771e67f5a3b166734e41d8c6ee3ebd03c977c 100644
--- a/include/class.lock.php
+++ b/include/class.lock.php
@@ -18,139 +18,130 @@
  * Mainly used as a helper...
  */
 
-class TicketLock {
-    var $id;
-    var $ht;
-    
-    function TicketLock($id, $tid=0) {
-        $this->id=0;
-        $this->load($id, $tid);
-    }
-
-    function load($id=0, $tid=0) {
-
-        if(!$id && $this->ht['id'])
-            $id=$this->ht['id'];
-
-        $sql='SELECT l.*, TIME_TO_SEC(TIMEDIFF(expire,NOW())) as timeleft '
-            .' ,IF(s.staff_id IS NULL,"staff",CONCAT_WS(" ", s.lastname, s.firstname)) as staff '
-            .' FROM '.TICKET_LOCK_TABLE. ' l '
-            .' LEFT JOIN '.STAFF_TABLE.' s ON(s.staff_id=l.staff_id) '
-            .' WHERE lock_id='.db_input($id);
+class TicketLock extends VerySimpleModel {
 
-        if($tid) 
-            $sql.=' AND ticket_id='.db_input($tid);
+    static $meta = array(
+        'table' => TICKET_LOCK_TABLE,
+        'pk' => array('lock_id'),
+        'joins' => array(
+            'ticket' => array(
+                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+            ),
+        ),
+    );
 
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+    var $expiretime;
 
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['id']=$this->ht['lock_id'];
-        $this->ht['expiretime']=time()+$this->ht['timeleft'];
-        
-        return true;
-    }
-  
-    function reload() {
-        return $this->load();
+    function __onload() {
+        if (isset($this->expire))
+            $this->expiretime = strtotime($this->expire);
     }
 
     function getId() {
-        return $this->id;
+        return $this->lock_id;
     }
 
     function getStaffId() {
-        return $this->ht['staff_id'];
+        return $this->staff_id;
     }
 
     function getStaffName() {
-        return $this->ht['staff'];
+        return $this->staff->getName();
     }
 
     function getCreateTime() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function getExpireTime() {
-        return $this->ht['expire'];
+        return $this->expire;
     }
     //Get remaiming time before the lock expires
     function getTime() {
-        return $this->isExpired()?0:($this->ht['expiretime']-time());
+        return $this->isExpired()?0:($this->expiretime-time());
     }
 
     //Should we be doing realtime check here? (Ans: not really....expiretime is local & based on loadtime)
     function isExpired() {
-        return (time()>$this->ht['expiretime']);
+        return (time()>$this->expiretime);
     }
-   
+
     //Renew existing lock.
     function renew($lockTime=0) {
 
         if(!$lockTime || !is_numeric($lockTime)) //XXX: test to  make it works.
-            $lockTime = '(TIME_TO_SEC(TIMEDIFF(expire,created))/60)';
-            
-
-        $sql='UPDATE '.TICKET_LOCK_TABLE
-            .' SET expire=DATE_ADD(NOW(),INTERVAL '.$lockTime.' MINUTE) '
-            .' WHERE lock_id='.db_input($this->getId());
-        //echo $sql;
-        if(!db_query($sql) || !db_affected_rows())
-            return false;
-        
-        $this->reload();
-        
-        return true;
+            $lockTime = $cfg->getLockTime();
+
+        $this->expire = SqlExpression::plus(
+            SqlFunction::NOW(),
+            SqlInterval::MINUTE($lockTime)
+        );
+        return $this->save();
     }
 
     //release aka delete a lock.
     function release() {
-        //FORCED release - we don't give a ....
-        $sql='DELETE FROM '.TICKET_LOCK_TABLE.' WHERE lock_id='.db_input($this->getId()).' LIMIT 1';
-        return (db_query($sql) && db_affected_rows());
+        return $this->delete();
     }
 
     /* ----------------------- Static functions ---------------------------*/
-    function lookup($id, $tid) {
-        return ($id  && ($lock = new TicketLock($id,$tid)) && $lock->getId()==$id)?$lock:null;
+    static function lookup($id, $tid=false) {
+        if ($tid)
+            return parent::lookup(array('lock_id' => $id, 'ticket_id' => $tid));
+        else
+            return parent::lookup($id);
     }
 
-    //Create a ticket lock...this function assumes the caller checked for access & validity of ticket & staff x-ship.    
-    function acquire($ticketId, $staffId, $lockTime) {
+    //Create a ticket lock...this function assumes the caller checked for access & validity of ticket & staff x-ship.
+    static function acquire($ticketId, $staffId, $lockTime) {
 
-        if(!$ticketId or !$staffId or !$lockTime)
+        if (!$ticketId or !$staffId or !$lockTime)
             return 0;
 
-
-        //Cleanup any expired locks on the ticket.
-        db_query('DELETE FROM '.TICKET_LOCK_TABLE.' WHERE ticket_id='.db_input($ticketId).' AND expire<NOW()');
-        //create the new lock.
-        $sql='INSERT IGNORE INTO '.TICKET_LOCK_TABLE.' SET created=NOW() '
-            .',ticket_id='.db_input($ticketId)
-            .',staff_id='.db_input($staffId)
-            .',expire=DATE_ADD(NOW(),INTERVAL '.$lockTime.' MINUTE) ';
-
-        return db_query($sql)?db_insert_id():0;
-    }
-
-    function create($ticketId, $staffId, $lockTime) {
-        if(($id=self::acquire($ticketId, $staffId, $lockTime)))
-            return self::lookup($id);
-    }
-
-    //Simply remove ALL locks a user (staff) holds on a ticket(s).
-    function removeStaffLocks($staffId, $ticketId=0) {
-        $sql='DELETE FROM '.TICKET_LOCK_TABLE.' WHERE staff_id='.db_input($staffId);
-        if($ticketId)
-            $sql.=' AND ticket_id='.db_input($ticketId);
-
-        return db_query($sql);
-    }
-
-    //Called  via cron
-    function cleanup() {
-        //Cleanup any expired locks.
-        db_query('DELETE FROM '.TICKET_LOCK_TABLE.' WHERE expire<NOW()');
+        // Cleanup any expired locks on the ticket.
+        static::objects()->filter(array(
+            'ticket_id' => $ticketId,
+            'expire__lt' => SqlFunction::NOW()
+        ))->delete();
+
+        // Create the new lock.
+        $lock = parent::create(array(
+            'created' => SqlFunction::NOW(),
+            'ticket_id' => $ticketId,
+            'staff_id' => $staffId,
+            'expire' => SqlExpression::plus(
+                SqlFunction::NOW(),
+                SqlInterval::MINUTE($lockTime)
+            ),
+        ));
+        if ($lock->save(true))
+            return $lock;
+    }
+
+    static function create($ticketId, $staffId, $lockTime) {
+        if ($lock = self::acquire($ticketId, $staffId, $lockTime))
+            return $lock;
+    }
+
+    // Simply remove ALL locks a user (staff) holds on a ticket(s).
+    static function removeStaffLocks($staffId, $ticketId=0) {
+        $locks = static::objects()->filter(array(
+            'staff_id' => $staffId,
+        ));
+        if ($ticketId)
+            $locks->filter(array('ticket_id' => $ticketId));
+
+        return $locks->delete();
+    }
+
+    // Called via cron
+    static function cleanup() {
+        static::objects()->filter(array(
+            'expire__lt' => SqlFunction::NOW()
+        ))->delete();
     }
 }
 ?>
diff --git a/include/class.organization.php b/include/class.organization.php
index 785b6d36b489416b99c164efcb6f20865cc6f250..214f7fb2e35c7cd89cb63a6ba4fe9e375ec9bcd2 100644
--- a/include/class.organization.php
+++ b/include/class.organization.php
@@ -25,6 +25,9 @@ class OrganizationModel extends VerySimpleModel {
             'users' => array(
                 'reverse' => 'User.org',
             ),
+            'cdata' => array(
+                'constraint' => array('id' => 'OrganizationCdata.org_id'),
+            ),
         )
     );
 
@@ -100,6 +103,21 @@ class OrganizationModel extends VerySimpleModel {
     }
 }
 
+class OrganizationCdata extends VerySimpleModel {
+    static $meta = array(
+        'table' => 'org__cdata',
+        'view' => true,
+        'pk' => array('org_id'),
+    );
+
+    function getQuery($compiler) {
+        $form = OrganizationForm::getDefaultForm();
+        $exclude = array('name');
+        return '('.$form->getCrossTabQuery($form->type, 'org_id', $exclude).')';
+    }
+}
+
+
 class Organization extends OrganizationModel {
     var $_entries;
     var $_forms;
diff --git a/include/class.orm.php b/include/class.orm.php
index 7e8cde195d5ce6bbb88bbabe7e787a204275c937..f9e51f55332c15e6546cee9cec5688a29d19e10b 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -33,6 +33,7 @@ class ModelMeta implements ArrayAccess {
         'table' => false,
         'defer' => array(),
         'select_related' => array(),
+        'view' => false,
     );
     var $model;
 
@@ -61,34 +62,39 @@ class ModelMeta implements ArrayAccess {
         if (!isset($meta['joins']))
             $meta['joins'] = array();
         foreach ($meta['joins'] as $field => &$j) {
-            if (isset($j['reverse'])) {
-                list($fmodel, $key) = explode('.', $j['reverse']);
-                $info = $fmodel::$meta['joins'][$key];
-                $constraint = array();
-                if (!is_array($info['constraint']))
-                    throw new OrmConfigurationException(sprintf(__(
-                        // `reverse` here is the reverse of an ORM relationship
-                        '%s: Reverse does not specify any constraints'),
-                        $j['reverse']));
-                foreach ($info['constraint'] as $foreign => $local) {
-                    list(,$field) = explode('.', $local);
-                    $constraint[$field] = "$fmodel.$foreign";
-                }
-                $j['constraint'] = $constraint;
-                if (!isset($j['list']))
-                    $j['list'] = true;
-                $j['null'] = $info['null'] ?: false;
-            }
-            // XXX: Make this better (ie. composite keys)
-            $keys = array_keys($j['constraint']);
-            $foreign = $j['constraint'][$keys[0]];
-            $j['fkey'] = explode('.', $foreign);
-            $j['local'] = $keys[0];
+            $this->processJoin($j);
         }
         unset($j);
         $this->base = $meta;
     }
 
+    function processJoin(&$j) {
+        if (isset($j['reverse'])) {
+            list($fmodel, $key) = explode('.', $j['reverse']);
+            $info = $fmodel::$meta['joins'][$key];
+            $constraint = array();
+            if (!is_array($info['constraint']))
+                throw new OrmConfigurationException(sprintf(__(
+                    // `reverse` here is the reverse of an ORM relationship
+                    '%s: Reverse does not specify any constraints'),
+                    $j['reverse']));
+            foreach ($info['constraint'] as $foreign => $local) {
+                list(,$field) = explode('.', $local);
+                $constraint[$field] = "$fmodel.$foreign";
+            }
+            $j['constraint'] = $constraint;
+            if (!isset($j['list']))
+                $j['list'] = true;
+            if (!isset($j['null']))
+                $j['null'] = $info['null'] ?: false;
+        }
+        // XXX: Make this better (ie. composite keys)
+        $keys = array_keys($j['constraint']);
+        $foreign = $j['constraint'][$keys[0]];
+        $j['fkey'] = explode('.', $foreign);
+        $j['local'] = $keys[0];
+    }
+
     function offsetGet($field) {
         if (!isset($this->base[$field]))
             $this->setupLazy($field);
@@ -218,6 +224,11 @@ class VerySimpleModel {
                 return;
             }
             if ($value === null) {
+                if (in_array($j['local'], static::$meta['pk'])) {
+                    // Reverse relationship — don't null out local PK
+                    $this->ht[$field] = $value;
+                    return;
+                }
                 // Pass. Set local field to NULL in logic below
             }
             elseif ($value instanceof $j['fkey'][0]) {
@@ -355,6 +366,7 @@ class VerySimpleModel {
         }
 
         $pk = static::$meta['pk'];
+        $wasnew = $this->__new__;
 
         if ($this->__new__) {
             if (count($pk) == 1)
@@ -362,7 +374,6 @@ class VerySimpleModel {
                 $this->ht[$pk[0]] = $ex->insert_id();
             $this->__new__ = false;
             Signal::send('model.created', $this);
-            $this->__onload();
         }
         else {
             $data = array('dirty' => $this->dirty);
@@ -377,6 +388,8 @@ class VerySimpleModel {
             $self = static::lookup($this->get('pk'));
             $this->ht = $self->ht;
         }
+        if ($wasnew)
+            $this->__onload();
         $this->dirty = array();
         return $this->get($pk[0]);
     }
@@ -452,7 +465,15 @@ class SqlFunction {
     }
 
     function toSql($compiler, $model=false, $alias=false) {
-        return sprintf('%s(%s)%s', $this->func, implode(',', $this->args),
+        $args = array();
+        foreach ($this->args as $A) {
+            if ($A instanceof SqlFunction)
+                $A = $A->toSql($compiler, $model);
+            else
+                $A = $compiler->input($A);
+            $args[] = $A;
+        }
+        return sprintf('%s(%s)%s', $this->func, implode(',', $args),
             $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
     }
 
@@ -470,6 +491,81 @@ class SqlFunction {
     }
 }
 
+class SqlExpression extends SqlFunction {
+    var $operator;
+    var $operands;
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $O = array();
+        foreach ($this->args as $operand) {
+            if ($operand instanceof SqlFunction)
+                $O[] = $operand->toSql($compiler, $model);
+            else
+                $O[] = $compiler->input($operand);
+        }
+        return implode(' '.$this->func.' ', $O)
+            . ($alias ? ' AS '.$compiler->quote($alias) : '');
+    }
+
+    static function __callStatic($operator, $operands) {
+        switch ($operator) {
+            case 'minus':
+                $operator = '-'; break;
+            case 'plus':
+                $operator = '+'; break;
+            case 'times':
+                $operator = '*'; break;
+            default:
+                throw new InvalidArgumentException('Invalid operator specified');
+        }
+        return parent::__callStatic($operator, $operands);
+    }
+}
+
+class SqlInterval extends SqlFunction {
+    var $type;
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $A = $this->args[0];
+        if ($A instanceof SqlFunction)
+            $A = $A->toSql($compiler, $model);
+        else
+            $A = $compiler->input($A);
+        return sprintf('INTERVAL %s %s',
+            $A,
+            $this->func)
+            . ($alias ? ' AS '.$compiler->quote($alias) : '');
+    }
+
+    static function __callStatic($interval, $args) {
+        if (count($args) != 1) {
+            throw new InvalidArgumentException("Interval expects a single interval value");
+        }
+        return parent::__callStatic($interval, $args);
+    }
+}
+
+class SqlField extends SqlFunction {
+    function __construct($field) {
+        $this->field = $field;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        list($field) = $compiler->getField($this->field, $model);
+        return $field;
+    }
+}
+
+class SqlCode extends SqlFunction {
+    function __construct($code) {
+        $this->code = $code;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        return $this->code;
+    }
+}
+
 class Aggregate extends SqlFunction {
 
     var $func;
@@ -505,7 +601,7 @@ class Aggregate extends SqlFunction {
     }
 }
 
-class QuerySet implements IteratorAggregate, ArrayAccess {
+class QuerySet implements IteratorAggregate, ArrayAccess, Serializable {
     var $model;
 
     var $constraints = array();
@@ -516,6 +612,8 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
     var $values = array();
     var $defer = array();
     var $annotations = array();
+    var $extra = array();
+    var $distinct = array();
     var $lock = false;
 
     const LOCK_EXCLUSIVE = 1;
@@ -556,6 +654,12 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
         $this->ordering = array_merge($this->ordering, func_get_args());
         return $this;
     }
+    function getSortFields() {
+        $ordering = $this->ordering;
+        if ($this->extra['order_by'])
+            $ordering = array_merge($ordering, $this->extra['order_by']);
+        return $ordering;
+    }
 
     function lock($how=false) {
         $this->lock = $how ?: self::LOCK_EXCLUSIVE;
@@ -577,8 +681,22 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
         return $this;
     }
 
+    function extra(array $extra) {
+        foreach ($extra as $section=>$info) {
+            $this->extra[$section] = array_merge($this->extra[$section] ?: array(), $info);
+        }
+        return $this;
+    }
+
+    function distinct() {
+        foreach (func_get_args() as $D)
+            $this->distinct[] = $D;
+        return $this;
+    }
+
     function values() {
-        $this->values = func_get_args();
+        foreach (func_get_args() as $A)
+            $this->values[$A] = $A;
         $this->iterator = 'HashArrayIterator';
         // This disables related models
         $this->related = false;
@@ -727,19 +845,36 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
 
         // Load defaults from model
         $model = $this->model;
-        if (!$this->ordering && isset($model::$meta['ordering']))
-            $this->ordering = $model::$meta['ordering'];
-        if (!$this->related && $model::$meta['select_related'])
-            $this->related = $model::$meta['select_related'];
-        if (!$this->defer && $model::$meta['defer'])
-            $this->defer = $model::$meta['defer'];
+        $query = clone $this;
+        if (!$query->ordering && isset($model::$meta['ordering']))
+            $query->ordering = $model::$meta['ordering'];
+        if (!$query->related && $model::$meta['select_related'])
+            $query->related = $model::$meta['select_related'];
+        if (!$query->defer && $model::$meta['defer'])
+            $query->defer = $model::$meta['defer'];
 
         $class = $this->compiler;
         $compiler = new $class($options);
-        $this->query = $compiler->compileSelect($this);
+        $this->query = $compiler->compileSelect($query);
 
         return $this->query;
     }
+
+    function serialize() {
+        $info = get_object_vars($this);
+        unset($info['query']);
+        unset($info['limit']);
+        unset($info['offset']);
+        unset($info['_iterator']);
+        return serialize($info);
+    }
+
+    function unserialize($data) {
+        $data = unserialize($data);
+        foreach ($data as $name => $val) {
+            $this->{$name} = $val;
+        }
+    }
 }
 
 class DoesNotExist extends Exception {}
@@ -857,6 +992,13 @@ class ModelInstanceManager extends ResultSet {
      * database-backed fields are managed by the Model instance.
      */
     function getOrBuild($modelClass, $fields) {
+        // Check for NULL primary key, used with related model fetching. If
+        // the PK is NULL, then consider the object to also be NULL
+        foreach ($modelClass::$meta['pk'] as $pkf) {
+            if (!isset($fields[$pkf])) {
+                return null;
+            }
+        }
         $annotations = $this->queryset->annotations;
         $extras = array();
         // For annotations, drop them from the $fields list and add them to
@@ -922,7 +1064,7 @@ class ModelInstanceManager extends ResultSet {
                     // Build the root model
                     $model = $this->getOrBuild($this->model, $record);
                 }
-                else {
+                elseif ($model) {
                     $i = 0;
                     // Traverse the declared path and link the related model
                     $tail = array_pop($path);
@@ -972,6 +1114,23 @@ class FlatArrayIterator extends ResultSet {
     }
 }
 
+class HashArrayIterator extends ResultSet {
+    function __construct($queryset) {
+        $this->resource = $queryset->getQuery();
+    }
+    function fillTo($index) {
+        while ($this->resource && $index >= count($this->cache)) {
+            if ($row = $this->resource->getArray()) {
+                $this->cache[] = $row;
+            } else {
+                $this->resource->close();
+                $this->resource = null;
+                break;
+            }
+        }
+    }
+}
+
 class InstrumentedList extends ModelInstanceManager {
     var $key;
     var $id;
@@ -1317,10 +1476,22 @@ class SqlCompiler {
         return $this->params;
     }
 
-    function getJoins() {
+    function getJoins($queryset) {
         $sql = '';
         foreach ($this->joins as $j)
             $sql .= $j['sql'];
+        // Add extra items from QuerySet
+        if (isset($queryset->extra['tables'])) {
+            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;
+                }
+                $sql .= $join.$S;
+            }
+        }
         return $sql;
     }
 
@@ -1390,6 +1561,8 @@ class MySqlCompiler extends SqlCompiler {
     static $operators = array(
         'exact' => '%1$s = %2$s',
         'contains' => array('self', '__contains'),
+        'startwith' => array('self', '__startswith'),
+        'endswith' => array('self', '__endswith'),
         'gt' => '%1$s > %2$s',
         'lt' => '%1$s < %2$s',
         'gte' => '%1$s >= %2$s',
@@ -1398,12 +1571,27 @@ class MySqlCompiler extends SqlCompiler {
         'like' => '%1$s LIKE %2$s',
         'hasbit' => '%1$s & %2$s != 0',
         'in' => array('self', '__in'),
+        'intersect' => array('self', '__find_in_set'),
     );
 
+    // Thanks, http://stackoverflow.com/a/3683868
+    function like_escape($what, $e='\\') {
+        return str_replace(array($e, '%', '_'), array($e.$e, $e.'%', $e.'_'), $what);
+    }
+
     function __contains($a, $b) {
         # {%a} like %{$b}%
-        # XXX: Escape $b
-        return sprintf('%s LIKE %s', $a, $this->input($b = "%$b%"));
+        # Escape $b
+        $b = $this->like_escape($b);
+        return sprintf('%s LIKE %s', $a, $this->input("%$b%"));
+    }
+    function __startswith($a, $b) {
+        $b = $this->like_escape($b);
+        return sprintf('%s LIKE %s', $a, $this->input("%$b"));
+    }
+    function __endswith($a, $b) {
+        $b = $this->like_escape($b);
+        return sprintf('%s LIKE %s', $a, $this->input("$b%"));
     }
 
     function __in($a, $b) {
@@ -1423,6 +1611,19 @@ class MySqlCompiler extends SqlCompiler {
             : sprintf('%s IS NOT NULL', $a);
     }
 
+    function __find_in_set($a, $b) {
+        if (is_array($b)) {
+            $sql = array();
+            foreach (array_map(array($this, 'input'), $b) as $b) {
+                $sql[] = sprintf('FIND_IN_SET(%s, %s)', $b, $a);
+            }
+            $parens = count($sql) > 1;
+            $sql = implode(' OR ', $sql);
+            return $parens ? ('('.$sql.')') : $sql;
+        }
+        return sprintf('FIND_IN_SET(%s, %s)', $b, $a);
+    }
+
     function compileJoin($tip, $model, $alias, $info, $extra=false) {
         $constraints = array();
         $join = ' JOIN ';
@@ -1453,7 +1654,11 @@ class MySqlCompiler extends SqlCompiler {
         if ($extra instanceof Q) {
             $constraints[] = $this->compileQ($extra, $model, self::SLOT_JOINS);
         }
-        return $join.$this->quote($rmodel::$meta['table'])
+        // Support inline views
+        $table = ($rmodel::$meta['view'])
+            ? $rmodel::getQuery($this)
+            : $this->quote($rmodel::$meta['table']);
+        return $join.$table
             .' '.$alias.' ON ('.implode(' AND ', $constraints).')';
     }
 
@@ -1476,7 +1681,7 @@ class MySqlCompiler extends SqlCompiler {
      * (string) token to be placed into the compiled SQL statement. For
      * MySQL, this is always the string '?'.
      */
-    function input(&$what, $slot=false) {
+    function input($what, $slot=false) {
         if ($what instanceof QuerySet) {
             $q = $what->getQuery(array('nosort'=>true));
             $this->params = array_merge($q->params);
@@ -1520,6 +1725,11 @@ class MySqlCompiler extends SqlCompiler {
             else
                 $having[] = $C;
         }
+        if (isset($queryset->extra['where'])) {
+            foreach ($queryset->extra['where'] as $S) {
+                $where[] = '('.$S.')';
+            }
+        }
         if ($where)
             $where = ' WHERE '.implode(' AND ', $where);
         if ($having)
@@ -1531,7 +1741,7 @@ class MySqlCompiler extends SqlCompiler {
         $model = $queryset->model;
         $table = $model::$meta['table'];
         list($where, $having) = $this->getWhereHavingClause($queryset);
-        $joins = $this->getJoins();
+        $joins = $this->getJoins($queryset);
         $sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where;
         $exec = new MysqlExecutor($sql, $this->params);
         $row = $exec->getArray();
@@ -1549,15 +1759,20 @@ class MySqlCompiler extends SqlCompiler {
 
         // Compile the ORDER BY clause
         $sort = '';
-        if ($queryset->ordering && !isset($this->options['nosort'])) {
+        if (($columns = $queryset->getSortFields()) && !isset($this->options['nosort'])) {
             $orders = array();
-            foreach ($queryset->ordering as $sort) {
+            foreach ($columns as $sort) {
                 $dir = 'ASC';
-                if ($sort[0] == '-') {
-                    $dir = 'DESC';
-                    $sort = substr($sort, 1);
+                if ($sort instanceof SqlFunction) {
+                    $field = $sort->toSql($this, $model);
+                }
+                else {
+                    if ($sort[0] == '-') {
+                        $dir = 'DESC';
+                        $sort = substr($sort, 1);
+                    }
+                    list($field) = $this->getField($sort, $model);
                 }
-                list($field) = $this->getField($sort, $model);
                 // TODO: Throw exception if $field can be indentified as
                 //       invalid
                 $orders[] = $field.' '.$dir;
@@ -1579,7 +1794,7 @@ class MySqlCompiler extends SqlCompiler {
                 // Handle deferreds
                 if (isset($defer[$f]))
                     continue;
-                $fields[] = $rootAlias . '.' . $this->quote($f);
+                $fields[$rootAlias . '.' . $this->quote($f)] = true;
                 $theseFields[] = $f;
             }
             $fieldMap[] = array($theseFields, $model);
@@ -1602,22 +1817,29 @@ class MySqlCompiler extends SqlCompiler {
                         // Handle deferreds
                         if (isset($defer[$sr . '__' . $f]))
                             continue;
-                        $fields[] = $alias . '.' . $this->quote($f);
+                        elseif (isset($fields[$alias.'.'.$this->quote($f)]))
+                            continue;
+                        $fields[$alias . '.' . $this->quote($f)] = true;
                         $theseFields[] = $f;
                     }
-                    $fieldMap[] = array($theseFields, $fmodel, $parts);
+                    if ($theseFields) {
+                        $fieldMap[] = array($theseFields, $fmodel, $parts);
+                    }
                     $full_path .= '__';
                 }
             }
         }
         // Support retrieving only a list of values rather than a model
         elseif ($queryset->values) {
-            foreach ($queryset->values as $v) {
+            foreach ($queryset->values as $alias=>$v) {
                 list($f) = $this->getField($v, $model);
                 if ($f instanceof SqlFunction)
-                    $fields[] = $f->toSql($this, $model);
-                else
-                    $fields[] = $f;
+                    $fields[$f->toSql($this, $model, $alias)] = true;
+                else {
+                    if (!is_int($alias))
+                        $f .= ' AS '.$this->quote($alias);
+                    $fields[$f] = true;
+                }
             }
         }
         // Simple selection from one table
@@ -1627,13 +1849,14 @@ class MySqlCompiler extends SqlCompiler {
                 foreach ($model::$meta['fields'] as $f) {
                     if (isset($queryset->defer[$f]))
                         continue;
-                    $fields[] = $rootAlias .'.'. $this->quote($f);
+                    $fields[$rootAlias .'.'. $this->quote($f)] = true;
                 }
             }
             else {
-                $fields[] = $rootAlias.'.*';
+                $fields[$rootAlias.'.*'] = true;
             }
         }
+        $fields = array_keys($fields);
         // Add in annotations
         if ($queryset->annotations) {
             foreach ($queryset->annotations as $A) {
@@ -1642,14 +1865,25 @@ class MySqlCompiler extends SqlCompiler {
                 if ($fieldMap)
                     $fieldMap[0][0][] = $A->getAlias();
             }
-            $group_by = array();
             foreach ($model::$meta['pk'] as $pk)
                 $group_by[] = $rootAlias .'.'. $pk;
-            if ($group_by)
-                $group_by = ' GROUP BY '.implode(',', $group_by);
         }
+        // Add in SELECT extras
+        if (isset($queryset->extra['select'])) {
+            foreach ($queryset->extra['select'] as $name=>$expr) {
+                if ($expr instanceof SqlFunction)
+                    $expr = $expr->toSql($this, false, $name);
+                $fields[] = $expr;
+            }
+        }
+        if (isset($queryset->distinct)) {
+            foreach ($queryset->distinct as $d)
+                list($group_by[]) = $this->getField($d, $model);
+        }
+        $group_by = $group_by ? ' GROUP BY '.implode(',', $group_by) : '';
+
+        $joins = $this->getJoins($queryset);
 
-        $joins = $this->getJoins();
         $sql = 'SELECT '.implode(', ', $fields).' FROM '
             .$table.$joins.$where.$group_by.$having.$sort;
         if ($queryset->limit)
@@ -1715,7 +1949,7 @@ class MySqlCompiler extends SqlCompiler {
         $model = $queryset->model;
         $table = $model::$meta['table'];
         list($where, $having) = $this->getWhereHavingClause($queryset);
-        $joins = $this->getJoins();
+        $joins = $this->getJoins($queryset);
         $sql = 'DELETE '.$this->quote($table).'.* FROM '
             .$this->quote($table).$joins.$where;
         return new MysqlExecutor($sql, $this->params);
@@ -1729,7 +1963,7 @@ class MySqlCompiler extends SqlCompiler {
             $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value));
         $set = implode(', ', $set);
         list($where, $having) = $this->getWhereHavingClause($queryset);
-        $joins = $this->getJoins();
+        $joins = $this->getJoins($queryset);
         $sql = 'UPDATE '.$this->quote($table).' SET '.$set.$joins.$where;
         return new MysqlExecutor($sql, $this->params);
     }
@@ -1892,11 +2126,16 @@ class MysqlExecutor {
     }
 
     function __toString() {
-        return $this->sql;
+        $self = $this;
+        $x = 0;
+        return preg_replace_callback('/\?/', function($m) use ($self, &$x) {
+            $p = $self->params[$x++];
+            return db_real_escape($p, is_string($p));
+        }, $this->sql);
     }
 }
 
-class Q {
+class Q implements Serializable {
     const NEGATED = 0x0001;
     const ANY =     0x0002;
 
@@ -1905,7 +2144,7 @@ class Q {
     var $negated = false;
     var $ored = false;
 
-    function __construct($filter, $flags=0) {
+    function __construct($filter=array(), $flags=0) {
         if (!is_array($filter))
             $filter = array($filter);
         $this->constraints = $filter;
@@ -1926,6 +2165,20 @@ class Q {
         return $this;
     }
 
+    function union() {
+        $this->ored = true;
+    }
+
+    function add($constraints) {
+        if (is_array($constraints))
+            $this->constraints = array_merge($this->constraints, $constraints);
+        elseif ($constraints instanceof static)
+            $this->constraints[] = $constraints;
+        else
+            throw new InvalidArgumentException('Expected an instance of Q or an array thereof');
+        return $this;
+    }
+
     static function not(array $constraints) {
         return new static($constraints, self::NEGATED);
     }
@@ -1933,5 +2186,21 @@ class Q {
     static function any(array $constraints) {
         return new static($constraints, self::ANY);
     }
+
+    function serialize() {
+        return serialize(array(
+            'f' =>
+                ($this->negated ? self::NEGATED : 0)
+              | ($this->ored ? self::ANY : 0),
+            'c' => $this->constraints
+        ));
+    }
+
+    function unserialize($data) {
+        $data = unserialize($data);
+        $this->constraints = $data['c'];
+        $this->ored = $data['f'] & self::ANY;
+        $this->negated = $data['f'] & self::NEGATED;
+    }
 }
 ?>
diff --git a/include/class.pagenate.php b/include/class.pagenate.php
index 361d21f89855a4982661ffdf6421bbeae360a203..acec4fb6cacbe6fb6b69d81c4267fcfa2369c507 100644
--- a/include/class.pagenate.php
+++ b/include/class.pagenate.php
@@ -127,5 +127,9 @@ class PageNate {
         return $html;
     }
 
+    function paginate(QuerySet $qs) {
+        return $qs->limit($this->getLimit())->offset($this->getStart());
+    }
+
 }
 ?>
diff --git a/include/class.priority.php b/include/class.priority.php
index 63a7434c479f88075ec1170d2a6410768891a0ef..67bb0d8e33c87ea1a6e1a17bf9c16ba130c7c21e 100644
--- a/include/class.priority.php
+++ b/include/class.priority.php
@@ -14,55 +14,36 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Priority {
+class Priority extends VerySimpleModel {
 
-    var $id;
-    var $ht;
-
-    function Priority($id){
-
-        $this->id =0;
-        $this->load($id);
-    }
-
-    function load($id) {
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-
-        $sql='SELECT *  FROM '.PRIORITY_TABLE
-            .' WHERE priority_id='.db_input($id);
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht= db_fetch_array($res);
-        $this->id= $this->ht['priority_id'];
-
-        return true;;
-    }
+    static $meta = array(
+        'table' => PRIORITY_TABLE,
+        'pk' => array('priority_id'),
+        'ordering' => array('-priority_urgency')
+    );
 
     function getId() {
-        return $this->id;
+        return $this->priority_id;
     }
 
     function getTag() {
-        return $this->ht['priority'];
+        return $this->priority;
     }
 
     function getDesc() {
-        return $this->ht['priority_desc'];
+        return $this->priority_desc;
     }
 
     function getColor() {
-        return $this->ht['priority_color'];
+        return $this->priority_color;
     }
 
     function getUrgency() {
-        return $this->ht['priority_urgency'];
+        return $this->priority_urgency;
     }
 
     function isPublic() {
-        return ($this->ht['ispublic']);
+        return $this->ispublic;
     }
 
     function __toString() {
@@ -70,20 +51,16 @@ class Priority {
     }
 
     /* ------------- Static ---------------*/
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($p=new Priority($id)) && $p->getId()==$id)?$p:null;
-    }
-
-    function getPriorities( $publicOnly=false) {
-
+    static function getPriorities( $publicOnly=false) {
         $priorities=array();
-        $sql ='SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE;
-        if($publicOnly)
-            $sql.=' WHERE ispublic=1';
 
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name)=db_fetch_row($res))
-                $priorities[$id] = $name;
+        $objects = static::objects()->values_flat('priority_id', 'priority_desc');
+        if ($publicOnly)
+            $objects->filter(array('ispublic'=>1));
+
+        foreach ($objects as $row) {
+            list($id, $name) = $row;
+            $priorities[$id] = $name;
         }
 
         return $priorities;
diff --git a/include/class.search.php b/include/class.search.php
index fd8c7e2cff06868b63439c980252d4b4719a71a0..68076e619847a82a9ee9bba0736b98fe3cdd0623 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -31,7 +31,7 @@ abstract class SearchBackend {
     const SORT_OLDEST = 3;
 
     abstract function update($model, $id, $content, $new=false, $attrs=array());
-    abstract function find($query, $criteria, $model=false, $sort=array());
+    abstract function find($query, QuerySet $criteria);
 
     static function register($backend=false) {
         $backend = $backend ?: get_called_class();
@@ -61,9 +61,9 @@ class SearchInterface {
         $this->bootstrap();
     }
 
-    function find($query, $criteria, $model=false, $sort=array()) {
+    function find($query, QuerySet $criteria) {
         $query = Format::searchable($query);
-        return $this->backend->find($query, $criteria, $model, $sort);
+        return $this->backend->find($query, $criteria);
     }
 
     function update($model, $id, $content, $new=false, $attrs=array()) {
@@ -278,7 +278,7 @@ class MysqlSearchBackend extends SearchBackend {
         return implode(' ', $results);
     }
 
-    function find($query, $criteria=array(), $model=false, $sort=array()) {
+    function find($query, QuerySet $criteria) {
         global $thisstaff;
 
         $mode = ' IN BOOLEAN MODE';
@@ -292,63 +292,32 @@ class MysqlSearchBackend extends SearchBackend {
         $P = TABLE_PREFIX;
         $sort = '';
 
-        if ($query) {
-            $tables[] = "(
-                SELECT object_type, object_id, $search AS `relevance`
-                FROM `{$P}_search` `search`
-                WHERE $search
-            ) `search`";
-            $sort = 'ORDER BY `search`.`relevance`';
-        }
-
-        switch ($model) {
+        switch ($criteria->model) {
         case false:
-        case 'Ticket':
-            $tables[] = "(select ticket_id as ticket_id from {$P}ticket
-            ) B1 ON (B1.ticket_id = search.object_id and search.object_type = 'T')";
-            $tables[] = "(select A2.id as thread_id, A1.ticket_id from {$P}ticket A1
-                join {$P}ticket_thread A2 on (A1.ticket_id = A2.ticket_id)
-            ) B2 ON (B2.thread_id = search.object_id and search.object_type = 'H')";
-            $tables[] = "(select A3.id as user_id, A1.ticket_id from {$P}user A3
-                join {$P}ticket A1 on (A1.user_id = A3.id)
-            ) B3 ON (B3.user_id = search.object_id and search.object_type = 'U')";
-            $tables[] = "(select A4.id as org_id, A1.ticket_id from {$P}organization A4
-                join {$P}user A3 on (A3.org_id = A4.id) join {$P}ticket A1 on (A1.user_id = A3.id)
-            ) B4 ON (B4.org_id = search.object_id and search.object_type = 'O')";
-            $key = 'COALESCE(B1.ticket_id, B2.ticket_id, B3.ticket_id, B4.ticket_id)';
-            $tables[] = "{$P}ticket A1 ON (A1.ticket_id = {$key})";
-            $tables[] = "{$P}ticket_status A2 ON (A1.status_id = A2.id)";
-            $cdata_search = false;
-            $where = array();
-
-            if ($criteria) {
-                foreach ($criteria as $name=>$value) {
-                    switch ($name) {
-                    case 'status_id':
-                        $where[] = 'A2.id = '.db_input($value);
-                        break;
-                    case 'state':
-                        $where[] = 'A2.state = '.db_input($value);
-                        break;
-                    case 'state__in':
-                        $where[] = 'A2.state IN ('.implode(',',db_input($value)).')';
-                        break;
-                    case 'topic_id':
-                    case 'staff_id':
-                    case 'team_id':
-                    case 'dept_id':
-                    case 'user_id':
-                    case 'isanswered':
-                    case 'isoverdue':
-                    case 'number':
-                        $where[] = sprintf('A1.%s = %s', $name, db_input($value));
-                        break;
-                    case 'created__gte':
-                        $where[] = sprintf('A1.created >= %s', db_input($value));
-                        break;
-                    case 'created__lte':
-                        $where[] = sprintf('A1.created <= %s', db_input($value));
-                        break;
+        case 'TicketModel':
+            if ($query) {
+            $key = 'COALESCE(Z1.ticket_id, Z2.ticket_id)';
+            $criteria->extra(array(
+                'select' => array(
+                    'key' => $key,
+                    'relevance'=>'`search`.`relevance`',
+                ),
+                'order_by' => array('relevance'),
+                'tables' => array(
+                    "(SELECT object_type, object_id, $search AS `relevance`
+                        FROM `{$P}_search` `search` WHERE $search) `search`",
+                    "(select ticket_id as ticket_id from {$P}ticket
+                ) Z1 ON (Z1.ticket_id = search.object_id and search.object_type = 'T')",
+                    "(select A2.id as thread_id, A1.ticket_id from {$P}ticket A1
+                    join {$P}ticket_thread A2 on (A1.ticket_id = A2.ticket_id)
+                ) Z2 ON (Z2.thread_id = search.object_id and search.object_type = 'H')",
+                )
+            ));
+            // XXX: This is extremely ugly
+            $criteria->filter(array('ticket_id'=>new SqlCode($key)));
+            $criteria->distinct('ticket_id');
+            }
+            /*
                     case 'email':
                     case 'org_id':
                     case 'form_id':
@@ -371,57 +340,17 @@ class MysqlSearchBackend extends SearchBackend {
                         }
                     }
                 }
-            }
-            if ($cdata_search)
-                $tables[] = TABLE_PREFIX.'ticket__cdata cdata'
-                    .' ON (cdata.ticket_id = A1.ticket_id)';
-
-            // Always consider the current staff's access
-            $thisstaff->getDepts();
-            $access = array();
-            $access[] = '(A1.staff_id=' . db_input($thisstaff->getId())
-                .' AND A2.state="open")';
-
-            if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-                $access[] = 'A1.dept_id IN ('
-                    . ($depts ? implode(',', db_input($depts)) : 0)
-                    . ')';
-
-            if (($teams = $thisstaff->getTeams()) && count(array_filter($teams)))
-                $access[] = 'A1.team_id IN ('
-                    .implode(',', db_input(array_filter($teams)))
-                    .') AND A2.state="open"';
-
-            $where[] = '(' . implode(' OR ', $access) . ')';
+             */
 
             // TODO: Consider sorting preferences
-
-            $sql = 'SELECT DISTINCT '
-                . $key
-                . ' FROM '
-                . implode(' LEFT JOIN ', $tables)
-                . ' WHERE ' . implode(' AND ', $where)
-                . $sort
-                . ' LIMIT 500';
         }
 
-        $class = get_class();
-        $auto_create = function($db_error) use ($class) {
-
-            if ($db_error != 1146)
-                // Perform the standard error handling
-                return true;
-
+        // TODO: Ensure search table exists;
+        if (false) {
             // Create the search table automatically
             $class::createSearchTable();
-        };
-        $res = db_query($sql, $auto_create);
-        $object_ids = array();
-
-        while ($row = db_fetch_row($res))
-            $object_ids[] = $row[0];
-
-        return $object_ids;
+        }
+        return $criteria;
     }
 
     static function createSearchTable() {
@@ -629,3 +558,436 @@ Signal::connect('system.install',
         array('MysqlSearchBackend', '__init'));
 
 MysqlSearchBackend::register();
+
+// Saved search system
+
+/**
+ *
+ * Fields:
+ * id - (int:unsigned:auto:pk) unique identifier
+ * flags - (int:unsigned) flags for this queue
+ * staff_id - (int:unsigned) Agent to whom this queue belongs (can be null
+ *      for public saved searches)
+ * title - (text:60) name of the queue
+ * config - (text) JSON encoded search configuration for the queue
+ * created - (date) date initially created
+ * updated - (date:auto_update) time of last update
+ */
+class SavedSearch extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => 'ost_queue', # QUEUE_TABLE
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+    );
+
+    const FLAG_PUBLIC =     0x0001;
+    const FLAG_QUEUE =      0x0002;
+
+    static function forStaff(Staff $agent) {
+        return static::objects()->filter(Q::any(array(
+            'staff_id' => $agent->getId(),
+            'flags__hasbit' => self::FLAG_PUBLIC,
+        )));
+    }
+
+    function getFormFromSession($key, $source=false) {
+        if (isset($_SESSION[$key])) {
+            $source = $source ?: array();
+            $state = $_SESSION[$key];
+            // Pull out 'other' fields from the state so the fields will be
+            // added to the form. The state will be loaded below
+            foreach ($state as $k=>$v) {
+                $info = array();
+                if (!preg_match('/^:(\w+)!(\d+)\+search/', $k, $info)) {
+                    continue;
+                }
+                list($k,) = explode('+', $k, 2);
+                $source['fields'][] = ":{$info[1]}!{$info[2]}";
+            }
+        }
+        return $this->getForm($source);
+    }
+
+    function getForm($source=false) {
+        // XXX: Ensure that the UIDs generated for these fields are
+        //      consistent between requests
+
+        $searchable = $this->getCurrentSearchFields($source);
+        $fields = array(
+            'keywords' => new TextboxField(array(
+                'configuration' => array(
+                    'size' => 40,
+                    'length' => 400,
+                    'classes' => 'full-width headline',
+                    'placeholder' => __('Keywords — Optional'),
+                ),
+            )),
+        );
+        foreach ($searchable as $name=>$field) {
+            $fields = array_merge($fields, self::getSearchField($field, $name));
+        }
+
+        $form = new Form($fields, $source);
+        $form->addValidator(function($form) {
+            $selected = 0;
+            foreach ($form->getFields() as $F) {
+                if (substr($F->get('name'), -7) == '+search' && $F->getClean())
+                    $selected += 1;
+                // Consider keyword searches
+                elseif ($F->get('name') == 'keywords' && $F->getClean())
+                    $selected += 1;
+            }
+            if (!$selected)
+                $form->addError(__('No fields selected for searching'));
+        });
+        return $form;
+    }
+
+    function getCurrentSearchFields($source=false) {
+        $core = array(
+            'state' =>      new TicketStateChoiceField(array(
+                'label' => __('State'),
+            )),
+            'status_id' =>  new TicketStatusChoiceField(array(
+                'label' => __('Status'),
+            )),
+            'flags' =>      new TicketFlagChoiceField(array(
+                'label' => __('Flags'),
+            )),
+            'dept_id'   =>  new DepartmentChoiceField(array(
+                'label' => __('Department'),
+            )),
+            'assignee'  =>  new AssigneeChoiceField(array(
+                'label' => __('Assignee'),
+            )),
+            'topic_id'  =>  new HelpTopicChoiceField(array(
+                'label' => __('Help Topic'),
+            )),
+            'created'   =>  new DateTimeField(array(
+                'label' => __('Created'),
+            )),
+            'duedate'   =>  new DateTimeField(array(
+                'label' => __('Due Date'),
+            )),
+        );
+
+        // Add 'other' fields added dynamically
+        if (is_array($source) && isset($source['fields'])) {
+            foreach ($source['fields'] as $f) {
+                $info = array();
+                if (!preg_match('/^:(\w+)!(\d+)/', $f, $info)) {
+                    continue;
+                }
+                $id = $info[2];
+                if (is_numeric($id) && ($field = DynamicFormField::lookup($id))) {
+                    $impl = $field->getImpl();
+                    $impl->set('label', sprintf('%s / %s',
+                        $field->form->getLocal('title'), $field->getLocal('label')
+                    ));
+                    $core[":{$info[1]}!{$info[2]}"] = $impl;
+                }
+            }
+        }
+
+        return $core;
+    }
+
+    static function getSearchField($field, $name) {
+        $pieces = array();
+        $pieces["{$name}+search"] = new BooleanField(array(
+            'configuration' => array('desc' => $field->get('label'))
+        ));
+        $methods = $field->getSearchMethods();
+        $pieces["{$name}+method"] = new ChoiceField(array(
+            'choices' => $methods,
+            'default' => key($methods),
+            'visibility' => new VisibilityConstraint(new Q(array(
+                "{$name}+search__eq" => true,
+            )), VisibilityConstraint::HIDDEN),
+        ));
+        foreach ($field->getSearchMethodWidgets() as $m=>$w) {
+            if (!$w)
+                continue;
+            list($class, $args) = $w;
+            $args['required'] = true;
+            $args['visibility'] = new VisibilityConstraint(new Q(array(
+                    "{$name}+method__eq" => $m,
+                )), VisibilityConstraint::HIDDEN);
+            $pieces["{$name}+{$m}"] = new $class($args);
+        }
+        return $pieces;
+    }
+
+    function mangleQuerySet(QuerySet $qs, $form=false) {
+        $form = $form ?: $this->getForm();
+        $searchable = $this->getCurrentSearchFields($form->getSource());
+
+        // Figure out fields to search on
+        foreach ($form->getFields() as $f) {
+            if (substr($f->get('name'), -7) == '+search' && $f->getClean()) {
+                $name = substr($f->get('name'), 0, -7);
+                // Determine the search method and fetch the original field
+                if (($M = $form->getField("{$name}+method"))
+                    && ($method = $M->getClean())
+                    && ($field = $searchable[$name])
+                ) {
+                    // Request the field to generate a search Q for the
+                    // search method and given value
+                    $value = null;
+                    if ($value = $form->getField("{$name}+{$method}"))
+                        $value = $value->getClean();
+
+                    if ($name[0] == ':') {
+                        // This was an 'other' field, fetch a special "name"
+                        // for it which will be the ORM join path
+                        static $other_paths = array(
+                            ':ticket' => 'cdata__',
+                            ':user' => 'user__cdata__',
+                            ':organization' => 'user__org__cdata__',
+                        );
+                        $column = $field->get('name') ?: 'field_'.$field->get('id');
+                        list($type,$id) = explode('!', $name, 2);
+                        $OP = $other_paths[$type];
+                        if ($type == ':field') {
+                            $DF = DynamicFormField::lookup($id);
+                            TicketModel::registerCustomData($DF->form);
+                            $OP = 'cdata+'.$DF->form->id.'__';
+                        }
+                        // XXX: Last mile — find a better idea
+                        switch (array($type, $column)) {
+                        case array(':user', 'name'):
+                            $name = 'user__name';
+                            break;
+                        case array(':user', 'email'):
+                            $name = 'user__emails__address';
+                            break;
+                        case array(':organization', 'name'):
+                            $name = 'user__org__name';
+                            break;
+                        default:
+                            $name = $OP . $column;
+                        }
+                    }
+
+                    // Add the criteria to the QuerySet
+                    if ($Q = $field->getSearchQ($method, $value, $name))
+                        $qs = $qs->filter($Q);
+                }
+            }
+        }
+
+        // Consider keyword searching
+        if ($keywords = $form->getField('keywords')->getClean()) {
+            global $ost;
+
+            $qs = $ost->searcher->find($keywords, $qs);
+        }
+
+        return $qs;
+    }
+
+    function checkAccess(Staff $agent) {
+        return $agent->getId() == $this->staff_id
+            || $this->hasFlag(self::FLAG_PUBLIC);
+    }
+
+    protected function hasFlag($flag) {
+        return $this->get('flag') & $flag !== 0;
+    }
+
+    protected function clearFlag($flag) {
+        return $this->set('flag', $this->get('flag') & ~$flag);
+    }
+
+    protected function setFlag($flag) {
+        return $this->set('flag', $this->get('flag') | $flag);
+    }
+
+    static function create($vars=array()) {
+        $inst = parent::create($vars);
+        $inst->created = SqlFunction::NOW();
+        return $inst;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+}
+
+// Advanced search special fields
+
+class HelpTopicChoiceField extends ChoiceField {
+    function hasIdValue() {
+        return true;
+    }
+
+    function getChoices() {
+        return Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
+    }
+}
+
+require_once INCLUDE_DIR . 'class.dept.php';
+class DepartmentChoiceField extends ChoiceField {
+    function getChoices() {
+        return Dept::getDepartments();
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+}
+
+class AssigneeChoiceField extends ChoiceField {
+    function getChoices() {
+        $items = array(
+            'M' => __('Me'),
+            'T' => __('One of my teams'),
+        );
+        foreach (Staff::getStaffMembers() as $id=>$name) {
+            $items['s' . $id] = $name;
+        }
+        foreach (Team::getTeams() as $id=>$name) {
+            $items['t' . $id] = $name;
+        }
+        return $items;
+    }
+
+    function getSearchMethods() {
+        return array(
+            'assigned' =>   __('assigned'),
+            '!assigned' =>  __('unassigned'),
+            'includes' =>   __('includes'),
+            '!includes' =>  __('does not include'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'assigned' => null,
+            '!assigned' => null,
+            'includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+            '!includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        global $thisstaff;
+
+        $Q = new Q();
+        switch ($method) {
+        case 'assigned':
+            $Q->negate();
+        case '!assigned':
+            $Q->add(array('team_id' => 0,
+                'staff_id' => 0));
+            break;
+        case '!includes':
+            $Q->negate();
+        case 'includes':
+            $teams = $agents = array();
+            foreach ($value as $id => $ST) {
+                switch ($id[0]) {
+                case 'M':
+                    $agents[] = $thisstaff->getId();
+                    break;
+                case 's':
+                    $agents[] = (int) substr($id, 1);
+                    break;
+                case 'T':
+                    $teams = array_merge($thisstaff->getTeams());
+                    break;
+                case 't':
+                    $teams[] = (int) substr($id, 1);
+                    break;
+                }
+            }
+            $constraints = array();
+            if ($teams)
+                $constraints['team_id__in'] = $teams;
+            if ($agents)
+                $constraints['staff_id__in'] = $agents;
+            $Q->add(Q::any($constraints));
+        }
+        return $Q;
+    }
+}
+
+class TicketStateChoiceField extends ChoiceField {
+    function getChoices() {
+        return array(
+            'open' => __('Open'),
+            'closed' => __('Closed'),
+            'archived' => __('Archived'),
+            'deleted' => __('Deleted'),
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        return parent::getSearchQ($method, $value, 'status__state');
+    }
+}
+
+class TicketFlagChoiceField extends ChoiceField {
+    function getChoices() {
+        return array(
+            'isanswered' =>   __('Answered'),
+            'isoverdue' =>    __('Overdue'),
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $Q = new Q();
+        if (isset($value['isanswered']))
+            $Q->add(array('isanswered' => 1));
+        if (isset($value['isoverdue']))
+            $Q->add(array('isoverdue' => 1));
+        if ($method == '!includes')
+            $Q->negate();
+        if ($Q->constraints)
+            return $Q;
+    }
+}
+
+class TicketStatusChoiceField extends SelectionField {
+    static $widget = 'ChoicesWidget';
+
+    function getList() {
+        return new TicketStatusList(
+            DynamicList::lookup(
+                array('type' => 'ticket-status'))
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+}
diff --git a/include/class.sla.php b/include/class.sla.php
index 93e39307175c053d345b31b0c597124268b1a029..490508660f54d2e1303bf391d48a8082eb758e05 100644
--- a/include/class.sla.php
+++ b/include/class.sla.php
@@ -14,6 +14,13 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
+class SlaModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => SLA_TABLE,
+        'pk' => array('sla_id'),
+    );
+}
+
 class SLA {
 
     var $id;
diff --git a/include/class.staff.php b/include/class.staff.php
index 33f9cef7f14a870da5d7b5e37a34393c8bbd8a4f..57b35bd02a22ef325a5ab4f6352cb627a4425a86 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -22,61 +22,38 @@ include_once(INCLUDE_DIR.'class.passwd.php');
 include_once(INCLUDE_DIR.'class.user.php');
 include_once(INCLUDE_DIR.'class.auth.php');
 
-class Staff extends AuthenticatedUser {
-
-    var $ht;
-    var $id;
-
-    var $dept;
+class Staff extends VerySimpleModel
+implements AuthenticatedUser {
+
+    static $meta = array(
+        'table' => STAFF_TABLE,
+        'pk' => array('staff_id'),
+        'select_related' => array('group'),
+        'joins' => array(
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.dept_id'),
+            ),
+            'group' => array(
+                'constraint' => array('group_id' => 'Group.group_id'),
+            ),
+            'teams' => array(
+                'constraint' => array('staff_id' => 'StaffTeamMember.staff_id'),
+            ),
+        ),
+    );
+
+    var $authkey;
     var $departments;
-    var $group;
     var $teams;
     var $timezone;
-    var $stats;
+    var $stats = array();
+    var $_extra;
+    var $passwd_change;
 
-    function Staff($var) {
-        $this->id =0;
-        return ($this->load($var));
-    }
-
-    function load($var='') {
-
-        if(!$var && !($var=$this->getId()))
-            return false;
-
-        $sql='SELECT staff.created as added, grp.*, staff.* '
-            .' FROM '.STAFF_TABLE.' staff '
-            .' LEFT JOIN '.GROUP_TABLE.' grp ON(grp.group_id=staff.group_id)
-               WHERE ';
-
-        if (is_numeric($var))
-            $sql .= 'staff_id='.db_input($var);
-        elseif (Validator::is_email($var))
-            $sql .= 'email='.db_input($var);
-        elseif (is_string($var))
-            $sql .= 'username='.db_input($var);
-        else
-            return null;
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return NULL;
-
-
-        $this->ht=db_fetch_array($res);
-        $this->id  = $this->ht['staff_id'];
-        $this->teams = $this->ht['teams'] = array();
-        $this->group = $this->dept = null;
-        $this->departments = $this->stats = array();
-
-        //WE have to patch info here to support upgrading from old versions.
-        if(($time=strtotime($this->ht['passwdreset']?$this->ht['passwdreset']:$this->ht['added'])))
-            $this->ht['passwd_change'] = time()-$time; //XXX: check timezone issues.
-
-        return ($this->id);
-    }
-
-    function reload() {
-        return $this->load();
+    function __onload() {
+        // WE have to patch info here to support upgrading from old versions.
+        if ($time=strtotime($this->passwdreset ?: (isset($this->added) ? $this->added : '')))
+            $this->passwd_change = time()-$time; //XXX: check timezone issues.
     }
 
     function __toString() {
@@ -88,7 +65,9 @@ class Staff extends AuthenticatedUser {
     }
 
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        $base['group'] = $base['group_id'];
+        return $base;
     }
 
     function getInfo() {
@@ -106,6 +85,23 @@ class Staff extends AuthenticatedUser {
         return StaffAuthenticationBackend::getBackend($authkey);
     }
 
+    function setAuthKey($key) {
+        $this->authkey = $key;
+    }
+
+    function getAuthKey() {
+        return $this->authkey;
+    }
+
+    // logOut the user
+    function logOut() {
+
+        if ($bk = $this->getAuthBackend())
+            return $bk->signOut($this);
+
+        return false;
+    }
+
     /*compares user password*/
     function check_passwd($password, $autoupdate=true) {
 
@@ -118,10 +114,9 @@ class Staff extends AuthenticatedUser {
             return false;
 
         //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change.
-        $sql='UPDATE '.STAFF_TABLE.' SET passwd='.db_input(Passwd::hash($password))
-            .' WHERE staff_id='.db_input($this->getId());
+        $this->passwd = Passwd::hash($password);
 
-        if(!$autoupdate || !db_query($sql))
+        if(!$autoupdate || !$this->save())
             $this->forcePasswdRest();
 
         return true;
@@ -132,18 +127,19 @@ class Staff extends AuthenticatedUser {
     }
 
     function hasPassword() {
-        return (bool) $this->ht['passwd'];
+        return (bool) $this->passwd;
     }
 
     function forcePasswdRest() {
-        return db_query('UPDATE '.STAFF_TABLE.' SET change_passwd=1 WHERE staff_id='.db_input($this->getId()));
+        $this->change_passwd = 1;
+        return $this->update();
     }
 
     /* check if passwd reset is due. */
     function isPasswdResetDue() {
         global $cfg;
         return ($cfg && $cfg->getPasswdResetPeriod()
-                    && $this->ht['passwd_change']>($cfg->getPasswdResetPeriod()*30*24*60*60));
+                    && $this->passwd_change>($cfg->getPasswdResetPeriod()*30*24*60*60));
     }
 
     function isPasswdChangeDue() {
@@ -151,81 +147,81 @@ class Staff extends AuthenticatedUser {
     }
 
     function getRefreshRate() {
-        return $this->ht['auto_refresh_rate'];
+        return $this->auto_refresh_rate;
     }
 
     function getPageLimit() {
-        return $this->ht['max_page_size'];
+        return $this->max_page_size;
     }
 
     function getId() {
-        return $this->id;
+        return $this->staff_id;
     }
 
     function getEmail() {
-        return $this->ht['email'];
+        return $this->email;
     }
 
     function getUserName() {
-        return $this->ht['username'];
+        return $this->username;
     }
 
     function getPasswd() {
-        return $this->ht['passwd'];
+        return $this->passwd;
     }
 
     function getName() {
-        return new PersonsName($this->ht['firstname'].' '.$this->ht['lastname']);
+        return new PersonsName($this->firstname.' '.$this->lastname);
     }
 
     function getFirstName() {
-        return $this->ht['firstname'];
+        return $this->firstname;
     }
 
     function getLastName() {
-        return $this->ht['lastname'];
+        return $this->lastname;
     }
 
     function getSignature() {
-        return $this->ht['signature'];
+        return $this->signature;
     }
 
     function getDefaultSignatureType() {
-        return $this->ht['default_signature_type'];
+        return $this->default_signature_type;
     }
 
     function getDefaultPaperSize() {
-        return $this->ht['default_paper_size'];
+        return $this->default_paper_size;
     }
 
     function forcePasswdChange() {
-        return ($this->ht['change_passwd']);
+        return $this->change_passwd;
     }
 
     function getDepartments() {
 
-        if($this->departments)
+        if (isset($this->departments))
             return $this->departments;
 
-        //Departments the staff is "allowed" to access...
+        // Departments the staff is "allowed" to access...
         // based on the group they belong to + user's primary dept + user's managed depts.
-        $sql='SELECT DISTINCT d.dept_id FROM '.STAFF_TABLE.' s '
-            .' LEFT JOIN '.GROUP_DEPT_TABLE.' g ON(s.group_id=g.group_id) '
-            .' INNER JOIN '.DEPT_TABLE.' d ON(d.dept_id=s.dept_id OR d.manager_id=s.staff_id OR d.dept_id=g.dept_id) '
-            .' WHERE s.staff_id='.db_input($this->getId());
-
-        $depts = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id)=db_fetch_row($res))
-                $depts[] = $id;
-        } else { //Neptune help us! (fallback)
-            $depts = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId()));
+        $dept_ids = array();
+        $depts = Dept::objects()
+            ->filter(Q::any(array(
+                'dept_id' => $this->dept_id,
+                'groups__group_id' => $this->group_id,
+                'manager_id' => $this->getId(),
+            )))
+            ->values_flat('dept_id');
+
+        foreach ($depts as $row) {
+            list($id) = $row;
+            $dept_ids[] = $id;
         }
-
-        $this->departments = array_filter(array_unique($depts));
-
-
-        return $this->departments;
+        if (!$dept_ids) { //Neptune help us! (fallback)
+            $dept_ids = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId()));
+        }
+        return $this->departments = array_filter(array_unique($dept_ids));
     }
 
     function getDepts() {
@@ -240,39 +236,31 @@ class Staff extends AuthenticatedUser {
     }
 
     function getGroupId() {
-        return $this->ht['group_id'];
+        return $this->group_id;
     }
 
     function getGroup() {
-
-        if(!$this->group && $this->getGroupId())
-            $this->group = Group::lookup($this->getGroupId());
-
         return $this->group;
     }
 
     function getDeptId() {
-        return $this->ht['dept_id'];
+        return $this->dept_id;
     }
 
     function getDept() {
-
-        if(!$this->dept && $this->getDeptId())
-            $this->dept= Dept::lookup($this->getDeptId());
-
         return $this->dept;
     }
 
     function getLanguage() {
-        return $this->ht['lang'];
+        return $this->lang;
     }
 
     function getTimezone() {
-        return $this->ht['timezone'];
+        return $this->timezone;
     }
 
     function getLocale() {
-        return $this->ht['locale'];
+        return $this->locale;
     }
 
     function isManager() {
@@ -284,19 +272,19 @@ class Staff extends AuthenticatedUser {
     }
 
     function isGroupActive() {
-        return ($this->ht['group_enabled']);
+        return $this->group->group_enabled;
     }
 
     function isactive() {
-        return ($this->ht['isactive']);
+        return $this->isactive;
     }
 
     function isVisible() {
-         return ($this->ht['isvisible']);
+         return $this->isvisible;
     }
 
     function onVacation() {
-        return ($this->ht['onvacation']);
+        return $this->onvacation;
     }
 
     function isAvailable() {
@@ -304,7 +292,7 @@ class Staff extends AuthenticatedUser {
     }
 
     function showAssignedOnly() {
-        return ($this->ht['assigned_only']);
+        return $this->assigned_only;
     }
 
     function isAccessLimited() {
@@ -312,7 +300,7 @@ class Staff extends AuthenticatedUser {
     }
 
     function isAdmin() {
-        return ($this->ht['isadmin']);
+        return $this->isadmin;
     }
 
     function isTeamMember($teamId) {
@@ -324,39 +312,39 @@ class Staff extends AuthenticatedUser {
     }
 
     function canCreateTickets() {
-        return ($this->ht['can_create_tickets']);
+        return $this->group->can_create_tickets;
     }
 
     function canEditTickets() {
-        return ($this->ht['can_edit_tickets']);
+        return $this->group->can_edit_tickets;
     }
 
     function canDeleteTickets() {
-        return ($this->ht['can_delete_tickets']);
+        return $this->group->can_delete_tickets;
     }
 
     function canCloseTickets() {
-        return ($this->ht['can_close_tickets']);
+        return $this->group->can_close_tickets;
     }
 
     function canPostReply() {
-        return ($this->ht['can_post_ticket_reply']);
+        return $this->group->can_post_ticket_reply;
     }
 
     function canViewStaffStats() {
-        return ($this->ht['can_view_staff_stats']);
+        return $this->group->can_view_staff_stats;
     }
 
     function canAssignTickets() {
-        return ($this->ht['can_assign_tickets']);
+        return $this->group->can_assign_tickets;
     }
 
     function canTransferTickets() {
-        return ($this->ht['can_transfer_tickets']);
+        return $this->group->can_transfer_tickets;
     }
 
     function canBanEmails() {
-        return ($this->ht['can_ban_emails']);
+        return $this->group->can_ban_emails;
     }
 
     function canManageTickets() {
@@ -366,7 +354,7 @@ class Staff extends AuthenticatedUser {
     }
 
     function canManagePremade() {
-        return ($this->ht['can_manage_premade']);
+        return $this->group->can_manage_premade;
     }
 
     function canManageCannedResponses() {
@@ -374,7 +362,7 @@ class Staff extends AuthenticatedUser {
     }
 
     function canManageFAQ() {
-        return ($this->ht['can_manage_faq']);
+        return $this->group->can_manage_faq;
     }
 
     function canManageFAQs() {
@@ -382,12 +370,13 @@ class Staff extends AuthenticatedUser {
     }
 
     function showAssignedTickets() {
-        return ($this->ht['show_assigned_tickets']);
+        return $this->show_assigned_tickets;
     }
 
     function getTeams() {
 
-        if(!$this->teams) {
+        if (!isset($this->teams)) {
+            $this->teams = array();
             $sql='SELECT team_id FROM '.TEAM_MEMBER_TABLE
                 .' WHERE staff_id='.db_input($this->getId());
             if(($res=db_query($sql)) && db_num_rows($res))
@@ -422,20 +411,18 @@ class Staff extends AuthenticatedUser {
 
     function getExtraAttr($attr=false, $default=null) {
         if (!isset($this->_extra))
-            $this->_extra = JsonDataParser::decode($this->ht['extra']);
+            $this->_extra = JsonDataParser::decode($this->extra);
 
         return $attr ? (@$this->_extra[$attr] ?: $default) : $this->_extra;
     }
 
     function setExtraAttr($attr, $value, $commit=true) {
         $this->getExtraAttr();
-        $this->extra[$attr] = $value;
+        $this->_extra[$attr] = $value;
 
         if ($commit) {
-            $sql='UPDATE '.STAFF_TABLE.' SET '
-                .'`extra`='.db_input(JsonDataEncoder::encode($this->extra))
-                .' WHERE staff_id='.db_input($this->getId());
-            db_query($sql);
+            $this->extra = JsonDataEncoder::encode($this->_extra);
+            $this->save();
         }
     }
 
@@ -445,12 +432,8 @@ class Staff extends AuthenticatedUser {
             Internationalization::getCurrentLanguage(),
             false);
 
-        $sql='UPDATE '.STAFF_TABLE.' SET '
-            // Update time of last login
-            .'  `lastlogin`=NOW() '
-            .', `extra`='.db_input(JsonDataEncoder::encode($this->extra))
-            .' WHERE staff_id='.db_input($this->getId());
-         db_query($sql);
+        $this->lastlogin = SqlFunction::NOW();
+        $this->save();
     }
 
     //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues
@@ -460,7 +443,7 @@ class Staff extends AuthenticatedUser {
         $vars['firstname']=Format::striptags($vars['firstname']);
         $vars['lastname']=Format::striptags($vars['lastname']);
 
-        if($this->getId()!=$vars['id'])
+        if (isset($this->staff_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Internal error occurred');
 
         if(!$vars['firstname'])
@@ -473,7 +456,8 @@ class Staff extends AuthenticatedUser {
             $errors['email']=__('Valid email is required');
         elseif(Email::getIdByEmail($vars['email']))
             $errors['email']=__('Already in-use as system email');
-        elseif(($uid=Staff::getIdByEmail($vars['email'])) && $uid!=$this->getId())
+        elseif (($uid=static::getIdByEmail($vars['email']))
+                && (!isset($this->staff_id) || $uid!=$this->getId()))
             $errors['email']=__('Email already in-use by another agent');
 
         if($vars['phone'] && !Validator::is_phone($vars['phone']))
@@ -517,154 +501,140 @@ class Staff extends AuthenticatedUser {
         $_SESSION['staff:lang'] = null;
         TextDomain::configureForUser($this);
 
-        $sql='UPDATE '.STAFF_TABLE.' SET updated=NOW() '
-            .' ,firstname='.db_input($vars['firstname'])
-            .' ,lastname='.db_input($vars['lastname'])
-            .' ,email='.db_input($vars['email'])
-            .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"'
-            .' ,phone_ext='.db_input($vars['phone_ext'])
-            .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"'
-            .' ,signature='.db_input(Format::sanitize($vars['signature']))
-            .' ,timezone='.db_input($vars['timezone'])
-            .' ,locale='.db_input($vars['locale'])
-            .' ,show_assigned_tickets='.db_input(isset($vars['show_assigned_tickets'])?1:0)
-            .' ,max_page_size='.db_input($vars['max_page_size'])
-            .' ,auto_refresh_rate='.db_input($vars['auto_refresh_rate'])
-            .' ,default_signature_type='.db_input($vars['default_signature_type'])
-            .' ,default_paper_size='.db_input($vars['default_paper_size'])
-            .' ,lang='.db_input($vars['lang']);
-
-        if($vars['passwd1']) {
-            $sql.=' ,change_passwd=0, passwdreset=NOW(), passwd='.db_input(Passwd::hash($vars['passwd1']));
+        $this->firstname = $vars['firstname'];
+        $this->lastname = $vars['lastname'];
+        $this->email = $vars['email'];
+        $this->phone = Format::phone($vars['phone']);
+        $this->phone_ext = $vars['phone_ext'];
+        $this->mobile = Format::phone($vars['mobile']);
+        $this->signature = Format::sanitize($vars['signature']);
+        $this->timezone = $vars['timezone'];
+        $this->locale = $vars['locale'];
+        $this->show_assigned_tickets = isset($vars['show_assigned_tickets'])?1:0;
+        $this->max_page_size = $vars['max_page_size'];
+        $this->auto_refresh_rate = $vars['auto_refresh_rate'];
+        $this->default_signature_type = $vars['default_signature_type'];
+        $this->default_paper_size = $vars['default_paper_size'];
+        $this->lang = $vars['lang'];
+
+        if ($vars['passwd1']) {
+            $this->change_passwd = 0;
+            $this->passwdreset = SqlFunction::NOW();
+            $this->passwd = Passwd::hash($vars['passwd1']);
             $info = array('password' => $vars['passwd1']);
             Signal::send('auth.pwchange', $this, $info);
             $this->cancelResetTokens();
         }
 
-        $sql.=' WHERE staff_id='.db_input($this->getId());
-
-        //echo $sql;
-
-        return (db_query($sql));
-    }
-
-
-    function updateTeams($teams) {
-
-        if($teams) {
-            foreach($teams as $k=>$id) {
-                $sql='INSERT IGNORE INTO '.TEAM_MEMBER_TABLE.' SET updated=NOW() '
-                    .' ,staff_id='.db_input($this->getId()).', team_id='.db_input($id);
-                db_query($sql);
+        return $this->save();
+    }
+
+    function updateTeams($team_ids) {
+        if ($team_ids && is_array($team_ids)) {
+            $teams = StaffTeamMember::objects()
+                ->filter(array('staff_id' => $this->getId()));
+            foreach ($teams as $member) {
+                if ($idx = array_search($member->team_id, $team_ids)) {
+                    // XXX: Do we really need to track the time of update?
+                    $member->updated = SqlFunction::NOW();
+                    $member->save();
+                    unset($team_ids[$idx]);
+                }
+                else {
+                    $member->delete();
+                }
+            }
+            foreach ($team_ids as $id) {
+                StaffTeamMember::create(array(
+                    'updated'=>SqlFunction::NOW(),
+                    'staff_id'=>$this->getId(), 'team_id'=>$id
+                ))->save();
             }
         }
-
-        $sql='DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($this->getId());
-        if($teams)
-            $sql.=' AND team_id NOT IN('.implode(',', db_input($teams)).')';
-
-        db_query($sql);
-
         return true;
     }
 
-    function update($vars, &$errors) {
+    function delete() {
+        global $thisstaff;
 
-        if(!$this->save($this->getId(), $vars, $errors))
+        if (!$thisstaff || $this->getId() == $thisstaff->getId())
             return false;
 
-        $this->updateTeams($vars['teams']);
-        $this->reload();
+        if (!parent::delete())
+            return false;
 
-        Signal::send('model.modified', $this);
+        // DO SOME HOUSE CLEANING
+        //Move remove any ticket assignments...TODO: send alert to Dept. manager?
+        db_query('UPDATE '.TICKET_TABLE.' SET staff_id=0 WHERE staff_id='.db_input($this->getId()));
 
-        return true;
-    }
+        //Update the poster and clear staff_id on ticket thread table.
+        db_query('UPDATE '.TICKET_THREAD_TABLE
+                .' SET staff_id=0, poster= '.db_input($this->getName()->getOriginal())
+                .' WHERE staff_id='.db_input($this->getId()));
 
-    function delete() {
-        global $thisstaff;
+        // Cleanup Team membership table.
+        TeamMember::objects()
+            ->filter(array('staff_id'=>$this->getId()))
+            ->delete();
 
-        if (!$thisstaff || $this->getId() == $thisstaff->getId())
-            return 0;
-
-        $sql='DELETE FROM '.STAFF_TABLE
-            .' WHERE staff_id = '.db_input($this->getId()).' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            // DO SOME HOUSE CLEANING
-            //Move remove any ticket assignments...TODO: send alert to Dept. manager?
-            db_query('UPDATE '.TICKET_TABLE.' SET staff_id=0 WHERE staff_id='.db_input($this->getId()));
-
-            //Update the poster and clear staff_id on ticket thread table.
-            db_query('UPDATE '.TICKET_THREAD_TABLE
-                    .' SET staff_id=0, poster= '.db_input($this->getName()->getOriginal())
-                    .' WHERE staff_id='.db_input($this->getId()));
-
-            //Cleanup Team membership table.
-            db_query('DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($this->getId()));
-        }
-
-        Signal::send('model.deleted', $this);
-
-        return $num;
+        return true;
     }
 
     /**** Static functions ********/
-    function getStaffMembers($availableonly=false) {
+    static function lookup($var) {
+        if (is_array($var))
+            return parent::lookup($var);
+        elseif (is_numeric($var))
+            return parent::lookup(array('staff_id'=>$var));
+        elseif (Validator::is_email($var))
+            return parent::lookup(array('email'=>$var));
+        elseif (is_string($var))
+            return parent::lookup(array('username'=>$var));
+        else
+            return null;
+    }
+
+    static function getStaffMembers($availableonly=false) {
 
-        $sql='SELECT s.staff_id, CONCAT_WS(" ", s.firstname, s.lastname) as name '
-            .' FROM '.STAFF_TABLE.' s ';
+        $members = static::objects()->order_by('lastname', 'firstname');
 
-        if($availableonly) {
-            $sql.=' INNER JOIN '.GROUP_TABLE.' g ON(g.group_id=s.group_id AND g.group_enabled=1) '
-                 .' WHERE s.isactive=1 AND s.onvacation=0';
+        if ($availableonly) {
+            $members = $members->filter(array(
+                'group__group_enabled' => 1,
+                'onvacation' => 0,
+                'isactive' => 1,
+            ));
         }
 
-        $sql.='  ORDER BY s.lastname, s.firstname';
         $users=array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name) = db_fetch_row($res))
-                $users[$id] = $name;
+        foreach ($members as $M) {
+            $users[$S->id] = $M->getName();
         }
 
         return $users;
     }
 
-    function getAvailableStaffMembers() {
+    static function getAvailableStaffMembers() {
         return self::getStaffMembers(true);
     }
 
-    function getIdByUsername($username) {
-
-        $sql='SELECT staff_id FROM '.STAFF_TABLE.' WHERE username='.db_input($username);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
-
-        return $id;
-    }
-    function getIdByEmail($email) {
-
-        $sql='SELECT staff_id FROM '.STAFF_TABLE.' WHERE email='.db_input($email);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
-
-        return $id;
+    static function getIdByUsername($username) {
+        $row = static::objects()->filter(array('username' => $username))
+            ->values_flat('staff_id')->first();
+        return $row ? $row[0] : 0;
     }
 
-    function lookup($id) {
-        return ($id && ($staff= new Staff($id)) && $staff->getId()) ? $staff : null;
+    function getIdByEmail($email) {
+        $row = static::objects()->filter(array('email' => $email))
+            ->values_flat('staff_id')->first();
+        return $row ? $row[0] : 0;
     }
 
 
-    function create($vars, &$errors) {
-        if(($id=self::save(0, $vars, $errors)) && ($staff=Staff::lookup($id))) {
-            if ($vars['teams'])
-                $staff->updateTeams($vars['teams']);
-            if ($vars['welcome_email'])
-                $staff->sendResetEmail('registration-staff');
-            Signal::send('model.created', $staff);
-        }
-
-        return $id;
+    static function create($vars=false) {
+        $staff = parent::create($vars);
+        $staff->created = SqlFunction::NOW();
+        return $staff;
     }
 
     function cancelResetTokens() {
@@ -717,7 +687,7 @@ class Staff extends AuthenticatedUser {
                 $email->getEmail()
             ), false);
 
-        $lang = $this->ht['lang'] ?: $this->getExtraAttr('browser_lang');
+        $lang = $this->lang ?: $this->getExtraAttr('browser_lang');
         $msg = $ost->replaceTemplateVariables(array(
             'subj' => $content->getLocalName($lang),
             'body' => $content->getLocalBody($lang),
@@ -730,13 +700,19 @@ class Staff extends AuthenticatedUser {
             $msg['body']);
     }
 
-    function save($id, $vars, &$errors) {
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    function update($vars, &$errors) {
 
         $vars['username']=Format::striptags($vars['username']);
         $vars['firstname']=Format::striptags($vars['firstname']);
         $vars['lastname']=Format::striptags($vars['lastname']);
 
-        if($id && $id!=$vars['id'])
+        if (isset($this->staff_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Internal Error');
 
         if(!$vars['firstname'])
@@ -747,14 +723,16 @@ class Staff extends AuthenticatedUser {
         $error = '';
         if(!$vars['username'] || !Validator::is_username($vars['username'], $error))
             $errors['username']=($error) ? $error : __('Username is required');
-        elseif(($uid=Staff::getIdByUsername($vars['username'])) && $uid!=$id)
+        elseif (($uid=static::getIdByUsername($vars['username']))
+                && (!isset($this->staff_id) || $uid!=$this->getId()))
             $errors['username']=__('Username already in use');
 
         if(!$vars['email'] || !Validator::is_email($vars['email']))
             $errors['email']=__('Valid email is required');
         elseif(Email::getIdByEmail($vars['email']))
             $errors['email']=__('Already in use system email');
-        elseif(($uid=Staff::getIdByEmail($vars['email'])) && $uid!=$id)
+        elseif (($uid=static::getIdByEmail($vars['email']))
+                && (!isset($this->staff_id) || $uid!=$this->getId()))
             $errors['email']=__('Email already in use by another agent');
 
         if($vars['phone'] && !Validator::is_phone($vars['phone']))
@@ -784,55 +762,66 @@ class Staff extends AuthenticatedUser {
         if(!$vars['group_id'])
             $errors['group_id']=__('Group is required');
 
-        if($errors) return false;
-
+        if ($errors)
+            return false;
 
-        $sql='SET updated=NOW() '
-            .' ,isadmin='.db_input($vars['isadmin'])
-            .' ,isactive='.db_input($vars['isactive'])
-            .' ,isvisible='.db_input(isset($vars['isvisible'])?1:0)
-            .' ,onvacation='.db_input(isset($vars['onvacation'])?1:0)
-            .' ,assigned_only='.db_input(isset($vars['assigned_only'])?1:0)
-            .' ,dept_id='.db_input($vars['dept_id'])
-            .' ,group_id='.db_input($vars['group_id'])
-            .' ,timezone='.db_input($vars['timezone'])
-            .' ,username='.db_input($vars['username'])
-            .' ,firstname='.db_input($vars['firstname'])
-            .' ,lastname='.db_input($vars['lastname'])
-            .' ,email='.db_input($vars['email'])
-            .' ,backend='.db_input($vars['backend'])
-            .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"'
-            .' ,phone_ext='.db_input($vars['phone_ext'])
-            .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"'
-            .' ,signature='.db_input(Format::sanitize($vars['signature']))
-            .' ,notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($vars['passwd1']) {
-            $sql.=' ,passwd='.db_input(Passwd::hash($vars['passwd1']));
-
-            if(isset($vars['change_passwd']))
-                $sql.=' ,change_passwd=1';
+        $this->isadmin = $vars['isadmin'];
+        $this->isactive = $vars['isactive'];
+        $this->isvisible = isset($vars['isvisible'])?1:0;
+        $this->onvacation = isset($vars['onvacation'])?1:0;
+        $this->assigned_only = isset($vars['assigned_only'])?1:0;
+        $this->dept_id = $vars['dept_id'];
+        $this->group_id = $vars['group_id'];
+        $this->timezone = $vars['timezone'];
+        $this->username = $vars['username'];
+        $this->firstname = $vars['firstname'];
+        $this->lastname = $vars['lastname'];
+        $this->email = $vars['email'];
+        $this->backend = $vars['backend'];
+        $this->phone = Format::phone($vars['phone']);
+        $this->phone_ext = $vars['phone_ext'];
+        $this->mobile = Format::phone($vars['mobile']);
+        $this->signature = Format::sanitize($vars['signature']);
+        $this->notes = Format::sanitize($vars['notes']);
+
+        if ($vars['passwd1']) {
+            $this->passwd = Passwd::hash($vars['passwd1']);
+            if (isset($vars['change_passwd']))
+                $this->change_passwd = 1;
+        }
+        elseif (!isset($vars['change_passwd'])) {
+            $this->change_passwd = 0;
         }
-        elseif (!isset($vars['change_passwd']))
-            $sql .= ' ,change_passwd=0';
 
-        if($id) {
-            $sql='UPDATE '.STAFF_TABLE.' '.$sql.' WHERE staff_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+        if ($this->save() && $this->updateTeams($vars['teams'])) {
+            if ($vars['welcome_email'])
+                $this->sendResetEmail('registration-staff');
+            return true;
+        }
 
+        if (isset($this->staff_id)) {
             $errors['err']=sprintf(__('Unable to update %s.'), __('this agent'))
                .' '.__('Internal error occurred');
         } else {
-            $sql='INSERT INTO '.STAFF_TABLE.' '.$sql.', created=NOW()';
-            if(db_query($sql) && ($uid=db_insert_id()))
-                return $uid;
-
             $errors['err']=sprintf(__('Unable to create %s.'), __('this agent'))
                .' '.__('Internal error occurred');
         }
-
         return false;
     }
 }
+
+class StaffTeamMember extends VerySimpleModel {
+    static $meta = array(
+        'table' => TEAM_MEMBER_TABLE,
+        'pk' => array('staff_id', 'team_id'),
+        'joins' => array(
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+            ),
+            'team' => array(
+                'constraint' => array('team_id' => 'Team.team_id'),
+            ),
+        ),
+    );
+}
 ?>
diff --git a/include/class.team.php b/include/class.team.php
index c0a9b8a1d4c20fba116a535a60e658d427493700..8ba31af52bc5ee2800dae7936875149df6003e5f 100644
--- a/include/class.team.php
+++ b/include/class.team.php
@@ -14,43 +14,23 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Team {
-
-    var $id;
-    var $ht;
+class Team extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => TEAM_TABLE,
+        'pk' => array('team_id'),
+        'joins' => array(
+            'staffmembers' => array(
+                'reverse' => 'StaffTeamMember.team'
+            ),
+            'lead' => array(
+                'constraint' => array('lead_id' => 'Staff.staff_id'),
+            ),
+        ),
+    );
 
     var $members;
 
-    function Team($id) {
-
-        return $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT team.*,count(member.staff_id) as members '
-            .' FROM '.TEAM_TABLE.' team '
-            .' LEFT JOIN '.TEAM_MEMBER_TABLE.' member USING(team_id) '
-            .' WHERE team.team_id='.db_input($id)
-            .' GROUP BY team.team_id ';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['team_id'];
-        $this->members=array();
-
-        return $this->id;
-    }
-
-    function reload() {
-        return $this->load($this->getId());
-    }
-
     function asVar() {
         return $this->__toString();
     }
@@ -60,49 +40,37 @@ class Team {
     }
 
     function getId() {
-        return $this->id;
+        return $this->team_id;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
     }
 
     function getNumMembers() {
-        return $this->ht['members'];
+        return $this->members->count();
     }
 
     function getMembers() {
-
-        if(!$this->members && $this->getNumMembers()) {
-            $sql='SELECT m.staff_id FROM '.TEAM_MEMBER_TABLE.' m '
-                .'LEFT JOIN '.STAFF_TABLE.' s USING(staff_id) '
-                .'WHERE m.team_id='.db_input($this->getId()).' AND s.staff_id IS NOT NULL '
-                .'ORDER BY s.lastname, s.firstname';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    if(($staff= Staff::lookup($id)))
-                        $this->members[]= $staff;
-            }
+        if (!isset($this->members)) {
+            $this->members = Staff::objects()
+                ->filter(array('teams__team_id'=>$this->getId()))
+                ->order_by('lastname', 'firstname');
         }
-
         return $this->members;
     }
 
     function hasMember($staff) {
-        return db_count(
-             'SELECT COUNT(*) FROM '.TEAM_MEMBER_TABLE
-            .' WHERE team_id='.db_input($this->getId())
-            .'   AND staff_id='.db_input($staff->getId())) !== 0;
+        return $this->getMembers()
+            ->filter(array('staff_id'=>$staff->getId()))
+            ->count() !== 0;
     }
 
     function getLeadId() {
-        return $this->ht['lead_id'];
+        return $this->lead_id;
     }
 
     function getTeamLead() {
-        if(!$this->lead && $this->getLeadId())
-            $this->lead=Staff::lookup($this->getLeadId());
-
         return $this->lead;
     }
 
@@ -111,7 +79,9 @@ class Team {
     }
 
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        unset($base['staffmembers']);
+        return $base;
     }
 
     function getInfo() {
@@ -119,7 +89,7 @@ class Team {
     }
 
     function isEnabled() {
-        return ($this->ht['isenabled']);
+        return $this->isenabled;
     }
 
     function isActive() {
@@ -127,11 +97,11 @@ class Team {
     }
 
     function alertsEnabled() {
-        return !$this->ht['noalerts'];
+        return !$this->noalerts;
     }
 
     function getTranslateTag($subtag) {
-        return _H(sprintf('team.%s.%s', $subtag, $this->id));
+        return _H(sprintf('team.%s.%s', $subtag, $this->getId()));
     }
     function getLocal($subtag) {
         $tag = $this->getTranslateTag($subtag);
@@ -144,49 +114,30 @@ class Team {
         return $T != $tag ? $T : $default;
     }
 
-    function update($vars, &$errors) {
-
-        //reset team lead if they're being deleted
-        if($this->getLeadId()==$vars['lead_id']
-                && $vars['remove'] && in_array($this->getLeadId(), $vars['remove']))
-            $vars['lead_id']=0;
-
-        //Save the changes...
-        if(!Team::save($this->getId(), $vars, $errors))
-            return false;
+    function updateMembership($vars) {
 
-        //Delete staff marked for removal...
-        if($vars['remove']) {
-            $sql='DELETE FROM '.TEAM_MEMBER_TABLE
-                .' WHERE team_id='.db_input($this->getId())
-                .' AND staff_id IN ('
-                    .implode(',', db_input($vars['remove']))
-                .')';
-            db_query($sql);
+        // Delete staff marked for removal...
+        if ($vars['remove']) {
+            $this->staffmembers
+                ->filter(array(
+                    'staff_id__in' => $vars['remove']))
+                ->delete();
         }
-
-        //Reload.
-        $this->reload();
-
         return true;
     }
 
     function delete() {
         global $thisstaff;
 
-        if(!$thisstaff || !($id=$this->getId()))
+        if (!$thisstaff || !($id=$this->getId()))
             return false;
 
         # Remove the team
-        $res = db_query(
-            'DELETE FROM '.TEAM_TABLE.' WHERE team_id='.db_input($id)
-          .' LIMIT 1');
-        if (db_affected_rows($res) != 1)
+        if (!parent::delete())
             return false;
 
         # Remove members of this team
-        db_query('DELETE FROM '.TEAM_MEMBER_TABLE
-               .' WHERE team_id='.db_input($id));
+        $this->staffmembers->delete();
 
         # Reset ticket ownership for tickets owned by this team
         db_query('UPDATE '.TICKET_TABLE.' SET team_id=0 WHERE team_id='
@@ -195,91 +146,105 @@ class Team {
         return true;
     }
 
-    /* ----------- Static function ------------------*/
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($team= new Team($id)) && $team->getId()==$id)?$team:null;
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 
+    /* ----------- Static function ------------------*/
 
-    function getIdbyName($name) {
-
-        $sql='SELECT team_id FROM '.TEAM_TABLE.' WHERE name='.db_input($name);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
+    static function getIdbyName($name) {
+        $row = static::objects()
+            ->filter(array('name'=>$name))
+            ->values_flat('team_id')
+            ->first();
 
-        return $id;
+        return $row ? $row[0] : null;
     }
 
-    function getTeams( $availableOnly=false ) {
+    static function getTeams( $availableOnly=false ) {
+        static $names;
+
+        if (isset($names))
+            return $names;
+
+        $names = array();
+        $teams = static::objects()
+            ->values_flat('team_id', 'name', 'isenabled');
 
-        $teams=array();
-        $sql='SELECT team_id, name, isenabled FROM '.TEAM_TABLE;
-        if($availableOnly) {
+        if ($availableOnly) {
             //Make sure the members are active...TODO: include group check!!
-            $sql='SELECT t.team_id, t.name, count(m.staff_id) as members '
-                .' FROM '.TEAM_TABLE.' t '
-                .' LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.team_id=t.team_id) '
-                .' INNER JOIN '.STAFF_TABLE.' s ON(s.staff_id=m.staff_id AND s.isactive=1 AND onvacation=0) '
-                .' INNER JOIN '.GROUP_TABLE.' g ON(g.group_id=s.group_id AND g.group_enabled=1) '
-                .' WHERE t.isenabled=1 '
-                .' GROUP BY t.team_id '
-                .' HAVING members>0'
-                .' ORDER by t.name ';
+            $teams->annotate(array('members'=>Aggregate::COUNT('staffmembers')))
+                ->filter(array(
+                    'isenabled'=>1,
+                    'staffmembers__staff__isactive'=>1,
+                    'staffmembers__staff__onvacation'=>0,
+                    'staffmembers__staff__group__group_enabled'=>1,
+                ))
+                ->filter(array('members__gt'=>0))
+                ->order_by('name');
         }
-        if(($res = db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name, $isenabled) = db_fetch_row($res)) {
-                $teams[$id] = self::getLocalById($id, 'name', $name);
-                if (!$isenabled)
-                    $teams[$id] .= ' ' . __('(disabled)');
-            }
+
+        foreach ($teams as $row) {
+            list($id, $name, $isenabled) = $row;
+            $names[$id] = self::getLocalById($id, 'name', $name);
+            if (!$isenabled)
+                $names[$id] .= ' ' . __('(disabled)');
         }
 
-        return $teams;
+        return $names;
     }
 
-    function getActiveTeams() {
+    static function getActiveTeams() {
         return self::getTeams(true);
     }
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+    static function create($vars=array()) {
+        $team = parent::create($vars);
+        $team->created = SqlFunction::NOW();
+        return $team;
     }
 
-    function save($id, $vars, &$errors) {
-        if($id && $vars['id']!=$id)
+    function update($vars, &$errors) {
+        if (isset($this->team_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Missing or invalid team');
 
         if(!$vars['name']) {
             $errors['name']=__('Team name is required');
         } elseif(strlen($vars['name'])<3) {
             $errors['name']=__('Team name must be at least 3 chars.');
-        } elseif(($tid=Team::getIdByName($vars['name'])) && $tid!=$id) {
+        } elseif(($tid=static::getIdByName($vars['name']))
+                && (!isset($this->team_id) || $tid!=$this->getId())) {
             $errors['name']=__('Team name already exists');
         }
 
-        if($errors) return false;
+        if ($errors)
+            return false;
 
-        $sql='SET updated=NOW(),isenabled='.db_input($vars['isenabled']).
-             ',name='.db_input($vars['name']).
-             ',noalerts='.db_input(isset($vars['noalerts'])?$vars['noalerts']:0).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
+        $this->isenabled = $vars['isenabled'];
+        $this->name = $vars['name'];
+        $this->noalerts = isset($vars['noalerts'])?$vars['noalerts']:0;
+        $this->notes = Format::sanitize($vars['notes']);
+        if (isset($vars['lead_id']))
+            $this->lead_id = $vars['lead_id'];
 
-        if($id) {
-            $sql='UPDATE '.TEAM_TABLE.' '.$sql.',lead_id='.db_input($vars['lead_id']).' WHERE team_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+        // reset team lead if they're being removed from the team
+        if ($this->getLeadId() == $vars['lead_id']
+                && $vars['remove'] && in_array($this->getLeadId(), $vars['remove']))
+            $this->lead_id = 0;
 
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this team'))
-               .' '.__('Internal error occurred');
-        } else {
-            $sql='INSERT INTO '.TEAM_TABLE.' '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
+        if ($this->save())
+            return $this->updateMembership($vars);
 
+        if ($this->__new__) {
             $errors['err']=sprintf(__('Unable to create %s.'), __('this team'))
                .' '.__('Internal error occurred');
         }
-
+        else {
+            $errors['err']=sprintf(__('Unable to update %s.'), __('this team'))
+               .' '.__('Internal error occurred');
+        }
         return false;
     }
 }
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 807ea26819b0299bd2f630086363a969cd04f0cb..7caac30d9b936543bcf5df2ff6ed503cc01fc9c9 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -34,6 +34,123 @@ require_once(INCLUDE_DIR.'class.user.php');
 require_once(INCLUDE_DIR.'class.collaborator.php');
 require_once(INCLUDE_DIR.'class.faq.php');
 
+class TicketModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TICKET_TABLE,
+        'pk' => array('ticket_id'),
+        'joins' => array(
+            'user' => array(
+                'constraint' => array('user_id' => 'User.id')
+            ),
+            'status' => array(
+                'constraint' => array('status_id' => 'TicketStatus.id')
+            ),
+            'lock' => array(
+                'reverse' => 'TicketLock.ticket',
+                'list' => false,
+                'null' => true,
+            ),
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.dept_id'),
+            ),
+            'sla' => array(
+                'constraint' => array('sla_id' => 'SlaModel.id'),
+                'null' => true,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+                'null' => true,
+            ),
+            'team' => array(
+                'constraint' => array('team_id' => 'Team.team_id'),
+                'null' => true,
+            ),
+            'topic' => array(
+                'constraint' => array('topic_id' => 'Topic.topic_id'),
+                'null' => true,
+            ),
+            'cdata' => array(
+                'reverse' => 'TicketCData.ticket',
+                'list' => false,
+            ),
+        )
+    );
+
+    function getId() {
+        return $this->ticket_id;
+    }
+
+    function getEffectiveDate() {
+         return Format::datetime(max(
+             strtotime($this->lastmessage),
+             strtotime($this->closed),
+             strtotime($this->reopened),
+             strtotime($this->created)
+         ));
+    }
+
+    function delete() {
+
+        if (($ticket=Ticket::lookup($this->getId())) && @$ticket->delete())
+            return true;
+
+        return false;
+    }
+
+    static function registerCustomData(DynamicForm $form) {
+        if (!isset(static::$meta['joins']['cdata+'.$form->id])) {
+            $cdata_class = <<<EOF
+class DynamicForm{$form->id} extends DynamicForm {
+    static function getInstance() {
+        static \$instance;
+        if (!isset(\$instance))
+            \$instance = static::lookup({$form->id});
+        return \$instance;
+    }
+}
+class TicketCdataForm{$form->id} {
+    static \$meta = array(
+        'view' => true,
+        'pk' => array('ticket_id'),
+        'joins' => array(
+            'ticket' => array(
+                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
+            ),
+        )
+    );
+    static function getQuery(\$compiler) {
+        return '('.DynamicForm{$form->id}::getCrossTabQuery('T', 'ticket_id').')';
+    }
+}
+EOF;
+            eval($cdata_class);
+            static::$meta['joins']['cdata+'.$form->id] = array(
+                'reverse' => 'TicketCdataForm'.$form->id.'.ticket',
+                'null' => true,
+            );
+            // This may be necessary if the model has already been inspected
+            if (static::$meta instanceof ModelMeta)
+                static::$meta->processJoin(static::$meta['joins']['cdata+'.$form->id]);
+        }
+    }
+}
+
+class TicketCData extends VerySimpleModel {
+    static $meta = array(
+        'pk' => array('ticket_id'),
+        'joins' => array(
+            'ticket' => array(
+                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
+            ),
+            ':priority' => array(
+                'constraint' => array('priority' => 'Priority.priority_id'),
+                'null' => true,
+            ),
+        ),
+    );
+}
+TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';
+
 
 class Ticket {
 
@@ -66,8 +183,6 @@ class Ticket {
             return false;
 
         $sql='SELECT  ticket.*, lock_id, dept_name '
-            .' ,IF(sla.id IS NULL, NULL, '
-                .'DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate '
             .' ,count(distinct attach.attach_id) as attachments'
             .' FROM '.TICKET_TABLE.' ticket '
             .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '
@@ -166,7 +281,7 @@ class Ticket {
     }
 
     function isLocked() {
-        return ($this->getLockId());
+        return null !== $this->getLock();
     }
 
     function checkStaffAccess($staff) {
@@ -297,7 +412,22 @@ class Ticket {
     }
 
     function getSLADueDate() {
-        return $this->ht['sla_duedate'];
+        if ($sla = $this->getSLA()) {
+            $dt = new DateTime($this->getCreateDate());
+
+            return $dt
+                ->add(new DateInterval('PT' . $sla->getGracePeriod() . 'H'))
+                ->format('Y-m-d H:i:s');
+        }
+    }
+
+    function updateEstDueDate() {
+        $estimatedDueDate = $this->getEstDueDate();
+        if ($estimatedDueDate != $this->ht['est_duedate']) {
+            $sql = 'UPDATE '.TICKET_TABLE.' SET `est_duedate`='.db_input($estimatedDueDate)
+                .' WHERE `ticket_id`='.db_input($this->getId());
+            db_query($sql);
+        }
     }
 
     function getEstDueDate() {
@@ -393,15 +523,7 @@ class Ticket {
         return $info;
     }
 
-    function getLockId() {
-        return $this->ht['lock_id'];
-    }
-
     function getLock() {
-
-        if(!$this->tlock && $this->getLockId())
-            $this->tlock= TicketLock::lookup($this->getLockId(), $this->getId());
-
         return $this->tlock;
     }
 
@@ -421,10 +543,9 @@ class Ticket {
             return $lock;
         }
         //No lock on the ticket or it is expired
-        $this->tlock = null; //clear crap
-        $this->ht['lock_id'] = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock..
+        $this->tlock = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock..
         //load and return the newly created lock if any!
-        return $this->getLock();
+        return $this->tlock;
     }
 
     function getDept() {
@@ -774,10 +895,15 @@ class Ticket {
 
     function setSLAId($slaId) {
         if ($slaId == $this->getSLAId()) return true;
-        return db_query(
+        $rv = db_query(
              'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId)
             .' WHERE ticket_id='.db_input($this->getId()))
             && db_affected_rows();
+        if ($rv) {
+            $this->ht['sla_id'] = $slaId;
+            $this->sla = null;
+        }
+        return $rv;
     }
     /**
      * Selects the appropriate service-level-agreement plan for this ticket.
@@ -844,7 +970,7 @@ class Ticket {
         $ecb = null;
         switch($status->getState()) {
             case 'closed':
-                $sql.=', closed=NOW(), duedate=NULL ';
+                $sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL ';
                 if ($thisstaff)
                     $sql.=', staff_id='.db_input($thisstaff->getId());
 
@@ -857,7 +983,7 @@ class Ticket {
             case 'open':
                 // TODO: check current status if it allows for reopening
                 if ($this->isClosed()) {
-                    $sql .= ',closed=NULL, reopened=NOW() ';
+                    $sql .= ',closed=NULL, lastupdate=NOW(), reopened=NOW() ';
                     $ecb = function ($t) {
                         $t->logEvent('reopened', 'closed');
                     };
@@ -1135,7 +1261,7 @@ class Ticket {
     function onMessage($message, $autorespond=true) {
         global $cfg;
 
-        db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
+        db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastupdate=NOW(),lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
 
         // Auto-assign to closing staff or last respondent
         // If the ticket is closed and auto-claim is not enabled then put the
@@ -2147,10 +2273,14 @@ class Ticket {
                 && (!$this->getSLA() || $this->getSLA()->isTransient()))
             $this->selectSLAId();
 
+        // Update estimated due date in database
+        $estimatedDueDate = $this->getEstDueDate();
+        $this->updateEstDueDate();
+
         // Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue.
         if($this->isOverdue()
-                && (!$this->getEstDueDate() //Duedate + SLA cleared
-                    || Misc::db2gmtime($this->getEstDueDate()) > Misc::gmtime() //New due date in the future.
+                && (!$estimatedDueDate //Duedate + SLA cleared
+                    || Misc::db2gmtime($estimatedDueDate) > Misc::gmtime() //New due date in the future.
                     )) {
             $this->clearOverdue();
         }
@@ -2235,7 +2365,7 @@ class Ticket {
             $where[] = 'ticket.dept_id IN('.implode(',', db_input($depts)).') ';
 
         if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets()))
-            $where2 =' AND ticket.staff_id=0 ';
+            $where2 =' AND (ticket.staff_id=0 OR ticket.team_id=0)';
         $where = implode(' OR ', $where);
         if ($where) $where = 'AND ( '.$where.' ) ';
 
@@ -2253,7 +2383,7 @@ class Ticket {
                     ON (ticket.status_id=status.id
                             AND status.state=\'open\') '
                 .'WHERE ticket.isanswered = 1 '
-                . $where
+                . $where . ($cfg->showAnsweredTickets() ? $where2 : '')
 
                 .'UNION SELECT \'overdue\', count( ticket.ticket_id ) AS tickets '
                 .'FROM ' . TICKET_TABLE . ' ticket '
@@ -2618,6 +2748,7 @@ class Ticket {
         //We are ready son...hold on to the rails.
         $number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber();
         $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
+            .' ,lastupdate= NOW() '
             .' ,lastmessage= NOW()'
             .' ,user_id='.db_input($user->getId())
             .' ,`number`='.db_input($number)
@@ -2697,6 +2828,9 @@ class Ticket {
                 $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'));
         }
 
+        // Update the estimated due date in the database
+        $this->updateEstDueDate();
+
         /**********   double check auto-response  ************/
         //Override auto responder if the FROM email is one of the internal emails...loop control.
         if($autorespond && (Email::getIdByEmail($ticket->getEmail())))
diff --git a/include/class.topic.php b/include/class.topic.php
index 2db12f15149955b4a485e3724e383084d24354a8..ba9e1a82788c2d83c345f28f4b5f52b52de8ffe5 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -15,6 +15,7 @@
 **********************************************************************/
 
 require_once INCLUDE_DIR . 'class.sequence.php';
+require_once INCLUDE_DIR . 'class.filter.php';
 
 class Topic extends VerySimpleModel {
 
@@ -291,6 +292,15 @@ class Topic extends VerySimpleModel {
                 return $T != $tag ? $T : $default;
             };
 
+            $localize_this = function($id, $default) use ($localize) {
+                if (!$localize)
+                    return $default;
+
+                $tag = _H("topic.name.{$id}");
+                $T = CustomDataTranslation::translate($tag);
+                return $T != $tag ? $T : $default;
+            };
+
             // Resolve parent names
             foreach ($topics as $id=>$info) {
                 $name = $localize_this($id, $info['topic']);
diff --git a/include/class.user.php b/include/class.user.php
index 5c927b3f15d534e6f37472287ba0d899b9864829..aa016cd00ef53350f3d7d52480d208f55c62be4a 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -16,6 +16,7 @@
 **********************************************************************/
 require_once INCLUDE_DIR . 'class.orm.php';
 require_once INCLUDE_DIR . 'class.util.php';
+require_once INCLUDE_DIR . 'class.organization.php';
 
 class UserEmailModel extends VerySimpleModel {
     static $meta = array(
@@ -33,37 +34,11 @@ class UserEmailModel extends VerySimpleModel {
     }
 }
 
-class TicketModel extends VerySimpleModel {
-    static $meta = array(
-        'table' => TICKET_TABLE,
-        'pk' => array('ticket_id'),
-        'joins' => array(
-            'user' => array(
-                'constraint' => array('user_id' => 'UserModel.id')
-            ),
-            'status' => array(
-                'constraint' => array('status_id' => 'TicketStatus.id')
-            )
-        )
-    );
-
-    function getId() {
-        return $this->ticket_id;
-    }
-
-    function delete() {
-
-        if (($ticket=Ticket::lookup($this->getId())) && @$ticket->delete())
-            return true;
-
-        return false;
-    }
-}
-
 class UserModel extends VerySimpleModel {
     static $meta = array(
         'table' => USER_TABLE,
         'pk' => array('id'),
+        'select_related' => array('default_email'),
         'joins' => array(
             'emails' => array(
                 'reverse' => 'UserEmailModel.user',
@@ -83,17 +58,15 @@ class UserModel extends VerySimpleModel {
                 'null' => true,
                 'constraint' => array('default_email_id' => 'UserEmailModel.id')
             ),
+            'cdata' => array(
+                'constraint' => array('id' => 'UserCdata.user_id'),
+                'null' => true,
+            ),
         )
     );
 
     const PRIMARY_ORG_CONTACT   = 0x0001;
 
-    static function objects() {
-        $qs = parent::objects();
-        #$qs->select_related('default_email');
-        return $qs;
-    }
-
     function getId() {
         return $this->id;
     }
@@ -152,6 +125,20 @@ class UserModel extends VerySimpleModel {
     }
 }
 
+class UserCdata extends VerySimpleModel {
+    static $meta = array(
+        'table' => 'user__cdata',
+        'view' => true,
+        'pk' => array('user_id'),
+    );
+
+    static function getQuery($compiler) {
+        $form = UserForm::getUserForm();
+        $exclude = array('name', 'email');
+        return '('.$form->getCrossTabQuery($form->type, 'user_id', $exclude).')';
+    }
+}
+
 class User extends UserModel {
 
     var $_entries;
@@ -1163,8 +1150,4 @@ class UserList extends ListObject {
         return $list ? implode(', ', $list) : '';
     }
 }
-
-require_once(INCLUDE_DIR . 'class.organization.php');
-User::_inspect();
-UserAccount::_inspect();
 ?>
diff --git a/include/class.usersession.php b/include/class.usersession.php
index 9e7fd277baf58d59b39ade1255bd29a1308493a4..1d0b8e0b1862b9c2ddfadac651074415ce6127b7 100644
--- a/include/class.usersession.php
+++ b/include/class.usersession.php
@@ -161,10 +161,12 @@ class StaffSession extends Staff {
     var $session;
     var $token;
 
-    function __construct($var) {
-        parent::__construct($var);
-        $this->token = &$_SESSION[':token']['staff'];
-        $this->session= new UserSession($this->getId());
+    static function lookup($var) {
+        if ($staff = parent::lookup($var)) {
+            $staff->token = &$_SESSION[':token']['staff'];
+            $staff->session= new UserSession($staff->getId());
+        }
+        return $staff;
     }
 
     function isValid(){
diff --git a/include/client/header.inc.php b/include/client/header.inc.php
index 7d7652f5e9ff983d2ce9f9017e8e88a22f17b0ed..1c3f00dbd0cb09990c64306267527b75a5a67181 100644
--- a/include/client/header.inc.php
+++ b/include/client/header.inc.php
@@ -33,15 +33,16 @@ if (($lang = Internationalization::getCurrentLanguage())
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/flags.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/rtl.css"/>
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/chosen.min.css">
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script>
     <script src="<?php echo ROOT_PATH; ?>js/osticket.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
     <script src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-fonts.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/chosen.jquery.min.js"></script>
     <?php
     if($ost && ($headers=$ost->getExtraHeaders())) {
         echo "\n\t".implode("\n\t", $headers)."\n";
diff --git a/include/client/profile.inc.php b/include/client/profile.inc.php
index e981769e4eed2719ad5114af068b36241c771a25..553005bd994f947cdb7baaeb80c3184558daef61 100644
--- a/include/client/profile.inc.php
+++ b/include/client/profile.inc.php
@@ -25,13 +25,13 @@ if ($acct = $thisclient->getAccount()) {
             <?php echo __('Time Zone');?>:
         </td>
         <td>
-            <select name="timezone" multiple="multiple" id="timezone-dropdown">
+            <select name="timezone" class="chosen-select" id="timezone-dropdown">
                 <option value=""><?php echo __('System Default'); ?></option>
 <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
                 <option value="<?php echo $zone; ?>" <?php
                 if ($info['timezone'] == $zone)
                     echo 'selected="selected"';
-                ?>><?php echo $zone; ?></option>
+                ?>><?php echo str_replace('/', ' / ', $zone); ?></option>
 <?php } ?>
             </select>
             <div class="error"><?php echo $errors['timezone']; ?></div>
@@ -101,17 +101,10 @@ $selected = ($info['lang'] == $l['code']) ? 'selected="selected"' : ''; ?>
         window.location.href='index.php';"/>
 </p>
 </form>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.css"/>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.filter.css"/>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>/js/jquery.multiselect.filter.min.js"></script>
 <script type="text/javascript">
-$('#timezone-dropdown').multiselect({
-    multiple: false,
+$('#timezone-dropdown').chosen({
     header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
-    noneSelectedText: <?php echo JsonDataEncoder::encode(__('System Default')); ?>,
-    selectedList: 1,
-    minWidth: 400
-}).multiselectfilter({
-    placeholder: <?php echo JsonDataEncoder::encode(__('Search')); ?>
+    allow_single_deselect: true,
+    width: '350px'
 });
 </script>
diff --git a/include/client/register.inc.php b/include/client/register.inc.php
index c1923685fb82c9b0a2017a726120ea1cde1d2c86..deb3c5d978fef96242b55eb22faa350f771ae8b2 100644
--- a/include/client/register.inc.php
+++ b/include/client/register.inc.php
@@ -40,13 +40,14 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info);
             <?php echo __('Time Zone');?>:
         </td>
         <td>
-            <select name="timezone" multiple="multiple" id="timezone-dropdown">
-                <option value=""><?php echo __('System Default'); ?></option>
+            <select name="timezone" class="chosen-select" id="timezone-dropdown"
+                data-placeholder="<?php echo __('System Default'); ?>">
+                <option value=""></option>
 <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
                 <option value="<?php echo $zone; ?>" <?php
                 if ($info['timezone'] == $zone)
                     echo 'selected="selected"';
-                ?>><?php echo $zone; ?></option>
+                ?>><?php echo str_replace('/',' / ',$zone); ?></option>
 <?php } ?>
             </select>
             <div class="error"><?php echo $errors['timezone']; ?></div>
@@ -102,18 +103,10 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info);
         window.location.href='index.php';"/>
 </p>
 </form>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.css"/>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.filter.css"/>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>/js/jquery.multiselect.filter.min.js"></script>
 <script type="text/javascript">
-$('#timezone-dropdown').multiselect({
-    multiple: false,
-    header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
-    noneSelectedText: <?php echo JsonDataEncoder::encode(__('System Default')); ?>,
-    selectedList: 1,
-    minWidth: 400
-}).multiselectfilter({
-    placeholder: <?php echo JsonDataEncoder::encode(__('Search')); ?>
+$('#timezone-dropdown').chosen({
+    allow_single_deselect: true,
+    width: '350px'
 });
 </script>
 <?php if (!isset($info['timezone'])) { ?>
@@ -122,8 +115,7 @@ $('#timezone-dropdown').multiselect({
 <script type="text/javascript">
 $(function() {
     var zone = jstz.determine();
-    $('#timezone-dropdown').multiselect('widget').find('[value="' + zone.name() + '"]')
-        .each(function() { console.log(this); $(this).click(); });
+    $('#timezone-dropdown').val(zone.name()).trigger('chosen:updated');
 });
 </script>
 <?php }
diff --git a/include/client/templates/inline-form.tmpl.php b/include/client/templates/inline-form.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..696e1d7cf76c0ab2c1cb824aa25cd6118764a29d
--- /dev/null
+++ b/include/client/templates/inline-form.tmpl.php
@@ -0,0 +1,24 @@
+<div><?php
+foreach ($form->getFields() as $field) { ?>
+    <span style="display:inline-block;padding-right:5px;vertical-align:top">
+        <span class="<?php if ($field->get('required')) echo 'required'; ?>">
+            <?php echo Format::htmlchars($field->get('label')); ?></span>
+        <div><?php
+        $field->render(); ?>
+        <?php if ($field->get('required')) { ?>
+            <span class="error">*</span>
+        <?php
+        }
+        if ($field->get('hint') && !$field->isBlockLevel()) { ?>
+            <br/><em style="color:gray;display:inline-block"><?php
+                echo Format::htmlchars($field->get('hint')); ?></em>
+        <?php
+        }
+        foreach ($field->errors() as $e) { ?>
+            <br />
+            <span class="error"><?php echo Format::htmlchars($e); ?></span>
+        <?php } ?>
+        </div>
+    </span><?php
+} ?>
+</div>
diff --git a/include/pear/Mail.php b/include/pear/Mail.php
index 75132ac2a6c3e9d99bd1784feb41154f0cd71d3d..5d4d3b09dd61c14a501cc52cb8d2f9f1499bb98a 100644
--- a/include/pear/Mail.php
+++ b/include/pear/Mail.php
@@ -74,14 +74,16 @@ class Mail
     function &factory($driver, $params = array())
     {
         $driver = strtolower($driver);
-        @include_once 'Mail/' . $driver . '.php';
         $class = 'Mail_' . $driver;
+        if (!class_exists($class))
+            include_once PEAR_DIR.'Mail/' . $driver . '.php';
+
         if (class_exists($class)) {
             $mailer = new $class($params);
             return $mailer;
-        } else {
-            return PEAR::raiseError('Unable to find class for driver ' . $driver);
         }
+
+        return PEAR::raiseError('Unable to find class for driver ' . $driver);
     }
 
     /**
diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php
index 7315a7fa1667a3fb319b222edb224d2ca02857b3..5e32630d014942c0a4d2cad5b6a5629371dac19f 100644
--- a/include/staff/faq.inc.php
+++ b/include/staff/faq.inc.php
@@ -77,6 +77,7 @@ if ($topics = Topic::getAllHelpTopics()) {
         <div class="faded"><?php echo __('Check all help topics related to this FAQ.');?></div>
     </div>
     <select multiple="multiple" name="topics[]" class="multiselect"
+        data-placeholder="<?php echo __('Help Topics'); ?>"
         id="help-topic-selection" style="width:350px;">
     <?php while (list($topicId,$topic) = each($topics)) { ?>
         <option value="<?php echo $topicId; ?>" <?php
@@ -85,9 +86,7 @@ if ($topics = Topic::getAllHelpTopics()) {
     <?php } ?>
     </select>
     <script type="text/javascript">
-        $(function() { $("#help-topic-selection").multiselect({
-            noneSelectedText: '<?php echo __('Help Topics'); ?>'});
-         });
+        $(function() { $("#help-topic-selection").chosen(); });
     </script>
 <?php } ?>
     </div>
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index 3f7e4d1c72a65bf8942385cc04c13b2e09854368..ba294007755d41549bf35c9806bf32215f6df1e8 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -23,7 +23,7 @@ if (($lang = Internationalization::getCurrentLanguage())
     <script type="text/javascript" src="./js/scp.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/chosen.jquery.min.js"></script>
     <script type="text/javascript" src="./js/tips.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>
@@ -43,6 +43,7 @@ if (($lang = Internationalization::getCurrentLanguage())
     <link type="text/css" rel="stylesheet" href="./css/dropdown.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/loadingbar.css"/>
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/flags.css">
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/chosen.min.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/rtl.css"/>
     <link type="text/css" rel="stylesheet" href="./css/translatable.css"/>
     <script type="text/javascript" src="./js/jquery.dropdown.js"></script>
diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php
index 027c5b48974f12d8de33e3960a4fbd962e667444..73db1e02235e16af3f67678d704d41a98c03908d 100644
--- a/include/staff/helptopic.inc.php
+++ b/include/staff/helptopic.inc.php
@@ -45,7 +45,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
             <td>
                 <input type="text" size="30" name="topic" value="<?php echo $info['topic']; ?>"
-                data-primary-lang="<?php echo $cfg->getPrimaryLanguage(); ?>"
                 data-translate-tag="<?php echo $trans['name']; ?>"/>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['topic']; ?></span> <i class="help-tip icon-question-sign" href="#topic"></i>
             </td>
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php
index b46c33559b8387600b1169e26ebd6e3e0990bb58..665cf53d533126179e02726f512e434cb0280f3d 100644
--- a/include/staff/profile.inc.php
+++ b/include/staff/profile.inc.php
@@ -85,13 +85,14 @@ $info['id']=$staff->getId();
                 <?php echo __('Time Zone');?>:
             </td>
             <td>
-                <select name="timezone" multiple="multiple" id="timezone-dropdown">
-                    <option value=""><?php echo __('System Default'); ?></option>
+                <select name="timezone" class="chosen-select" id="timezone-dropdown"
+                    data-placeholder="<?php echo __('System Default'); ?>">
+                    <option value=""></option>
 <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
                     <option value="<?php echo $zone; ?>" <?php
                     if ($info['timezone'] == $zone)
                         echo 'selected="selected"';
-                    ?>><?php echo $zone; ?></option>
+                    ?>><?php echo str_replace('/',' / ',$zone); ?></option>
 <?php } ?>
                 </select>
                 <button class="action-button" onclick="javascript:
@@ -101,9 +102,8 @@ $info['id']=$staff->getId();
         if (window.jstz !== undefined) {
             clearInterval(recheck);
             var zone = jstz.determine();
-            $('#timezone-dropdown').multiselect('widget')
-                .find('[value=\'' + zone.name() + '\']')
-                .trigger('click');
+            $('#timezone-dropdown').val(zone.name()).trigger('chosen:updated');
+
         }
     }, 200);
     return false;"><i class="icon-map-marker"></i> <?php echo __('Auto Detect'); ?></button>
@@ -273,24 +273,11 @@ $info['id']=$staff->getId();
     <input type="button" name="cancel" value="<?php echo __('Cancel Changes');?>" onclick='window.location.href="index.php"'>
 </p>
 </form>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.filter.css"/>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.filter.min.js"></script>
 <script type="text/javascript">
-(function() {
-var I = setInterval(function() {
-    if (!$.fn.multiselect || !$.ech.multiselectfilter)
-        return;
-    clearInterval(I);
-    $('#timezone-dropdown').multiselect({
-        multiple: false,
-        header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
-        noneSelectedText: <?php echo JsonDataEncoder::encode(__('System Default')); ?>,
-        selectedList: 1,
-        minWidth: 400
-    }).multiselectfilter({
-        placeholder: <?php echo JsonDataEncoder::encode(__('Search')); ?>
+!(function() {
+    $('#timezone-dropdown').chosen({
+        allow_single_deselect: true,
+        width: '350px'
     });
-}, 25);
 })();
 </script>
diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php
index de02c0ad24f3271fb123d1fa046c03e998120f8e..a095fbf6ab63ef1ddb4bb98caf7ee369fd8bf5f0 100644
--- a/include/staff/settings-system.inc.php
+++ b/include/staff/settings-system.inc.php
@@ -142,12 +142,12 @@ $gmtime = Misc::gmtime();
         </tr>
         <tr><td width="220" class="required"><?php echo __('Default Time Zone');?>:</td>
             <td>
-                <select name="default_timezone" multiple="multiple" id="timezone-dropdown">
+                <select name="default_timezone" id="timezone-dropdown">
 <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
                     <option value="<?php echo $zone; ?>" <?php
                     if ($config['default_timezone'] == $zone)
                         echo 'selected="selected"';
-                    ?>><?php echo $zone; ?></option>
+                    ?>><?php echo str_replace('/',' / ',$zone); ?></option>
 <?php } ?>
                 </select>
                 <button class="action-button" onclick="javascript:
@@ -157,9 +157,8 @@ $gmtime = Misc::gmtime();
         if (window.jstz !== undefined) {
             clearInterval(recheck);
             var zone = jstz.determine();
-            $('#timezone-dropdown').multiselect('widget')
-                .find('[value=\'' + zone.name() + '\']')
-                .trigger('click');
+            $('#timezone-dropdown').val(zone.name()).trigger('chosen:updated');
+
         }
     }, 200);
     return false;"><i class="icon-map-marker"></i> <?php echo __('Auto Detect'); ?></button>
@@ -283,25 +282,12 @@ $gmtime = Misc::gmtime();
     <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes');?>">
 </p>
 </form>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.filter.css"/>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.filter.min.js"></script>
 <script type="text/javascript">
 (function() {
-var I = setInterval(function() {
-    if (!$.fn.multiselect || !$.ech.multiselectfilter)
-        return;
-    clearInterval(I);
-    $('#timezone-dropdown').multiselect({
-        multiple: false,
-        header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
-        noneSelectedText: <?php echo JsonDataEncoder::encode(__('Select Default Time Zone')); ?>,
-        selectedList: 1,
-        minWidth: 400
-    }).multiselectfilter({
-        placeholder: <?php echo JsonDataEncoder::encode(__('Search')); ?>
+    $('#timezone-dropdown').chosen({
+        allow_single_deselect: true,
+        width: '350px'
     });
-}, 25);
 })();
 $('#secondary_langs').sortable({
     cursor: 'move'
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 32d44e94a21eb13e36aa66447716a183835132d3..665797a0cc6e8343cfb6949853dae92c68b19c89 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -266,13 +266,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Time Zone');?>:
             </td>
             <td>
-                <select name="timezone" multiple="multiple" id="timezone-dropdown">
-                    <option value=""><?php echo __('System Default'); ?></option>
+                <select name="timezone" class="chosen-select" id="timezone-dropdown"
+                    data-placeholder="<?php echo __('System Default'); ?>">
+                    <option value=""></option>
 <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
                     <option value="<?php echo $zone; ?>" <?php
                     if ($info['timezone'] == $zone)
                         echo 'selected="selected"';
-                    ?>><?php echo $zone; ?></option>
+                    ?>><?php echo str_replace('/',' / ',$zone); ?></option>
 <?php } ?>
                 </select>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['timezone']; ?></span>
@@ -343,17 +344,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="staff.php"'>
 </p>
 </form>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.css"/>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.filter.css"/>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>/js/jquery.multiselect.filter.min.js"></script>
 <script type="text/javascript">
-$('#timezone-dropdown').multiselect({
-    multiple: false,
-    header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
-    noneSelectedText: <?php echo JsonDataEncoder::encode(__('System Default')); ?>,
-    selectedList: 1,
-    minWidth: 400
-}).multiselectfilter({
-    placeholder: <?php echo JsonDataEncoder::encode(__('Search')); ?>
-});
+!(function() {
+    $('#timezone-dropdown').chosen({
+        allow_single_deselect: true,
+        width: '350px'
+    });
+})();
 </script>
diff --git a/include/staff/team.inc.php b/include/staff/team.inc.php
index d8748d0feb161ad9f1c8d449d7b361da58676833..0d5fdd2d37e231408761e92d70adcd41b9dd531b 100644
--- a/include/staff/team.inc.php
+++ b/include/staff/team.inc.php
@@ -26,7 +26,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Team');?></h2>
+ <h2><?php echo __('Team');?>
     <i class="help-tip icon-question-sign" href="#teams"></i>
     </h2>
  <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
diff --git a/include/staff/templates/advanced-search-field.tmpl.php b/include/staff/templates/advanced-search-field.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..40712cc18cbf18a639437b3331a71ead5828a7b7
--- /dev/null
+++ b/include/staff/templates/advanced-search-field.tmpl.php
@@ -0,0 +1,10 @@
+<input type="hidden" name="fields[]" value="<?php echo $name; ?>"/>
+<?php foreach ($fields as $F) { ?>
+<fieldset id="field<?php echo $F->getWidget()->id;
+    ?>" <?php if (!$F->isVisible()) echo 'style="display:none;"'; ?>>
+    <?php echo $F->render(); ?>
+    <?php foreach ($F->errors() as $E) {
+        ?><div class="error"><?php echo $E; ?></div><?php
+    } ?>
+</fieldset>
+<?php } ?>
diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9f275f027eb8093a9cf383db3b9b5a07aad354c
--- /dev/null
+++ b/include/staff/templates/advanced-search.tmpl.php
@@ -0,0 +1,187 @@
+<?php
+  $ff_uid = FormField::$uid;
+?>
+<div id="advanced-search">
+<h3><?php echo __('Advanced Ticket Search');?></h3>
+<a class="close" href=""><i class="icon-remove-circle"></i></a>
+<hr/>
+<form action="#tickets/search" method="post" name="search">
+<div class="row">
+<div class="span6">
+    <input type="hidden" name="a" value="search">
+<?php
+foreach ($form->errors(true) ?: array() as $message) {
+    ?><div class="error-banner"><?php echo $message;?></div><?php
+}
+
+foreach ($form->getFields() as $name=>$field) { ?>
+    <fieldset id="field<?php echo $field->getWidget()->id;
+        ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>>
+        <?php echo $field->render(); ?>
+        <?php foreach ($field->errors() as $E) {
+            ?><div class="error"><?php echo $E; ?></div><?php
+        } ?>
+    </fieldset>
+    <?php if ($name[0] == ':') { ?>
+    <input type="hidden" name="fields[]" value="<?php echo $name; ?>"/>
+    <?php }
+}
+?>
+<div id="extra-fields"></div>
+<hr/>
+<select id="search-add-new-field" name="new-field" style="max-width: 100%;">
+    <option value="">— <?php echo __('Add Other Field'); ?> —</option>
+<?php
+foreach ($matches as $name => $fields) { ?>
+    <optgroup label="<?php echo $name; ?>">
+<?php
+    foreach ($fields as $id => $desc) { ?>
+        <option value="<?php echo $id; ?>" <?php
+            if (isset($state[$id])) echo 'disabled="disabled"';
+        ?>><?php echo $desc; ?></option>
+<?php } ?>
+    </optgroup>
+<?php } ?>
+</select>
+
+</div>
+<div class="span6" style="border-left: 1px solid #888;">
+<div style="margin-bottom: 0.5em;"><b style="font-size: 110%;"><?php echo __('Saved Searches'); ?></b></div>
+<div id="saved-searches" class="accordian">
+<?php foreach (SavedSearch::forStaff($thisstaff) as $S) { ?>
+    <dt class="saved-search">
+        <a href="#" class="load-search"><?php echo $S->title; ?>
+        <i class="icon-chevron-down pull-right"></i>
+        </a>
+    </dt>
+    <dd>
+        <span>
+            <button onclick="javascript:$(this).closest('form').attr({
+'method': 'get', 'action': '#tickets/search/<?php echo $S->id; ?>'});"><i class="icon-chevron-left"></i> Load</button>
+            <?php if ($thisstaff->isAdmin()) { ?>
+                <button><i class="icon-bullhorn"></i> <?php echo __('Publish'); ?></button>
+            <?php } ?>
+            <button onclick="javascript:
+$.ajax({
+    url: 'ajax.php/tickets/search/<?php echo $S->id; ?>',
+    type: 'POST',
+    data: {'form': $(this).closest('.dialog').find('form[name=search]').serializeArray()},
+    dataType: 'json',
+    success: function(json) {
+      if (!json.id)
+        return;
+      $('<dt>').effect('highlight');
+    }
+});
+return false;
+"><i class="icon-save"></i> <?php echo __('Update'); ?></button>
+        </span>
+        <span class="pull-right">
+            <button title="<?php echo __('Delete'); ?>" onclick="javascript:
+    if (!confirm(__('You sure?'))) return false;
+    var that = this;
+    $.ajax({
+        'url': 'ajax.php/tickets/search/<?php echo $S->id; ?>',
+        'type': 'delete',
+        'dataType': 'json',
+        'success': function(json) {
+            if (json.success) {
+                $(that).closest('dd').prev('dt').slideUp().next('dd').slideUp();
+            }
+        }
+    });
+    return false;
+"><i class="icon-trash"></i></button>
+        </span>
+    </dd>
+<?php } ?>
+</div>
+<div>
+    <form method="post">
+    <fieldset>
+    <input name="title" type="text" size="30" placeholder="Enter a title for the search"/>
+    <span class="action-buttons">
+        <span class="action-button">
+            <a href="#tickets/search/create" onclick="javascript:
+$.ajax({
+    url: 'ajax.php/' + $(this).attr('href').substr(1),
+    type: 'POST',
+    data: {'name': $(this).closest('form').find('[name=title]').val(),
+           'form': $(this).closest('.dialog').find('form[name=search]').serializeArray()},
+    dataType: 'json',
+    success: function(json) {
+      if (!json.id)
+        return;
+      $('<dt>')
+        .append($('<a>').text(' ' + json.title)
+          .prepend($('<i>').addClass('icon-chevron-left'))
+        ).appendTo($('#saved-searches'));
+    }
+});
+return false;
+"><i class="icon-save"></i> <?php echo __('Save'); ?></a>
+        </span>
+        <span class="action-button pull-right" data-dropdown="#save-dropdown-more">
+            <i class="icon-caret-down pull-right"></i>
+        </span>
+    </span>
+    </fieldset>
+    <div id="save-dropdown-more" class="action-dropdown anchor-right">
+      <ul>
+        <li><a href="#queue/create">
+            <i class="icon-list"></i> <?php echo __('Create Queue'); ?></a>
+        </li>
+      </ul>
+    </div>
+</div>
+</div>
+</div>
+
+<hr/>
+<div>
+    <div id="search-hint" class="pull-left">
+    </div>
+    <div class="buttons pull-right">
+        <button class="button" id="do_search"><i class="icon-search"></i> <?php echo __('Search'); ?></button>
+    </div>
+</div>
+
+</form>
+
+<script type="text/javascript">
+$(function() {
+  $('#advanced-search [data-dropdown]').dropdown();
+
+  var I = setInterval(function() {
+    var A = $('#saved-searches.accordian');
+    if (!A.length) return;
+    clearInterval(I);
+
+    var allPanels = $('dd', A).hide();
+    $('dt > a', A).click(function() {
+      $('dt', A).removeClass('active');
+      allPanels.slideUp();
+      $(this).parent().addClass('active').next().slideDown();
+      return false;
+    });
+  }, 200);
+
+  var ff_uid = <?php echo $ff_uid; ?>;
+  $('#search-add-new-field').on('change', function() {
+    var that=this;
+    $.ajax({
+      url: 'ajax.php/tickets/search/field/'+$(this).val(),
+      type: 'get',
+      data: {ff_uid: ff_uid},
+      dataType: 'json',
+      success: function(json) {
+        if (!json.success)
+          return false;
+        ff_uid = json.ff_uid;
+        $(that).find(':selected').prop('disabled', true);
+        $('#extra-fields').append($(json.html));
+      }
+    });
+  });
+});
+</script>
diff --git a/include/staff/templates/inline-form.tmpl.php b/include/staff/templates/inline-form.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..5de3a283a4f692ba432caeb842b32f84e5e21e0b
--- /dev/null
+++ b/include/staff/templates/inline-form.tmpl.php
@@ -0,0 +1,26 @@
+<div><?php
+foreach ($form->getFields() as $field) { ?>
+    <span style="display:inline-block;padding-right:5px;vertical-align:middle">
+<?php   if (!$field->isBlockLevel()) { ?>
+        <span class="<?php if ($field->get('required')) echo 'required'; ?>">
+            <?php echo Format::htmlchars($field->get('label')); ?></span>
+<?php   } ?>
+        <div><?php
+        $field->render(); ?>
+        <?php if ($field->get('required')) { ?>
+            <span class="error">*</span>
+        <?php
+        }
+        if ($field->get('hint') && !$field->isBlockLevel()) { ?>
+            <br/><em style="color:gray;display:inline-block"><?php
+                echo Format::htmlchars($field->get('hint')); ?></em>
+        <?php
+        }
+        foreach ($field->errors() as $e) { ?>
+            <br />
+            <span class="error"><?php echo Format::htmlchars($e); ?></span>
+        <?php } ?>
+        </div>
+    </span><?php
+} ?>
+</div>
diff --git a/include/staff/templates/org-profile.tmpl.php b/include/staff/templates/org-profile.tmpl.php
index bdfbd681a5f8376935e4a4de65392580f5e28682..68819629cbb3d01a142ba1e68f6bdea4feda2975 100644
--- a/include/staff/templates/org-profile.tmpl.php
+++ b/include/staff/templates/org-profile.tmpl.php
@@ -4,8 +4,6 @@ $info=($_POST && $errors)?Format::input($_POST):@Format::htmlchars($org->getInfo
 if (!$info['title'])
     $info['title'] = Format::htmlchars($org->getName());
 ?>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/>
 <h3><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
@@ -89,7 +87,9 @@ if ($ticket && $ticket->getOwnerId() == $user->getId())
                     <?php echo __('Primary Contacts'); ?>:
                 </td>
                 <td>
-                    <select name="contacts[]" id="primary_contacts" multiple="multiple">
+                    <select name="contacts[]" id="primary_contacts" multiple="multiple"
+                        data-placeholder="<?php echo __('Select Contacts'); ?>">
+                        <option value=""></option>
 <?php               foreach ($org->allMembers() as $u) { ?>
                         <option value="<?php echo $u->id; ?>" <?php
                             if ($u->isPrimaryContact())
@@ -170,6 +170,6 @@ $(function() {
         $('div#org-profile').fadeIn();
         return false;
     });
-    $("#primary_contacts").multiselect({'noneSelectedText':'<?php echo __('Select Contacts'); ?>'});
+    $("#primary_contacts").chosen({width: '300px'});
 });
 </script>
diff --git a/include/staff/templates/status-options.tmpl.php b/include/staff/templates/status-options.tmpl.php
index edfdf19564e0367a1ebaa97e3adc1e08d075cf04..cdcaa395bec9a424a61f6bf62f9b4a29f0ef58b2 100644
--- a/include/staff/templates/status-options.tmpl.php
+++ b/include/staff/templates/status-options.tmpl.php
@@ -15,7 +15,7 @@ $actions= array(
 ?>
 
 <span
-    class="action-button pull-right"
+    class="action-button"
     data-dropdown="#action-dropdown-statuses">
     <i class="icon-caret-down pull-right"></i>
     <a class="tickets-action"
diff --git a/include/staff/templates/user-account.tmpl.php b/include/staff/templates/user-account.tmpl.php
index da83774ea2654cd600744efb2ebbd3cd54e9c95c..0bb5d0793a9a6b108c16b51a5cb7b06e9f821385 100644
--- a/include/staff/templates/user-account.tmpl.php
+++ b/include/staff/templates/user-account.tmpl.php
@@ -66,13 +66,14 @@ if ($info['error']) {
                     <?php echo __('Time Zone');?>:
                 </td>
                 <td>
-                    <select name="timezone" multiple="multiple" id="timezone-dropdown">
-                        <option value=""><?php echo __('System Default'); ?></option>
+                    <select name="timezone" class="chosen-select" id="timezone-dropdown"
+                        data-placeholder="<?php echo __('System Default'); ?>">
+                        <option value=""></option>
     <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
                         <option value="<?php echo $zone; ?>" <?php
                         if ($info['timezone'] == $zone)
                             echo 'selected="selected"';
-                        ?>><?php echo $zone; ?></option>
+                        ?>><?php echo str_replace('/',' / ',$zone); ?></option>
     <?php } ?>
                     </select>
                     <div class="error"><?php echo $errors['timezone']; ?></div>
@@ -162,9 +163,6 @@ if ($info['error']) {
     </p>
 </form>
 <div class="clear"></div>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.css"/>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>/css/jquery.multiselect.filter.css"/>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>/js/jquery.multiselect.filter.min.js"></script>
 <script type="text/javascript">
 $(function() {
     $(document).on('click', 'input#sendemail', function(e) {
@@ -174,13 +172,11 @@ $(function() {
             $('tbody#password').show();
     });
 });
-$('#timezone-dropdown').multiselect({
-    multiple: false,
-    header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
-    noneSelectedText: <?php echo JsonDataEncoder::encode(__('System Default')); ?>,
-    selectedList: 1,
-    minWidth: 400
-}).multiselectfilter({
-    placeholder: <?php echo JsonDataEncoder::encode(__('Search')); ?>
-});
+!(function() {
+    $('#timezone-dropdown').chosen({
+        header: <?php echo JsonDataEncoder::encode(__('Time Zones')); ?>,
+        allow_single_deselect: true,
+        width: '350px'
+    });
+})();
 </script>
diff --git a/include/staff/templates/user-register.tmpl.php b/include/staff/templates/user-register.tmpl.php
index 015f82edf9fa5a66fe9d0c744deeb8a8eb665896..a68d1a23c17be5dcc3255a3d7955c638d275bd2d 100644
--- a/include/staff/templates/user-register.tmpl.php
+++ b/include/staff/templates/user-register.tmpl.php
@@ -5,14 +5,7 @@ if (!$info['title'])
     $info['title'] = sprintf(__('Register: %s'), Format::htmlchars($user->getName()));
 
 if (!$_POST) {
-
     $info['sendemail'] = true; // send email confirmation.
-
-    if (!isset($info['timezone_id']))
-        $info['timezone_id'] = $cfg->getDefaultTimezoneId();
-
-    if (!isset($info['dst']))
-        $info['dst'] = $cfg->observeDaylightSaving();
 }
 
 ?>
@@ -137,28 +130,17 @@ echo sprintf(__(
             </tr>
                 <td><?php echo __('Time Zone'); ?>:</td>
                 <td>
-                    <select name="timezone_id" id="timezone_id">
-                        <?php
-                        $sql='SELECT id, offset, timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-                        if(($res=db_query($sql)) && db_num_rows($res)){
-                            while(list($id, $offset, $tz) = db_fetch_row($res)) {
-                                $sel=($info['timezone_id']==$id) ? 'selected="selected"' : '';
-                                echo sprintf('<option value="%d" %s>GMT %s - %s</option>',
-                                        $id, $sel, $offset, $tz);
-                            }
-                        }
-                        ?>
+                    <select name="timezone" class="chosen-select" id="timezone-dropdown"
+                        data-placeholder="<?php echo __('System Default'); ?>">
+                        <option value=""></option>
+    <?php foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
+                        <option value="<?php echo $zone; ?>" <?php
+                        if ($info['timezone'] == $zone)
+                            echo 'selected="selected"';
+                        ?>><?php echo str_replace('/',' / ',$zone); ?></option>
+    <?php } ?>
                     </select>
-                    &nbsp;<span class="error"><?php echo $errors['timezone_id']; ?></span>
-                </td>
-            </tr>
-            <tr>
-                <td width="180">
-                   <?php echo __('Daylight Saving'); ?>:
-                </td>
-                <td>
-                    <input type="checkbox" name="dst" value="1" <?php echo $info['dst'] ? 'checked="checked"' : ''; ?>>
-                    <?php echo __('Observe daylight saving'); ?>
+                    &nbsp;<span class="error"><?php echo $errors['timezone']; ?></span>
                 </td>
             </tr>
         </tbody>
@@ -184,5 +166,9 @@ $(function() {
         else
             $('tbody#password').show();
     });
+    $('#timezone-dropdown').chosen({
+        allow_single_deselect: true,
+        width: '350px'
+    });
 });
 </script>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 1c5b5f003c453e4c131517c908d8efb91836a15f..5f852956dd1f570195bf3efb7f6d480df7ace91d 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -1,311 +1,125 @@
 <?php
-if(!defined('OSTSCPINC') || !$thisstaff || !@$thisstaff->isStaff()) die('Access Denied');
+$search = SavedSearch::create();
+$tickets = TicketModel::objects();
+$clear_button = false;
+$date_header = $date_col = false;
 
-$qstr='&'; //Query string collector
-if($_REQUEST['status']) { //Query string status has nothing to do with the real status used below; gets overloaded.
-    $qstr.='status='.urlencode($_REQUEST['status']);
-}
-
-//See if this is a search
-$search=($_REQUEST['a']=='search');
-$searchTerm='';
-//make sure the search query is 3 chars min...defaults to no query with warning message
-if($search) {
-  $searchTerm=$_REQUEST['query'];
-  if( ($_REQUEST['query'] && strlen($_REQUEST['query'])<3)
-      || (!$_REQUEST['query'] && isset($_REQUEST['basic_search'])) ){ //Why do I care about this crap...
-      $search=false; //Instead of an error page...default back to regular query..with no search.
-      $errors['err']=__('Search term must be more than 3 chars');
-      $searchTerm='';
-  }
-}
-$showoverdue=$showanswered=false;
-$staffId=0; //Nothing for now...TODO: Allow admin and manager to limit tickets to single staff level.
-$showassigned= true; //show Assigned To column - defaults to true
+// Add "other" fields (via $_POST['other'][])
 
-//Get status we are actually going to use on the query...making sure it is clean!
-$status=null;
 switch(strtolower($_REQUEST['status'])){ //Status is overloaded
-    case 'open':
-        $status='open';
-		$results_type=__('Open Tickets');
-        break;
-    case 'closed':
-        $status='closed';
-		$results_type=__('Closed Tickets');
-        $showassigned=true; //closed by.
-        break;
-    case 'overdue':
-        $status='open';
-        $showoverdue=true;
-        $results_type=__('Overdue Tickets');
-        break;
-    case 'assigned':
-        $status='open';
-        $staffId=$thisstaff->getId();
-        $results_type=__('My Tickets');
-        break;
-    case 'answered':
-        $status='open';
-        $showanswered=true;
-        $results_type=__('Answered Tickets');
+case 'closed':
+    $status='closed';
+    $results_type=__('Closed Tickets');
+    $showassigned=true; //closed by.
+    $tickets->values('staff__firstname', 'staff__lastname', 'team__name', 'team_id');
+    break;
+case 'overdue':
+    $status='open';
+    $results_type=__('Overdue Tickets');
+    $tickets->filter(array('isoverdue'=>1));
+    break;
+case 'assigned':
+    $status='open';
+    $staffId=$thisstaff->getId();
+    $results_type=__('My Tickets');
+    $tickets->filter(array('staff_id'=>$thisstaff->getId()));
+    break;
+case 'answered':
+    $status='open';
+    $showanswered=true;
+    $results_type=__('Answered Tickets');
+    $tickets->filter(array('isanswered'=>1));
+    break;
+default:
+case 'search':
+    if (isset($_SESSION['advsearch'])) {
+        // XXX: De-duplicate and simplify this code
+        $form = $search->getFormFromSession('advsearch');
+        $form->loadState($_SESSION['advsearch']);
+        $tickets = $search->mangleQuerySet($tickets, $form);
+        $results_type=__('Advanced Search')
+            . '<a class="action-button" href="?clear_filter"><i class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>';
+        unset($_REQUEST['sort']);
         break;
-    default:
-        if (!$search && !isset($_REQUEST['advsid'])) {
-            $_REQUEST['status']=$status='open';
-            $results_type=__('Open Tickets');
-        }
-}
-
-$qwhere ='';
-/*
-   STRICT DEPARTMENTS BASED PERMISSION!
-   User can also see tickets assigned to them regardless of the ticket's dept.
-*/
-
-$depts=$thisstaff->getDepts();
-$qwhere =' WHERE ( '
-        .'  ( ticket.staff_id='.db_input($thisstaff->getId())
-        .' AND status.state="open") ';
-
-if(!$thisstaff->showAssignedOnly())
-    $qwhere.=' OR ticket.dept_id IN ('.($depts?implode(',', db_input($depts)):0).')';
-
-if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-    $qwhere.=' OR (ticket.team_id IN ('.implode(',', db_input(array_filter($teams)))
-            .') AND status.state="open") ';
-
-$qwhere .= ' )';
-
-//STATUS to states
-$states = array(
-    'open' => array('open'),
-    'closed' => array('closed'));
-
-if($status && isset($states[$status])) {
-    $qwhere.=' AND status.state IN (
-                '.implode(',', db_input($states[$status])).' ) ';
-}
-
-if (isset($_REQUEST['uid']) && $_REQUEST['uid']) {
-    $qwhere .= ' AND (ticket.user_id='.db_input($_REQUEST['uid'])
-            .' OR collab.user_id='.db_input($_REQUEST['uid']).') ';
-    $qstr .= '&uid='.urlencode($_REQUEST['uid']);
-}
-
-//Queues: Overloaded sub-statuses  - you've got to just have faith!
-if($staffId && ($staffId==$thisstaff->getId())) { //My tickets
-    $results_type=__('Assigned Tickets');
-    $qwhere.=' AND ticket.staff_id='.db_input($staffId);
-    $showassigned=false; //My tickets...already assigned to the staff.
-}elseif($showoverdue) { //overdue
-    $qwhere.=' AND ticket.isoverdue=1 ';
-}elseif($showanswered) { ////Answered
-    $qwhere.=' AND ticket.isanswered=1 ';
-}elseif(!strcasecmp($status, 'open') && !$search) { //Open queue (on search OPEN means all open tickets - regardless of state).
-    //Showing answered tickets on open queue??
-    if(!$cfg->showAnsweredTickets())
-        $qwhere.=' AND ticket.isanswered=0 ';
-
-    /* Showing assigned tickets on open queue?
-       Don't confuse it with show assigned To column -> F'it it's confusing - just trust me!
-     */
-    if(!($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets())) {
-        $qwhere.=' AND ticket.staff_id=0 '; //XXX: NOT factoring in team assignments - only staff assignments.
-        $showassigned=false; //Not showing Assigned To column since assigned tickets are not part of open queue
     }
+    // Fall-through and show open tickets
+case 'open':
+    $status='open';
+    $results_type=__('Open Tickets');
+    if (!$cfg->showAnsweredTickets())
+        $tickets->filter(array('isanswered'=>0));
+    if (!$cfg || !($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets()))
+        $tickets->filter(Q::any(array('staff_id'=>0, 'team_id'=>0)));
+    break;
 }
 
-//Search?? Somebody...get me some coffee
-$deep_search=false;
-$order_by=$order=null;
-if($search):
-    $qstr.='&a='.urlencode($_REQUEST['a']);
-    $qstr.='&t='.urlencode($_REQUEST['t']);
-
-    //query
-    if($searchTerm){
-        $qstr.='&query='.urlencode($searchTerm);
-        $queryterm=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes.
-        if (is_numeric($searchTerm)) {
-            $qwhere.=" AND ticket.`number` LIKE '$queryterm%'";
-        } elseif (strpos($searchTerm,'@') && Validator::is_email($searchTerm)) {
-            //pulling all tricks!
-            # XXX: What about searching for email addresses in the body of
-            #      the thread message
-            $qwhere.=" AND email.address='$queryterm'";
-        } else {//Deep search!
-            //This sucks..mass scan! search anything that moves!
-            require_once(INCLUDE_DIR.'ajax.tickets.php');
-
-            $tickets = TicketsAjaxApi::_search(array('query'=>$queryterm));
-            if (count($tickets)) {
-                $ticket_ids = implode(',',db_input($tickets));
-                $qwhere .= ' AND ticket.ticket_id IN ('.$ticket_ids.')';
-                $order_by = 'FIELD(ticket.ticket_id, '.$ticket_ids.')';
-                $order = ' ';
-            }
-            else
-                // No hits -- there should be an empty list of results
-                $qwhere .= ' AND false';
-        }
-   }
-
-endif;
-
-if ($_REQUEST['advsid'] && isset($_SESSION['adv_'.$_REQUEST['advsid']])) {
-    $ticket_ids = implode(',', db_input($_SESSION['adv_'.$_REQUEST['advsid']]));
-    $qstr.='advsid='.$_REQUEST['advsid'];
-    $qwhere .= ' AND ticket.ticket_id IN ('.$ticket_ids.')';
-    // Thanks, http://stackoverflow.com/a/1631794
-    $order_by = 'FIELD(ticket.ticket_id, '.$ticket_ids.')';
-    $order = ' ';
+// Apply primary ticket status
+if ($status)
+    $tickets->filter(array('status__state'=>$status));
+
+// Impose visibility constraints
+// ------------------------------------------------------------
+// -- Open and assigned to me
+$visibility = array(
+    new Q(array('status__state'=>'open', 'staff_id' => $thisstaff->getId()))
+);
+// -- Routed to a department of mine
+if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
+    $visibility[] = new Q(array('dept_id__in' => $depts));
+// -- Open and assigned to a team of mine
+if (($teams = $thisstaff->getTeams()) && count(array_filter($teams)))
+    $visibility[] = new Q(array(
+        'team_id__in' => array_filter($teams), 'status__state'=>'open'
+    ));
+$tickets->filter(Q::any($visibility));
+
+// Select pertinent columns
+// ------------------------------------------------------------
+$tickets->values('lock__lock_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_id', 'number', 'cdata__subject', 'user__default_email__address', 'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status__name', 'status__state', 'dept_id', 'dept__dept_name', 'user__name', 'lastupdate');
+
+// Apply requested quick filter
+
+// Apply requested sorting
+switch ($_REQUEST['sort']) {
+case 'number':
+    $tickets->extra(array(
+        'order_by'=>array(SqlExpression::times(new SqlField('number'), 1))
+    ));
+    break;
+case 'created':
+    $tickets->order_by('-created');
+    break;
+
+case 'priority,due':
+    $tickets->order_by('cdata__:priority__priority_urgency');
+    // Fall through to add in due date filter
+case 'due':
+    $date_header = __('Due Date');
+    $date_col = 'est_duedate';
+    $tickets->values('est_duedate');
+    $tickets->filter(array('est_duedate__isnull'=>false));
+    $tickets->order_by(new SqlField('est_duedate'));
+    break;
+
+default:
+case 'updated':
+    $tickets->order_by('cdata__:priority__priority_urgency', '-lastupdate');
+    break;
 }
 
-$sortOptions=array('date'=>'effective_date','ID'=>'ticket.`number`*1',
-    'pri'=>'pri.priority_urgency','name'=>'user.name','subj'=>'cdata.subject',
-    'status'=>'status.name','assignee'=>'assigned','staff'=>'staff',
-    'dept'=>'dept.dept_name');
-
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-
-//Sorting options...
-$queue = isset($_REQUEST['status'])?strtolower($_REQUEST['status']):$status;
-if($_REQUEST['sort'] && $sortOptions[$_REQUEST['sort']])
-    $order_by =$sortOptions[$_REQUEST['sort']];
-elseif($sortOptions[$_SESSION[$queue.'_tickets']['sort']]) {
-    $_REQUEST['sort'] = $_SESSION[$queue.'_tickets']['sort'];
-    $_REQUEST['order'] = $_SESSION[$queue.'_tickets']['order'];
-
-    $order_by = $sortOptions[$_SESSION[$queue.'_tickets']['sort']];
-    $order = $_SESSION[$queue.'_tickets']['order'];
-}
-
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])])
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
-
-//Save sort order for sticky sorting.
-if($_REQUEST['sort'] && $queue) {
-    $_SESSION[$queue.'_tickets']['sort'] = $_REQUEST['sort'];
-    $_SESSION[$queue.'_tickets']['order'] = $_REQUEST['order'];
-}
-
-//Set default sort by columns.
-if(!$order_by ) {
-    if($showanswered)
-        $order_by='ticket.lastresponse, ticket.created'; //No priority sorting for answered tickets.
-    elseif(!strcasecmp($status,'closed'))
-        $order_by='ticket.closed, ticket.created'; //No priority sorting for closed tickets.
-    elseif($showoverdue) //priority> duedate > age in ASC order.
-        $order_by='pri.priority_urgency ASC, ISNULL(ticket.duedate) ASC, ticket.duedate ASC, effective_date ASC, ticket.created';
-    else //XXX: Add due date here?? No -
-        $order_by='pri.priority_urgency ASC, effective_date DESC, ticket.created';
-}
-
-$order=$order?$order:'DESC';
-if($order_by && strpos($order_by,',') && $order)
-    $order_by=preg_replace('/(?<!ASC|DESC),/', " $order,", $order_by);
-
-$sort=$_REQUEST['sort']?strtolower($_REQUEST['sort']):'pri.priority_urgency'; //Urgency is not on display table.
-$x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-
-if($_GET['limit'])
-    $qstr.='&limit='.urlencode($_GET['limit']);
-
-$qselect ='SELECT ticket.ticket_id,tlock.lock_id,ticket.`number`,ticket.dept_id,ticket.staff_id,ticket.team_id '
-    .' ,user.name'
-    .' ,email.address as email, dept.dept_name, status.state '
-         .' ,status.name as status,ticket.source,ticket.isoverdue,ticket.isanswered,ticket.created ';
-
-$qfrom=' FROM '.TICKET_TABLE.' ticket '.
-       ' LEFT JOIN '.TICKET_STATUS_TABLE. ' status
-            ON (status.id = ticket.status_id) '.
-       ' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'.
-       ' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'.
-       ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id ';
-
-if ($_REQUEST['uid'])
-    $qfrom.=' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-        ON (ticket.ticket_id = collab.ticket_id )';
-
-
-$sjoin='';
-
-if($search && $deep_search) {
-    $sjoin.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )';
-}
-
-//get ticket count based on the query so far..
-$total=db_count("SELECT count(DISTINCT ticket.ticket_id) $qfrom $sjoin $qwhere");
-//pagenate
+// Apply requested pagination
 $pagelimit=($_GET['limit'] && is_numeric($_GET['limit']))?$_GET['limit']:PAGE_LIMIT;
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-$pageNav=new Pagenate($total,$page,$pagelimit);
-$pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
-
-//ADD attachment,priorities, lock and other crap
-$qselect.=' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate '
-         .' ,CAST(GREATEST(IFNULL(ticket.lastmessage, 0), IFNULL(ticket.closed, 0), IFNULL(ticket.reopened, 0), ticket.created) as datetime) as effective_date '
-         .' ,ticket.created as ticket_created, CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team '
-         .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned '
-         .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '
-         .' ,cdata.priority as priority_id, cdata.subject, pri.priority_desc, pri.priority_color';
-
-$qfrom.=' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()
-               AND tlock.staff_id!='.db_input($thisstaff->getId()).') '
-       .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) '
-       .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) '
-       .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
-       .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) '
-       .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) '
-       .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id) '
-       .' LEFT JOIN '.PRIORITY_TABLE.' pri ON (pri.priority_id = cdata.priority)';
+$pageNav=new Pagenate($tickets->count(), $page,$pagelimit);
+$tickets = $pageNav->paginate($tickets);
 
 TicketForm::ensureDynamicDataView();
 
-$query="$qselect $qfrom $qwhere ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-//echo $query;
-$hash = md5($query);
-$_SESSION['search_'.$hash] = $query;
-$res = db_query($query);
-$showing=db_num_rows($res)? ' &mdash; '.$pageNav->showing():"";
-if(!$results_type)
-    $results_type = sprintf(__('%s Tickets' /* %s will be a status such as 'open' */),
-        mb_convert_case($status, MB_CASE_TITLE));
-
-if($search)
-    $results_type.= ' ('.__('Search Results').')';
-
-$negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting..
+// Save the query to the session for exporting
+$_SESSION[':Q:tickets'] = $tickets;
 
-// Fetch the results
-$results = array();
-while ($row = db_fetch_array($res)) {
-    $results[$row['ticket_id']] = $row;
-}
-
-// Fetch attachment and thread entry counts
-if ($results) {
-    $counts_sql = 'SELECT ticket.ticket_id,
-        count(DISTINCT attach.attach_id) as attachments,
-        count(DISTINCT thread.id) as thread_count,
-        count(DISTINCT collab.id) as collaborators
-        FROM '.TICKET_TABLE.' ticket
-        LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) '
-     .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) '
-     .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-            ON ( ticket.ticket_id=collab.ticket_id) '
-     .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).')
-        GROUP BY ticket.ticket_id';
-    $ids_res = db_query($counts_sql);
-    while ($row = db_fetch_array($ids_res)) {
-        $results[$row['ticket_id']] += $row;
-    }
-}
-
-//YOU BREAK IT YOU FIX IT.
 ?>
+
 <!-- SEARCH FORM START -->
 <div id='basic_search'>
     <form action="tickets.php" method="get">
@@ -316,7 +130,9 @@ if ($results) {
             <td><input type="text" id="basic-ticket-search" name="query" size=30 value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"
                 autocomplete="off" autocorrect="off" autocapitalize="off"></td>
             <td><input type="submit" name="basic_search" class="button" value="<?php echo __('Search'); ?>"></td>
-            <td>&nbsp;&nbsp;<a href="#" id="go-advanced">[<?php echo __('advanced'); ?>]</a>&nbsp;<i class="help-tip icon-question-sign" href="#advanced"></i></td>
+            <td>&nbsp;&nbsp;<a href="#" onclick="javascript:
+                $.dialog('ajax.php/tickets/search', 201);"
+                >[<?php echo __('advanced'); ?>]</a>&nbsp;<i class="help-tip icon-question-sign" href="#advanced"></i></td>
         </tr>
     </table>
     </form>
@@ -331,19 +147,30 @@ if ($results) {
                 $results_type.$showing; ?></a></h2>
         </div>
         <div class="pull-right flush-right">
-
+            <span style="display:inline-block">
+                <span style="vertical-align: baseline">Sort:</span>
+            <select name="sort" onchange="javascript:addSearchParam('sort', $(this).val());">
+<?php foreach (array(
+    'updated' =>    __('Most Recently Updated'),
+    'created' =>    __('Most Recently Created'),
+    'due' =>        __('Due Soon'),
+    'priority,due' => __('Priority + Due Soon'),
+    'number' =>     __('Ticket Number'),
+) as $mode => $desc) { ?>
+            <option value="<?php echo $mode; ?>" <?php if ($mode == $_REQUEST['sort']) echo 'selected="selected"'; ?>><?php echo $desc; ?></option>
+<?php } ?>
+            </select>
+            </span>
             <?php
+            if ($thisstaff->canManageTickets()) {
+                echo TicketStatus::status_options();
+            }
             if ($thisstaff->canDeleteTickets()) { ?>
-            <a id="tickets-delete" class="action-button pull-right tickets-action"
+            <a id="tickets-delete" class="action-button tickets-action"
                 href="#tickets/status/delete"><i
             class="icon-trash"></i> <?php echo __('Delete'); ?></a>
             <?php
             } ?>
-            <?php
-            if ($thisstaff->canManageTickets()) {
-                echo TicketStatus::status_options();
-            }
-            ?>
         </div>
 </div>
 <div class="clear" style="margin-bottom:10px;"></div>
@@ -359,27 +186,21 @@ if ($results) {
 	        <th width="8px">&nbsp;</th>
             <?php } ?>
 	        <th width="70">
-                <a <?php echo $id_sort; ?> href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                    title="<?php echo sprintf(__('Sort by %s %s'), __('Ticket ID'), __($negorder)); ?>"><?php echo __('Ticket'); ?></a></th>
+                <?php echo __('Ticket'); ?></th>
 	        <th width="70">
-                <a  <?php echo $date_sort; ?> href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                    title="<?php echo sprintf(__('Sort by %s %s'), __('Date'), __($negorder)); ?>"><?php echo __('Date'); ?></a></th>
+                <?php echo $date_header ?: __('Date'); ?></th>
 	        <th width="280">
-                 <a <?php echo $subj_sort; ?> href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                    title="<?php echo sprintf(__('Sort by %s %s'), __('Subject'), __($negorder)); ?>"><?php echo __('Subject'); ?></a></th>
+                <?php echo __('Subject'); ?></th>
             <th width="170">
-                <a <?php echo $name_sort; ?> href="tickets.php?sort=name&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                     title="<?php echo sprintf(__('Sort by %s %s'), __('Name'), __($negorder)); ?>"><?php echo __('From');?></a></th>
+                <?php echo __('From');?></th>
             <?php
             if($search && !$status) { ?>
                 <th width="60">
-                    <a <?php echo $status_sort; ?> href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                        title="<?php echo sprintf(__('Sort by %s %s'), __('Status'), __($negorder)); ?>"><?php echo __('Status');?></a></th>
+                    <?php echo __('Status');?></th>
             <?php
             } else { ?>
                 <th width="60" <?php echo $pri_sort;?>>
-                    <a <?php echo $pri_sort; ?> href="tickets.php?sort=pri&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                        title="<?php echo sprintf(__('Sort by %s %s'), __('Priority'), __($negorder)); ?>"><?php echo __('Priority');?></a></th>
+                    <?php echo __('Priority');?></th>
             <?php
             }
 
@@ -387,19 +208,16 @@ if ($results) {
                 //Closed by
                 if(!strcasecmp($status,'closed')) { ?>
                     <th width="150">
-                        <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                            title="<?php echo sprintf(__('Sort by %s %s'), __("Closing Agent's Name"), __($negorder)); ?>"><?php echo __('Closed By'); ?></a></th>
+                        <?php echo __('Closed By'); ?></th>
                 <?php
                 } else { //assigned to ?>
                     <th width="150">
-                        <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                            title="<?php echo sprintf(__('Sort by %s %s'), __('Assignee'), __($negorder)); ?>"><?php echo __('Assigned To'); ?></a></th>
+                        <?php echo __('Assigned To'); ?></th>
                 <?php
                 }
             } else { ?>
                 <th width="150">
-                    <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>"
-                        title="<?php echo sprintf(__('Sort by %s %s'), __('Department'), __($negorder)); ?>"><?php echo __('Department');?></a></th>
+                    <?php echo __('Department');?></th>
             <?php
             } ?>
         </tr>
@@ -410,55 +228,55 @@ if ($results) {
         $subject_field = TicketForm::objects()->one()->getField('subject');
         $class = "row1";
         $total=0;
-        if($res && ($num=count($results))):
-            $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null;
-            foreach ($results as $row) {
-                $tag=$row['staff_id']?'assigned':'openticket';
+        $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null;
+        $subject_field = TicketForm::objects()->one()->getField('subject');
+        foreach ($tickets as $T) {
+            $total += 1;
+                $tag=$T['staff_id']?'assigned':'openticket';
                 $flag=null;
-                if($row['lock_id'])
+                if($T['lock__lock_id'])
                     $flag='locked';
-                elseif($row['isoverdue'])
+                elseif($T['isoverdue'])
                     $flag='overdue';
 
                 $lc='';
+                $dept = Dept::getLocalById($T['dept_id'], 'name', $T['dept__dept_name']);
                 if($showassigned) {
-                    if($row['staff_id'])
-                        $lc=sprintf('<span class="Icon staffAssigned">%s</span>',Format::truncate($row['staff'],40));
-                    elseif($row['team_id'])
-                        $lc=sprintf('<span class="Icon teamAssigned">%s</span>',Format::truncate($row['team'],40));
+                    if($T['staff_id'])
+                        $lc=sprintf('<span class="Icon staffAssigned">%s</span>',Format::truncate((string) new PersonsName($T['staff__firstname'], $T['staff__lastname']),40));
+                    elseif($T['team_id'])
+                        $lc=sprintf('<span class="Icon teamAssigned">%s</span>',
+                            Format::truncate(Team::getLocalById($T['team_id'], 'name', $T['team__name']),40));
                     else
                         $lc=' ';
                 }else{
-                    $lc=Format::truncate($row['dept_name'],40);
+                    $lc=Format::truncate($dept,40);
                 }
-                $tid=$row['number'];
-
-                $subject = Format::truncate($subject_field->display(
-                    $subject_field->to_php($row['subject']) ?: $row['subject']
-                ), 40);
+                $tid=$T['number'];
+                $subject = Format::truncate($subject_field->display($subject_field->to_php($T['cdata__subject'])),40);
                 $threadcount=$row['thread_count'];
-                if(!strcasecmp($row['state'],'open') && !$row['isanswered'] && !$row['lock_id']) {
+                if(!strcasecmp($T['status__state'],'open') && !$T['isanswered'] && !$T['lock__lock_id']) {
                     $tid=sprintf('<b>%s</b>',$tid);
                 }
                 ?>
-            <tr id="<?php echo $row['ticket_id']; ?>">
+            <tr id="<?php echo $T['ticket_id']; ?>">
                 <?php if($thisstaff->canManageTickets()) {
 
                     $sel=false;
-                    if($ids && in_array($row['ticket_id'], $ids))
+                    if($ids && in_array($T['ticket_id'], $ids))
                         $sel=true;
                     ?>
                 <td align="center" class="nohover">
                     <input class="ckb" type="checkbox" name="tids[]"
-                        value="<?php echo $row['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
+                        value="<?php echo $T['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
                 </td>
                 <?php } ?>
-                <td title="<?php echo $row['email']; ?>" nowrap>
-                  <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview" title="Preview Ticket"
-                    href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td>
-                <td align="center" nowrap><?php echo Format::datetime($row['effective_date']); ?></td>
+                <td title="<?php echo $T['user__default_email__address']; ?>" nowrap>
+                  <a class="Icon <?php echo strtolower($T['source']); ?>Ticket ticketPreview" title="Preview Ticket"
+                    href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $tid; ?></a></td>
+                <td align="center" nowrap><?php echo Format::datetime($T[$date_col ?: 'lastupdate']); ?></td>
                 <td><a <?php if ($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?>
-                    href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a>
+                    href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $subject; ?></a>
                      <?php
                         if ($threadcount>1)
                             echo "<small>($threadcount)</small>&nbsp;".'<i
@@ -469,32 +287,32 @@ if ($results) {
                             echo '<i class="icon-fixed-width icon-paperclip"></i>&nbsp;';
                     ?>
                 </td>
-                <td nowrap>&nbsp;<?php echo Format::htmlchars(
-                        Format::truncate($row['name'], 22, strpos($row['name'], '@'))); ?>&nbsp;</td>
+                <td nowrap>&nbsp;<?php $un = new PersonsName($T['user__name']); echo Format::htmlchars(
+                        Format::truncate($un, 22, strpos($un, '@'))); ?>&nbsp;</td>
                 <?php
                 if($search && !$status){
-                    $displaystatus=ucfirst($row['status']);
-                    if(!strcasecmp($row['state'],'open'))
+                    $displaystatus=ucfirst($T['status__name']);
+                    if(!strcasecmp($T['status__state'],'open'))
                         $displaystatus="<b>$displaystatus</b>";
                     echo "<td>$displaystatus</td>";
                 } else { ?>
-                <td class="nohover" align="center" style="background-color:<?php echo $row['priority_color']; ?>;">
-                    <?php echo $row['priority_desc']; ?></td>
+                <td class="nohover" align="center" style="background-color:<?php echo $T['cdata__:priority__priority_color']; ?>;">
+                    <?php echo $T['cdata__:priority__priority_desc']; ?></td>
                 <?php
                 }
                 ?>
                 <td nowrap>&nbsp;<?php echo $lc; ?></td>
             </tr>
             <?php
-            } //end of while.
-        else: //not tickets found!! set fetch error.
+            } //end of foreach
+        if (!$total)
             $ferror=__('There are no tickets matching your criteria.');
-        endif; ?>
+        ?>
     </tbody>
     <tfoot>
      <tr>
         <td colspan="7">
-            <?php if($res && $num && $thisstaff->canManageTickets()){ ?>
+            <?php if($total && $thisstaff->canManageTickets()){ ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -509,10 +327,10 @@ if ($results) {
     </tfoot>
     </table>
     <?php
-    if($num>0){ //if we actually had any tickets returned.
+    if($total>0){ //if we actually had any tickets returned.
         echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;';
-        echo '<a class="export-csv no-pjax" href="?a=export&h='
-            .$hash.'&status='.$_REQUEST['status'] .'">'.__('Export').'</a>&nbsp;<i class="help-tip icon-question-sign" href="#export"></i></div>';
+        echo '<a class="export-csv no-pjax" href="?a=export&status='
+            .$_REQUEST['status'] .'">'.__('Export').'</a>&nbsp;<i class="help-tip icon-question-sign" href="#export"></i></div>';
     } ?>
     </form>
 </div>
@@ -536,142 +354,6 @@ if ($results) {
      </p>
     <div class="clear"></div>
 </div>
-
-<div class="dialog" style="display:none;" id="advanced-search">
-    <h3><?php echo __('Advanced Ticket Search');?></h3>
-    <a class="close" href=""><i class="icon-remove-circle"></i></a>
-    <hr/>
-    <form action="tickets.php" method="post" id="search" name="search">
-        <input type="hidden" name="a" value="search">
-        <fieldset class="query">
-            <input type="input" id="query" name="query" size="20" placeholder="<?php echo __('Keywords') . ' &mdash; ' . __('Optional'); ?>">
-        </fieldset>
-        <fieldset class="span6">
-            <label for="statusId"><?php echo __('Statuses');?>:</label>
-            <select id="statusId" name="statusId">
-                 <option value="">&mdash; <?php echo __('Any Status');?> &mdash;</option>
-                <?php
-                foreach (TicketStatusList::getStatuses(
-                            array('states' => array('open', 'closed'))) as $s) {
-                    echo sprintf('<option data-state="%s" value="%d">%s</option>',
-                            $s->getState(), $s->getId(), __($s->getName()));
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="span6">
-            <label for="deptId"><?php echo __('Departments');?>:</label>
-            <select id="deptId" name="deptId">
-                <option value="">&mdash; <?php echo __('All Departments');?> &mdash;</option>
-                <?php
-                if(($mydepts = $thisstaff->getDepts()) && ($depts=Dept::getDepartments())) {
-                    foreach($depts as $id =>$name) {
-                        if(!in_array($id, $mydepts)) continue;
-                        echo sprintf('<option value="%d">%s</option>', $id, $name);
-                    }
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="span6">
-            <label for="flag"><?php echo __('Flags');?>:</label>
-            <select id="flag" name="flag">
-                 <option value="">&mdash; <?php echo __('Any Flags');?> &mdash;</option>
-                 <?php
-                 if (!$cfg->showAnsweredTickets()) { ?>
-                 <option data-state="open" value="answered"><?php echo __('Answered');?></option>
-                 <?php
-                 } ?>
-                 <option data-state="open" value="overdue"><?php echo __('Overdue');?></option>
-            </select>
-        </fieldset>
-        <fieldset class="owner span6">
-            <label for="assignee"><?php echo __('Assigned To');?>:</label>
-            <select id="assignee" name="assignee">
-                <option value="">&mdash; <?php echo __('Anyone');?> &mdash;</option>
-                <option value="0">&mdash; <?php echo __('Unassigned');?> &mdash;</option>
-                <option value="s<?php echo $thisstaff->getId(); ?>"><?php echo __('Me');?></option>
-                <?php
-                if(($users=Staff::getStaffMembers())) {
-                    echo '<OPTGROUP label="'.sprintf(__('Agents (%d)'),count($users)).'">';
-                    foreach($users as $id => $name) {
-                        $k="s$id";
-                        echo sprintf('<option value="%s">%s</option>', $k, $name);
-                    }
-                    echo '</OPTGROUP>';
-                }
-
-                if(($teams=Team::getTeams())) {
-                    echo '<OPTGROUP label="'.__('Teams').' ('.count($teams).')">';
-                    foreach($teams as $id => $name) {
-                        $k="t$id";
-                        echo sprintf('<option value="%s">%s</option>', $k, $name);
-                    }
-                    echo '</OPTGROUP>';
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="span6">
-            <label for="topicId"><?php echo __('Help Topics');?>:</label>
-            <select id="topicId" name="topicId">
-                <option value="" selected >&mdash; <?php echo __('All Help Topics');?> &mdash;</option>
-                <?php
-                if($topics=Topic::getHelpTopics()) {
-                    foreach($topics as $id =>$name)
-                        echo sprintf('<option value="%d" >%s</option>', $id, $name);
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="owner span6">
-            <label for="staffId"><?php echo __('Closed By');?>:</label>
-            <select id="staffId" name="staffId">
-                <option value="0">&mdash; <?php echo __('Anyone');?> &mdash;</option>
-                <option value="<?php echo $thisstaff->getId(); ?>"><?php echo __('Me');?></option>
-                <?php
-                if(($users=Staff::getStaffMembers())) {
-                    foreach($users as $id => $name)
-                        echo sprintf('<option value="%d">%s</option>', $id, $name);
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="date_range">
-            <label><?php echo __('Date Range').' &mdash; '.__('Create Date');?>:</label>
-            <input class="dp" type="input" size="20" name="startDate">
-            <span class="between"><?php echo __('TO');?></span>
-            <input class="dp" type="input" size="20" name="endDate">
-        </fieldset>
-        <?php
-        $tform = TicketForm::objects()->one();
-        echo $tform->getForm()->getMedia();
-        foreach ($tform->getInstance()->getFields() as $f) {
-            if (!$f->hasData())
-                continue;
-            elseif (!$f->getImpl()->hasSpecialSearch())
-                continue;
-            ?><fieldset class="span6">
-            <label><?php echo $f->getLabel(); ?>:</label><div><?php
-                     $f->render('search'); ?></div>
-            </fieldset>
-        <?php } ?>
-        <hr/>
-        <div id="result-count" class="clear"></div>
-        <p>
-            <span class="buttons pull-right">
-                <input type="submit" value="<?php echo __('Search');?>">
-            </span>
-            <span class="buttons pull-left">
-                <input type="reset" value="<?php echo __('Reset');?>">
-                <input type="button" value="<?php echo __('Cancel');?>" class="close">
-            </span>
-            <span class="spinner">
-                <img src="./images/ajax-loader.gif" width="16" height="16">
-            </span>
-        </p>
-    </form>
-</div>
 <script type="text/javascript">
 $(function() {
     $(document).off('.tickets');
@@ -691,3 +373,4 @@ $(function() {
     });
 });
 </script>
+
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index 595beac67aeeae21b3f07ae2976623b5a6fce595..595363a70fc0d9b2206f9b3054e4aca2deaa66e6 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-d7480e1c31a1f20d6954ecbb342722d3
+7c218d81e84b304c1436326c26ace09d
diff --git a/include/upgrader/streams/core/b26f29a6-d7480e1c.cleanup.sql b/include/upgrader/streams/core/b26f29a6-7c218d81.cleanup.sql
similarity index 100%
rename from include/upgrader/streams/core/b26f29a6-d7480e1c.cleanup.sql
rename to include/upgrader/streams/core/b26f29a6-7c218d81.cleanup.sql
diff --git a/include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql b/include/upgrader/streams/core/b26f29a6-7c218d81.patch.sql
similarity index 82%
rename from include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql
rename to include/upgrader/streams/core/b26f29a6-7c218d81.patch.sql
index d71ebd71f7038bc0b81ecabc28008cae21013db3..4d98136aa645bd97aaf5f32a775cb7e378ddbafe 100644
--- a/include/upgrader/streams/core/b26f29a6-d7480e1c.patch.sql
+++ b/include/upgrader/streams/core/b26f29a6-7c218d81.patch.sql
@@ -1,7 +1,7 @@
 /**
- * @signature d7480e1c31a1f20d6954ecbb342722d3
- * @version v1.9.5
- * @title Make editable content translatable
+ * @signature 7c218d81e84b304c1436326c26ace09d
+ * @version v1.9.6
+ * @title Make editable content translatable and add queues
  *
  * This patch adds support for translatable administratively editable
  * content, such as help topic names, department and group names, site page
@@ -28,7 +28,6 @@ ALTER TABLE `%TABLE_PREFIX%user_account`
     ADD `timezone` varchar(64) DEFAULT NULL AFTER `status`,
     ADD `extra` text AFTER `backend`;
 
-DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`;
 CREATE TABLE `%TABLE_PREFIX%translation` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `object_hash` char(16) CHARACTER SET ascii DEFAULT NULL,
@@ -51,7 +50,7 @@ CREATE TABLE `%TABLE_PREFIX%_timezones` (
     `offset` int,
     `dst` tinyint(1) unsigned,
     `south` tinyint(1) unsigned default 0,
-    `olson_name` varchar(32) 
+    `olson_name` varchar(32)
 ) DEFAULT CHARSET=utf8;
 
 INSERT INTO `%TABLE_PREFIX%_timezones` (`offset`, `dst`, `olson_name`) VALUES
@@ -149,7 +148,31 @@ UPDATE `%TABLE_PREFIX%user_account` A1
 
 DROP TABLE %TABLE_PREFIX%_timezones;
 
+ALTER TABLE `%TABLE_PREFIX%ticket`
+    ADD `est_duedate` datetime default NULL AFTER `duedate`,
+    ADD `lastupdate` datetime default NULL AFTER `lastresponse`;
+
+UPDATE `%TABLE_PREFIX%ticket` A1
+    JOIN `%TABLE_PREFIX%sla` A2 ON (A1.sla_id = A2.id)
+    SET A1.`est_duedate` =
+        COALESCE(A1.`duedate`, A1.`created` + INTERVAL A2.`grace_period` HOUR),
+      A1.`lastupdate` =
+        CAST(GREATEST(IFNULL(A1.lastmessage, 0), IFNULL(A1.closed, 0), IFNULL(A1.reopened, 0), A1.created) as DATETIME);
+
+CREATE TABLE `%TABLE_PREFIX%queue` (
+  `id` int(11) unsigned not null auto_increment,
+  `parent_id` int(11) unsigned not null default 0,
+  `flags` int(11) unsigned not null default 0,
+  `staff_id` int(11) unsigned not null default 0,
+  `sort` int(11) unsigned not null default 0,
+  `title` varchar(60),
+  `config` text,
+  `created` datetime not null,
+  `updated` datetime not null,
+  primary key (`id`)
+) DEFAULT CHARSET=utf8;
+
 -- Finished with patch
 UPDATE `%TABLE_PREFIX%config`
-    SET `value` = 'd7480e1c31a1f20d6954ecbb342722d3'
+    SET `value` = '7c218d81e84b304c1436326c26ace09d'
     WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/b26f29a6-d7480e1c.task.php b/include/upgrader/streams/core/b26f29a6-7c218d81.task.php
similarity index 100%
rename from include/upgrader/streams/core/b26f29a6-d7480e1c.task.php
rename to include/upgrader/streams/core/b26f29a6-7c218d81.task.php
diff --git a/js/chosen.jquery.min.js b/js/chosen.jquery.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c564f995a397b8b0315b44dbd3eb3956e1c57df
--- /dev/null
+++ b/js/chosen.jquery.min.js
@@ -0,0 +1,2 @@
+/* Chosen v1.2.0 | (c) 2011-2014 by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */
+!function(){var a,AbstractChosen,Chosen,SelectParser,b,c={}.hasOwnProperty,d=function(a,b){function d(){this.constructor=a}for(var e in b)c.call(b,e)&&(a[e]=b[e]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};SelectParser=function(){function SelectParser(){this.options_index=0,this.parsed=[]}return SelectParser.prototype.add_node=function(a){return"OPTGROUP"===a.nodeName.toUpperCase()?this.add_group(a):this.add_option(a)},SelectParser.prototype.add_group=function(a){var b,c,d,e,f,g;for(b=this.parsed.length,this.parsed.push({array_index:b,group:!0,label:this.escapeExpression(a.label),children:0,disabled:a.disabled}),f=a.childNodes,g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(this.add_option(c,b,a.disabled));return g},SelectParser.prototype.add_option=function(a,b,c){return"OPTION"===a.nodeName.toUpperCase()?(""!==a.text?(null!=b&&(this.parsed[b].children+=1),this.parsed.push({array_index:this.parsed.length,options_index:this.options_index,value:a.value,text:a.text,html:a.innerHTML,selected:a.selected,disabled:c===!0?c:a.disabled,group_array_index:b,classes:a.className,style:a.style.cssText})):this.parsed.push({array_index:this.parsed.length,options_index:this.options_index,empty:!0}),this.options_index+=1):void 0},SelectParser.prototype.escapeExpression=function(a){var b,c;return null==a||a===!1?"":/[\&\<\>\"\'\`]/.test(a)?(b={"<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},c=/&(?!\w+;)|[\<\>\"\'\`]/g,a.replace(c,function(a){return b[a]||"&amp;"})):a},SelectParser}(),SelectParser.select_to_array=function(a){var b,c,d,e,f;for(c=new SelectParser,f=a.childNodes,d=0,e=f.length;e>d;d++)b=f[d],c.add_node(b);return c.parsed},AbstractChosen=function(){function AbstractChosen(a,b){this.form_field=a,this.options=null!=b?b:{},AbstractChosen.browser_is_supported()&&(this.is_multiple=this.form_field.multiple,this.set_default_text(),this.set_default_values(),this.setup(),this.set_up_html(),this.register_observers())}return AbstractChosen.prototype.set_default_values=function(){var a=this;return this.click_test_action=function(b){return a.test_active_click(b)},this.activate_action=function(b){return a.activate_field(b)},this.active_field=!1,this.mouse_on_container=!1,this.results_showing=!1,this.result_highlighted=null,this.allow_single_deselect=null!=this.options.allow_single_deselect&&null!=this.form_field.options[0]&&""===this.form_field.options[0].text?this.options.allow_single_deselect:!1,this.disable_search_threshold=this.options.disable_search_threshold||0,this.disable_search=this.options.disable_search||!1,this.enable_split_word_search=null!=this.options.enable_split_word_search?this.options.enable_split_word_search:!0,this.group_search=null!=this.options.group_search?this.options.group_search:!0,this.search_contains=this.options.search_contains||!1,this.single_backstroke_delete=null!=this.options.single_backstroke_delete?this.options.single_backstroke_delete:!0,this.max_selected_options=this.options.max_selected_options||1/0,this.inherit_select_classes=this.options.inherit_select_classes||!1,this.display_selected_options=null!=this.options.display_selected_options?this.options.display_selected_options:!0,this.display_disabled_options=null!=this.options.display_disabled_options?this.options.display_disabled_options:!0},AbstractChosen.prototype.set_default_text=function(){return this.default_text=this.form_field.getAttribute("data-placeholder")?this.form_field.getAttribute("data-placeholder"):this.is_multiple?this.options.placeholder_text_multiple||this.options.placeholder_text||AbstractChosen.default_multiple_text:this.options.placeholder_text_single||this.options.placeholder_text||AbstractChosen.default_single_text,this.results_none_found=this.form_field.getAttribute("data-no_results_text")||this.options.no_results_text||AbstractChosen.default_no_result_text},AbstractChosen.prototype.mouse_enter=function(){return this.mouse_on_container=!0},AbstractChosen.prototype.mouse_leave=function(){return this.mouse_on_container=!1},AbstractChosen.prototype.input_focus=function(){var a=this;if(this.is_multiple){if(!this.active_field)return setTimeout(function(){return a.container_mousedown()},50)}else if(!this.active_field)return this.activate_field()},AbstractChosen.prototype.input_blur=function(){var a=this;return this.mouse_on_container?void 0:(this.active_field=!1,setTimeout(function(){return a.blur_test()},100))},AbstractChosen.prototype.results_option_build=function(a){var b,c,d,e,f;for(b="",f=this.results_data,d=0,e=f.length;e>d;d++)c=f[d],b+=c.group?this.result_add_group(c):this.result_add_option(c),(null!=a?a.first:void 0)&&(c.selected&&this.is_multiple?this.choice_build(c):c.selected&&!this.is_multiple&&this.single_set_selected_text(c.text));return b},AbstractChosen.prototype.result_add_option=function(a){var b,c;return a.search_match?this.include_option_in_results(a)?(b=[],a.disabled||a.selected&&this.is_multiple||b.push("active-result"),!a.disabled||a.selected&&this.is_multiple||b.push("disabled-result"),a.selected&&b.push("result-selected"),null!=a.group_array_index&&b.push("group-option"),""!==a.classes&&b.push(a.classes),c=document.createElement("li"),c.className=b.join(" "),c.style.cssText=a.style,c.setAttribute("data-option-array-index",a.array_index),c.innerHTML=a.search_text,this.outerHTML(c)):"":""},AbstractChosen.prototype.result_add_group=function(a){var b;return a.search_match||a.group_match?a.active_options>0?(b=document.createElement("li"),b.className="group-result",b.innerHTML=a.search_text,this.outerHTML(b)):"":""},AbstractChosen.prototype.results_update_field=function(){return this.set_default_text(),this.is_multiple||this.results_reset_cleanup(),this.result_clear_highlight(),this.results_build(),this.results_showing?this.winnow_results():void 0},AbstractChosen.prototype.reset_single_select_options=function(){var a,b,c,d,e;for(d=this.results_data,e=[],b=0,c=d.length;c>b;b++)a=d[b],a.selected?e.push(a.selected=!1):e.push(void 0);return e},AbstractChosen.prototype.results_toggle=function(){return this.results_showing?this.results_hide():this.results_show()},AbstractChosen.prototype.results_search=function(){return this.results_showing?this.winnow_results():this.results_show()},AbstractChosen.prototype.winnow_results=function(){var a,b,c,d,e,f,g,h,i,j,k,l;for(this.no_results_clear(),d=0,f=this.get_search_text(),a=f.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),i=new RegExp(a,"i"),c=this.get_search_regex(a),l=this.results_data,j=0,k=l.length;k>j;j++)b=l[j],b.search_match=!1,e=null,this.include_option_in_results(b)&&(b.group&&(b.group_match=!1,b.active_options=0),null!=b.group_array_index&&this.results_data[b.group_array_index]&&(e=this.results_data[b.group_array_index],0===e.active_options&&e.search_match&&(d+=1),e.active_options+=1),(!b.group||this.group_search)&&(b.search_text=b.group?b.label:b.text,b.search_match=this.search_string_match(b.search_text,c),b.search_match&&!b.group&&(d+=1),b.search_match?(f.length&&(g=b.search_text.search(i),h=b.search_text.substr(0,g+f.length)+"</em>"+b.search_text.substr(g+f.length),b.search_text=h.substr(0,g)+"<em>"+h.substr(g)),null!=e&&(e.group_match=!0)):null!=b.group_array_index&&this.results_data[b.group_array_index].search_match&&(b.search_match=!0)));return this.result_clear_highlight(),1>d&&f.length?(this.update_results_content(""),this.no_results(f)):(this.update_results_content(this.results_option_build()),this.winnow_results_set_highlight())},AbstractChosen.prototype.get_search_regex=function(a){var b;return b=this.search_contains?"":"^",new RegExp(b+a,"i")},AbstractChosen.prototype.search_string_match=function(a,b){var c,d,e,f;if(b.test(a))return!0;if(this.enable_split_word_search&&(a.indexOf(" ")>=0||0===a.indexOf("["))&&(d=a.replace(/\[|\]/g,"").split(" "),d.length))for(e=0,f=d.length;f>e;e++)if(c=d[e],b.test(c))return!0},AbstractChosen.prototype.choices_count=function(){var a,b,c,d;if(null!=this.selected_option_count)return this.selected_option_count;for(this.selected_option_count=0,d=this.form_field.options,b=0,c=d.length;c>b;b++)a=d[b],a.selected&&(this.selected_option_count+=1);return this.selected_option_count},AbstractChosen.prototype.choices_click=function(a){return a.preventDefault(),this.results_showing||this.is_disabled?void 0:this.results_show()},AbstractChosen.prototype.keyup_checker=function(a){var b,c;switch(b=null!=(c=a.which)?c:a.keyCode,this.search_field_scale(),b){case 8:if(this.is_multiple&&this.backstroke_length<1&&this.choices_count()>0)return this.keydown_backstroke();if(!this.pending_backstroke)return this.result_clear_highlight(),this.results_search();break;case 13:if(a.preventDefault(),this.results_showing)return this.result_select(a);break;case 27:return this.results_showing&&this.results_hide(),!0;case 9:case 38:case 40:case 16:case 91:case 17:break;default:return this.results_search()}},AbstractChosen.prototype.clipboard_event_checker=function(){var a=this;return setTimeout(function(){return a.results_search()},50)},AbstractChosen.prototype.container_width=function(){return null!=this.options.width?this.options.width:""+this.form_field.offsetWidth+"px"},AbstractChosen.prototype.include_option_in_results=function(a){return this.is_multiple&&!this.display_selected_options&&a.selected?!1:!this.display_disabled_options&&a.disabled?!1:a.empty?!1:!0},AbstractChosen.prototype.search_results_touchstart=function(a){return this.touch_started=!0,this.search_results_mouseover(a)},AbstractChosen.prototype.search_results_touchmove=function(a){return this.touch_started=!1,this.search_results_mouseout(a)},AbstractChosen.prototype.search_results_touchend=function(a){return this.touch_started?this.search_results_mouseup(a):void 0},AbstractChosen.prototype.outerHTML=function(a){var b;return a.outerHTML?a.outerHTML:(b=document.createElement("div"),b.appendChild(a),b.innerHTML)},AbstractChosen.browser_is_supported=function(){return"Microsoft Internet Explorer"===window.navigator.appName?document.documentMode>=8:/iP(od|hone)/i.test(window.navigator.userAgent)?!1:/Android/i.test(window.navigator.userAgent)&&/Mobile/i.test(window.navigator.userAgent)?!1:!0},AbstractChosen.default_multiple_text="Select Some Options",AbstractChosen.default_single_text="Select an Option",AbstractChosen.default_no_result_text="No results match",AbstractChosen}(),a=jQuery,a.fn.extend({chosen:function(b){return AbstractChosen.browser_is_supported()?this.each(function(){var c,d;c=a(this),d=c.data("chosen"),"destroy"===b&&d instanceof Chosen?d.destroy():d instanceof Chosen||c.data("chosen",new Chosen(this,b))}):this}}),Chosen=function(c){function Chosen(){return b=Chosen.__super__.constructor.apply(this,arguments)}return d(Chosen,c),Chosen.prototype.setup=function(){return this.form_field_jq=a(this.form_field),this.current_selectedIndex=this.form_field.selectedIndex,this.is_rtl=this.form_field_jq.hasClass("chosen-rtl")},Chosen.prototype.set_up_html=function(){var b,c;return b=["chosen-container"],b.push("chosen-container-"+(this.is_multiple?"multi":"single")),this.inherit_select_classes&&this.form_field.className&&b.push(this.form_field.className),this.is_rtl&&b.push("chosen-rtl"),c={"class":b.join(" "),style:"width: "+this.container_width()+";",title:this.form_field.title},this.form_field.id.length&&(c.id=this.form_field.id.replace(/[^\w]/g,"_")+"_chosen"),this.container=a("<div />",c),this.is_multiple?this.container.html('<ul class="chosen-choices"><li class="search-field"><input type="text" value="'+this.default_text+'" class="default" autocomplete="off" style="width:25px;" /></li></ul><div class="chosen-drop"><ul class="chosen-results"></ul></div>'):this.container.html('<a class="chosen-single chosen-default" tabindex="-1"><span>'+this.default_text+'</span><div><b></b></div></a><div class="chosen-drop"><div class="chosen-search"><input type="text" autocomplete="off" /></div><ul class="chosen-results"></ul></div>'),this.form_field_jq.hide().after(this.container),this.dropdown=this.container.find("div.chosen-drop").first(),this.search_field=this.container.find("input").first(),this.search_results=this.container.find("ul.chosen-results").first(),this.search_field_scale(),this.search_no_results=this.container.find("li.no-results").first(),this.is_multiple?(this.search_choices=this.container.find("ul.chosen-choices").first(),this.search_container=this.container.find("li.search-field").first()):(this.search_container=this.container.find("div.chosen-search").first(),this.selected_item=this.container.find(".chosen-single").first()),this.results_build(),this.set_tab_index(),this.set_label_behavior(),this.form_field_jq.trigger("chosen:ready",{chosen:this})},Chosen.prototype.register_observers=function(){var a=this;return this.container.bind("touchstart.chosen",function(b){a.container_mousedown(b)}),this.container.bind("touchend.chosen",function(b){a.container_mouseup(b)}),this.container.bind("mousedown.chosen",function(b){a.container_mousedown(b)}),this.container.bind("mouseup.chosen",function(b){a.container_mouseup(b)}),this.container.bind("mouseenter.chosen",function(b){a.mouse_enter(b)}),this.container.bind("mouseleave.chosen",function(b){a.mouse_leave(b)}),this.search_results.bind("mouseup.chosen",function(b){a.search_results_mouseup(b)}),this.search_results.bind("mouseover.chosen",function(b){a.search_results_mouseover(b)}),this.search_results.bind("mouseout.chosen",function(b){a.search_results_mouseout(b)}),this.search_results.bind("mousewheel.chosen DOMMouseScroll.chosen",function(b){a.search_results_mousewheel(b)}),this.search_results.bind("touchstart.chosen",function(b){a.search_results_touchstart(b)}),this.search_results.bind("touchmove.chosen",function(b){a.search_results_touchmove(b)}),this.search_results.bind("touchend.chosen",function(b){a.search_results_touchend(b)}),this.form_field_jq.bind("chosen:updated.chosen",function(b){a.results_update_field(b)}),this.form_field_jq.bind("chosen:activate.chosen",function(b){a.activate_field(b)}),this.form_field_jq.bind("chosen:open.chosen",function(b){a.container_mousedown(b)}),this.form_field_jq.bind("chosen:close.chosen",function(b){a.input_blur(b)}),this.search_field.bind("blur.chosen",function(b){a.input_blur(b)}),this.search_field.bind("keyup.chosen",function(b){a.keyup_checker(b)}),this.search_field.bind("keydown.chosen",function(b){a.keydown_checker(b)}),this.search_field.bind("focus.chosen",function(b){a.input_focus(b)}),this.search_field.bind("cut.chosen",function(b){a.clipboard_event_checker(b)}),this.search_field.bind("paste.chosen",function(b){a.clipboard_event_checker(b)}),this.is_multiple?this.search_choices.bind("click.chosen",function(b){a.choices_click(b)}):this.container.bind("click.chosen",function(a){a.preventDefault()})},Chosen.prototype.destroy=function(){return a(this.container[0].ownerDocument).unbind("click.chosen",this.click_test_action),this.search_field[0].tabIndex&&(this.form_field_jq[0].tabIndex=this.search_field[0].tabIndex),this.container.remove(),this.form_field_jq.removeData("chosen"),this.form_field_jq.show()},Chosen.prototype.search_field_disabled=function(){return this.is_disabled=this.form_field_jq[0].disabled,this.is_disabled?(this.container.addClass("chosen-disabled"),this.search_field[0].disabled=!0,this.is_multiple||this.selected_item.unbind("focus.chosen",this.activate_action),this.close_field()):(this.container.removeClass("chosen-disabled"),this.search_field[0].disabled=!1,this.is_multiple?void 0:this.selected_item.bind("focus.chosen",this.activate_action))},Chosen.prototype.container_mousedown=function(b){return this.is_disabled||(b&&"mousedown"===b.type&&!this.results_showing&&b.preventDefault(),null!=b&&a(b.target).hasClass("search-choice-close"))?void 0:(this.active_field?this.is_multiple||!b||a(b.target)[0]!==this.selected_item[0]&&!a(b.target).parents("a.chosen-single").length||(b.preventDefault(),this.results_toggle()):(this.is_multiple&&this.search_field.val(""),a(this.container[0].ownerDocument).bind("click.chosen",this.click_test_action),this.results_show()),this.activate_field())},Chosen.prototype.container_mouseup=function(a){return"ABBR"!==a.target.nodeName||this.is_disabled?void 0:this.results_reset(a)},Chosen.prototype.search_results_mousewheel=function(a){var b;return a.originalEvent&&(b=a.originalEvent.deltaY||-a.originalEvent.wheelDelta||a.originalEvent.detail),null!=b?(a.preventDefault(),"DOMMouseScroll"===a.type&&(b=40*b),this.search_results.scrollTop(b+this.search_results.scrollTop())):void 0},Chosen.prototype.blur_test=function(){return!this.active_field&&this.container.hasClass("chosen-container-active")?this.close_field():void 0},Chosen.prototype.close_field=function(){return a(this.container[0].ownerDocument).unbind("click.chosen",this.click_test_action),this.active_field=!1,this.results_hide(),this.container.removeClass("chosen-container-active"),this.clear_backstroke(),this.show_search_field_default(),this.search_field_scale()},Chosen.prototype.activate_field=function(){return this.container.addClass("chosen-container-active"),this.active_field=!0,this.search_field.val(this.search_field.val()),this.search_field.focus()},Chosen.prototype.test_active_click=function(b){var c;return c=a(b.target).closest(".chosen-container"),c.length&&this.container[0]===c[0]?this.active_field=!0:this.close_field()},Chosen.prototype.results_build=function(){return this.parsing=!0,this.selected_option_count=null,this.results_data=SelectParser.select_to_array(this.form_field),this.is_multiple?this.search_choices.find("li.search-choice").remove():this.is_multiple||(this.single_set_selected_text(),this.disable_search||this.form_field.options.length<=this.disable_search_threshold?(this.search_field[0].readOnly=!0,this.container.addClass("chosen-container-single-nosearch")):(this.search_field[0].readOnly=!1,this.container.removeClass("chosen-container-single-nosearch"))),this.update_results_content(this.results_option_build({first:!0})),this.search_field_disabled(),this.show_search_field_default(),this.search_field_scale(),this.parsing=!1},Chosen.prototype.result_do_highlight=function(a){var b,c,d,e,f;if(a.length){if(this.result_clear_highlight(),this.result_highlight=a,this.result_highlight.addClass("highlighted"),d=parseInt(this.search_results.css("maxHeight"),10),f=this.search_results.scrollTop(),e=d+f,c=this.result_highlight.position().top+this.search_results.scrollTop(),b=c+this.result_highlight.outerHeight(),b>=e)return this.search_results.scrollTop(b-d>0?b-d:0);if(f>c)return this.search_results.scrollTop(c)}},Chosen.prototype.result_clear_highlight=function(){return this.result_highlight&&this.result_highlight.removeClass("highlighted"),this.result_highlight=null},Chosen.prototype.results_show=function(){return this.is_multiple&&this.max_selected_options<=this.choices_count()?(this.form_field_jq.trigger("chosen:maxselected",{chosen:this}),!1):(this.container.addClass("chosen-with-drop"),this.results_showing=!0,this.search_field.focus(),this.search_field.val(this.search_field.val()),this.winnow_results(),this.form_field_jq.trigger("chosen:showing_dropdown",{chosen:this}))},Chosen.prototype.update_results_content=function(a){return this.search_results.html(a)},Chosen.prototype.results_hide=function(){return this.results_showing&&(this.result_clear_highlight(),this.container.removeClass("chosen-with-drop"),this.form_field_jq.trigger("chosen:hiding_dropdown",{chosen:this})),this.results_showing=!1},Chosen.prototype.set_tab_index=function(){var a;return this.form_field.tabIndex?(a=this.form_field.tabIndex,this.form_field.tabIndex=-1,this.search_field[0].tabIndex=a):void 0},Chosen.prototype.set_label_behavior=function(){var b=this;return this.form_field_label=this.form_field_jq.parents("label"),!this.form_field_label.length&&this.form_field.id.length&&(this.form_field_label=a("label[for='"+this.form_field.id+"']")),this.form_field_label.length>0?this.form_field_label.bind("click.chosen",function(a){return b.is_multiple?b.container_mousedown(a):b.activate_field()}):void 0},Chosen.prototype.show_search_field_default=function(){return this.is_multiple&&this.choices_count()<1&&!this.active_field?(this.search_field.val(this.default_text),this.search_field.addClass("default")):(this.search_field.val(""),this.search_field.removeClass("default"))},Chosen.prototype.search_results_mouseup=function(b){var c;return c=a(b.target).hasClass("active-result")?a(b.target):a(b.target).parents(".active-result").first(),c.length?(this.result_highlight=c,this.result_select(b),this.search_field.focus()):void 0},Chosen.prototype.search_results_mouseover=function(b){var c;return c=a(b.target).hasClass("active-result")?a(b.target):a(b.target).parents(".active-result").first(),c?this.result_do_highlight(c):void 0},Chosen.prototype.search_results_mouseout=function(b){return a(b.target).hasClass("active-result")?this.result_clear_highlight():void 0},Chosen.prototype.choice_build=function(b){var c,d,e=this;return c=a("<li />",{"class":"search-choice"}).html("<span>"+b.html+"</span>"),b.disabled?c.addClass("search-choice-disabled"):(d=a("<a />",{"class":"search-choice-close","data-option-array-index":b.array_index}),d.bind("click.chosen",function(a){return e.choice_destroy_link_click(a)}),c.append(d)),this.search_container.before(c)},Chosen.prototype.choice_destroy_link_click=function(b){return b.preventDefault(),b.stopPropagation(),this.is_disabled?void 0:this.choice_destroy(a(b.target))},Chosen.prototype.choice_destroy=function(a){return this.result_deselect(a[0].getAttribute("data-option-array-index"))?(this.show_search_field_default(),this.is_multiple&&this.choices_count()>0&&this.search_field.val().length<1&&this.results_hide(),a.parents("li").first().remove(),this.search_field_scale()):void 0},Chosen.prototype.results_reset=function(){return this.reset_single_select_options(),this.form_field.options[0].selected=!0,this.single_set_selected_text(),this.show_search_field_default(),this.results_reset_cleanup(),this.form_field_jq.trigger("change"),this.active_field?this.results_hide():void 0},Chosen.prototype.results_reset_cleanup=function(){return this.current_selectedIndex=this.form_field.selectedIndex,this.selected_item.find("abbr").remove()},Chosen.prototype.result_select=function(a){var b,c;return this.result_highlight?(b=this.result_highlight,this.result_clear_highlight(),this.is_multiple&&this.max_selected_options<=this.choices_count()?(this.form_field_jq.trigger("chosen:maxselected",{chosen:this}),!1):(this.is_multiple?b.removeClass("active-result"):this.reset_single_select_options(),c=this.results_data[b[0].getAttribute("data-option-array-index")],c.selected=!0,this.form_field.options[c.options_index].selected=!0,this.selected_option_count=null,this.is_multiple?this.choice_build(c):this.single_set_selected_text(c.text),(a.metaKey||a.ctrlKey)&&this.is_multiple||this.results_hide(),this.search_field.val(""),(this.is_multiple||this.form_field.selectedIndex!==this.current_selectedIndex)&&this.form_field_jq.trigger("change",{selected:this.form_field.options[c.options_index].value}),this.current_selectedIndex=this.form_field.selectedIndex,this.search_field_scale())):void 0},Chosen.prototype.single_set_selected_text=function(a){return null==a&&(a=this.default_text),a===this.default_text?this.selected_item.addClass("chosen-default"):(this.single_deselect_control_build(),this.selected_item.removeClass("chosen-default")),this.selected_item.find("span").text(a)},Chosen.prototype.result_deselect=function(a){var b;return b=this.results_data[a],this.form_field.options[b.options_index].disabled?!1:(b.selected=!1,this.form_field.options[b.options_index].selected=!1,this.selected_option_count=null,this.result_clear_highlight(),this.results_showing&&this.winnow_results(),this.form_field_jq.trigger("change",{deselected:this.form_field.options[b.options_index].value}),this.search_field_scale(),!0)},Chosen.prototype.single_deselect_control_build=function(){return this.allow_single_deselect?(this.selected_item.find("abbr").length||this.selected_item.find("span").first().after('<abbr class="search-choice-close"></abbr>'),this.selected_item.addClass("chosen-single-with-deselect")):void 0},Chosen.prototype.get_search_text=function(){return this.search_field.val()===this.default_text?"":a("<div/>").text(a.trim(this.search_field.val())).html()},Chosen.prototype.winnow_results_set_highlight=function(){var a,b;return b=this.is_multiple?[]:this.search_results.find(".result-selected.active-result"),a=b.length?b.first():this.search_results.find(".active-result").first(),null!=a?this.result_do_highlight(a):void 0},Chosen.prototype.no_results=function(b){var c;return c=a('<li class="no-results">'+this.results_none_found+' "<span></span>"</li>'),c.find("span").first().html(b),this.search_results.append(c),this.form_field_jq.trigger("chosen:no_results",{chosen:this})},Chosen.prototype.no_results_clear=function(){return this.search_results.find(".no-results").remove()},Chosen.prototype.keydown_arrow=function(){var a;return this.results_showing&&this.result_highlight?(a=this.result_highlight.nextAll("li.active-result").first())?this.result_do_highlight(a):void 0:this.results_show()},Chosen.prototype.keyup_arrow=function(){var a;return this.results_showing||this.is_multiple?this.result_highlight?(a=this.result_highlight.prevAll("li.active-result"),a.length?this.result_do_highlight(a.first()):(this.choices_count()>0&&this.results_hide(),this.result_clear_highlight())):void 0:this.results_show()},Chosen.prototype.keydown_backstroke=function(){var a;return this.pending_backstroke?(this.choice_destroy(this.pending_backstroke.find("a").first()),this.clear_backstroke()):(a=this.search_container.siblings("li.search-choice").last(),a.length&&!a.hasClass("search-choice-disabled")?(this.pending_backstroke=a,this.single_backstroke_delete?this.keydown_backstroke():this.pending_backstroke.addClass("search-choice-focus")):void 0)},Chosen.prototype.clear_backstroke=function(){return this.pending_backstroke&&this.pending_backstroke.removeClass("search-choice-focus"),this.pending_backstroke=null},Chosen.prototype.keydown_checker=function(a){var b,c;switch(b=null!=(c=a.which)?c:a.keyCode,this.search_field_scale(),8!==b&&this.pending_backstroke&&this.clear_backstroke(),b){case 8:this.backstroke_length=this.search_field.val().length;break;case 9:this.results_showing&&!this.is_multiple&&this.result_select(a),this.mouse_on_container=!1;break;case 13:this.results_showing&&a.preventDefault();break;case 32:this.disable_search&&a.preventDefault();break;case 38:a.preventDefault(),this.keyup_arrow();break;case 40:a.preventDefault(),this.keydown_arrow()}},Chosen.prototype.search_field_scale=function(){var b,c,d,e,f,g,h,i,j;if(this.is_multiple){for(d=0,h=0,f="position:absolute; left: -1000px; top: -1000px; display:none;",g=["font-size","font-style","font-weight","font-family","line-height","text-transform","letter-spacing"],i=0,j=g.length;j>i;i++)e=g[i],f+=e+":"+this.search_field.css(e)+";";return b=a("<div />",{style:f}),b.text(this.search_field.val()),a("body").append(b),h=b.width()+25,b.remove(),c=this.container.outerWidth(),h>c-10&&(h=c-10),this.search_field.css({width:h+"px"})}},Chosen}(AbstractChosen)}.call(this);
\ No newline at end of file
diff --git a/js/jquery.multiselect.filter.min.js b/js/jquery.multiselect.filter.min.js
deleted file mode 100644
index db828899af7fec5044190a002a777654ec33c514..0000000000000000000000000000000000000000
--- a/js/jquery.multiselect.filter.min.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * jQuery MultiSelect UI Widget Filtering Plugin 1.4
- * Copyright (c) 2011 Eric Hynds
- *
- * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
- *
- * Depends:
- *   - jQuery UI MultiSelect widget
- *
- * Dual licensed under the MIT and GPL licenses:
- *   http://www.opensource.org/licenses/mit-license.php
- *   http://www.gnu.org/licenses/gpl.html
- *
-*/
-(function(a){var f=/[\-\[\]{}()*+?.,\\\^$|#\s]/g;a.widget("ech.multiselectfilter",{options:{label:"Filter:",width:null,placeholder:"Enter keywords",autoReset:!1},_create:function(){var e;var b=this,c=this.options,d=this.instance=a(this.element).data("echMultiselect");this.header=d.menu.find(".ui-multiselect-header").addClass("ui-multiselect-hasfilter");e=this.wrapper=a('<div class="ui-multiselect-filter">'+(c.label.length?c.label:"")+'<input placeholder="'+c.placeholder+'" type="search"'+(/\d/.test(c.width)?  'style="width:'+c.width+'px"':"")+" /></div>").prependTo(this.header),c=e;this.inputs=d.menu.find('input[type="checkbox"], input[type="radio"]');this.input=c.find("input").bind({keydown:function(a){13===a.which&&a.preventDefault()},keyup:a.proxy(b._handler,b),click:a.proxy(b._handler,b)});this.updateCache();d._toggleChecked=function(c,d){var e=d&&d.length?d:this.labels.find("input"),i=this,e=e.not(b.instance._isOpen?":disabled, :hidden":":disabled").each(this._toggleState("checked",c));this.update(); var j=e.map(function(){return this.value}).get();this.element.find("option").filter(function(){!this.disabled&&-1<a.inArray(this.value,j)&&i._toggleState("selected",c).call(this)})};d=a(document).bind("multiselectrefresh",function(){b.updateCache();b._handler()});this.options.autoReset&&d.bind("multiselectclose",a.proxy(this._reset,this))},_handler:function(b){var c=a.trim(this.input[0].value.toLowerCase()),d=this.rows,g=this.inputs,h=this.cache;if(c){d.hide();var e=RegExp(c.replace(f,"\\$&"),"gi"); this._trigger("filter",b,a.map(h,function(a,b){return-1!==a.search(e)?(d.eq(b).show(),g.get(b)):null}))}else d.show();this.instance.menu.find(".ui-multiselect-optgroup-label").each(function(){var b=a(this),c=b.nextUntil(".ui-multiselect-optgroup-label").filter(function(){return"none"!==a.css(this,"display")}).length;b[c?"show":"hide"]()})},_reset:function(){this.input.val("").trigger("keyup")},updateCache:function(){this.rows=this.instance.menu.find(".ui-multiselect-checkboxes li:not(.ui-multiselect-optgroup-label)");this.cache=this.element.children().map(function(){var b=a(this);"optgroup"===this.tagName.toLowerCase()&&(b=b.children());return b.map(function(){return this.innerHTML.toLowerCase()}).get()}).get()},widget:function(){return this.wrapper},destroy:function(){a.Widget.prototype.destroy.call(this);this.input.val("").trigger("keyup");this.wrapper.remove()}})})(jQuery);
diff --git a/js/jquery.multiselect.min.js b/js/jquery.multiselect.min.js
deleted file mode 100644
index e9243506c0555f0d53b58b40d5d185ace8035b7c..0000000000000000000000000000000000000000
--- a/js/jquery.multiselect.min.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * jQuery MultiSelect UI Widget 1.13
- * Copyright (c) 2012 Eric Hynds
- *
- * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
- *
- * Depends:
- *   - jQuery 1.4.2+
- *   - jQuery UI 1.8 widget factory
- *
- * Optional:
- *   - jQuery UI effects
- *   - jQuery UI position utility
- *
- * Dual licensed under the MIT and GPL licenses:
- *   http://www.opensource.org/licenses/mit-license.php
- *   http://www.gnu.org/licenses/gpl.html
- *
- */
-(function(d){var k=0;d.widget("ech.multiselect",{options:{header:!0,height:175,minWidth:225,classes:"",checkAllText:"Check all",uncheckAllText:"Uncheck all",noneSelectedText:"Select options",selectedText:"# selected",selectedList:0,show:null,hide:null,autoOpen:!1,multiple:!0,position:{}},_create:function(){var a=this.element.hide(),b=this.options;this.speed=d.fx.speeds._default;this._isOpen=!1;a=(this.button=d('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></button>')).addClass("ui-multiselect ui-widget ui-state-default ui-corner-all").addClass(b.classes).attr({title:a.attr("title"),"aria-haspopup":!0,tabIndex:a.attr("tabIndex")}).insertAfter(a);(this.buttonlabel=d("<span />")).html(b.noneSelectedText).appendTo(a);var a=(this.menu=d("<div />")).addClass("ui-multiselect-menu ui-widget ui-widget-content ui-corner-all").addClass(b.classes).appendTo(document.body),c=(this.header=d("<div />")).addClass("ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix").appendTo(a);(this.headerLinkContainer=d("<ul />")).addClass("ui-helper-reset").html(function(){return!0===b.header?'<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>'+b.checkAllText+'</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>'+b.uncheckAllText+"</span></a></li>":"string"===typeof b.header?"<li>"+b.header+"</li>":""}).append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>').appendTo(c);(this.checkboxContainer=d("<ul />")).addClass("ui-multiselect-checkboxes ui-helper-reset").appendTo(a);this._bindEvents();this.refresh(!0);b.multiple||a.addClass("ui-multiselect-single")},_init:function(){!1===this.options.header&&this.header.hide();this.options.multiple||this.headerLinkContainer.find(".ui-multiselect-all, .ui-multiselect-none").hide();this.options.autoOpen&&this.open();this.element.is(":disabled")&&this.disable()},refresh:function(a){var b=this.element,c=this.options,f=this.menu,h=this.checkboxContainer,g=[],e="",i=b.attr("id")||k++;b.find("option").each(function(b){d(this);var a=this.parentNode,f=this.innerHTML,h=this.title,k=this.value,b="ui-multiselect-"+(this.id||i+"-option-"+b),l=this.disabled,n=this.selected,m=["ui-corner-all"],o=(l?"ui-multiselect-disabled ":" ")+this.className,j;"OPTGROUP"===a.tagName&&(j=a.getAttribute("label"),-1===d.inArray(j,g)&&(e+='<li class="ui-multiselect-optgroup-label '+a.className+'"><a href="#">'+j+"</a></li>",g.push(j)));l&&m.push("ui-state-disabled");n&&!c.multiple&&m.push("ui-state-active");e+='<li class="'+o+'">';e+='<label for="'+b+'" title="'+h+'" class="'+m.join(" ")+'">';e+='<input id="'+b+'" name="multiselect_'+i+'" type="'+(c.multiple?"checkbox":"radio")+'" value="'+k+'" title="'+f+'"';n&&(e+=' checked="checked"',e+=' aria-selected="true"');l&&(e+=' disabled="disabled"',e+=' aria-disabled="true"');e+=" /><span>"+f+"</span></label></li>"});h.html(e);this.labels=f.find("label");this.inputs=this.labels.children("input");this._setButtonWidth();this._setMenuWidth();this.button[0].defaultValue=this.update();a||this._trigger("refresh")},update:function(){var a=this.options,b=this.inputs,c=b.filter(":checked"),f=c.length,a=0===f?a.noneSelectedText:d.isFunction(a.selectedText)?a.selectedText.call(this,f,b.length,c.get()):/\d/.test(a.selectedList)&&0<a.selectedList&&f<=a.selectedList?c.map(function(){return d(this).next().html()}).get().join(", "):a.selectedText.replace("#",f).replace("#",b.length);this.buttonlabel.html(a);return a},_bindEvents:function(){function a(){b[b._isOpen? "close":"open"]();return!1}var b=this,c=this.button;c.find("span").bind("click.multiselect",a);c.bind({click:a,keypress:function(a){switch(a.which){case 27:case 38:case 37:b.close();break;case 39:case 40:b.open()}},mouseenter:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-hover")},mouseleave:function(){d(this).removeClass("ui-state-hover")},focus:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-focus")},blur:function(){d(this).removeClass("ui-state-focus")}});this.header.delegate("a","click.multiselect",function(a){if(d(this).hasClass("ui-multiselect-close"))b.close();else b[d(this).hasClass("ui-multiselect-all")?"checkAll":"uncheckAll"]();a.preventDefault()});this.menu.delegate("li.ui-multiselect-optgroup-label a","click.multiselect",function(a){a.preventDefault();var c=d(this),g=c.parent().nextUntil("li.ui-multiselect-optgroup-label").find("input:visible:not(:disabled)"),e=g.get(),c=c.parent().text();!1!==b._trigger("beforeoptgrouptoggle",a,{inputs:e,label:c})&&(b._toggleChecked(g.filter(":checked").length!==g.length,g),b._trigger("optgrouptoggle",a,{inputs:e,label:c,checked:e[0].checked}))}).delegate("label","mouseenter.multiselect",function(){d(this).hasClass("ui-state-disabled")||(b.labels.removeClass("ui-state-hover"),d(this).addClass("ui-state-hover").find("input").focus())}).delegate("label","keydown.multiselect",function(a){a.preventDefault();switch(a.which){case 9:case 27:b.close();break;case 38:case 40:case 37:case 39:b._traverse(a.which,this);break;case 13:d(this).find("input")[0].click()}}).delegate('input[type="checkbox"], input[type="radio"]',"click.multiselect",function(a){var c=d(this),g=this.value,e=this.checked,i=b.element.find("option");this.disabled||!1===b._trigger("click",a,{value:g,text:this.title,checked:e})?a.preventDefault():(c.focus(),c.attr("aria-selected",e),i.each(function(){this.value===g?this.selected=e:b.options.multiple||(this.selected=!1)}),b.options.multiple||(b.labels.removeClass("ui-state-active"),c.closest("label").toggleClass("ui-state-active",e),b.close()),b.element.trigger("change"),setTimeout(d.proxy(b.update,b),10))});d(document).bind("mousedown.multiselect",function(a){b._isOpen&&(!d.contains(b.menu[0],a.target)&&!d.contains(b.button[0],a.target)&&a.target!==b.button[0])&&b.close()});d(this.element[0].form).bind("reset.multiselect",function(){setTimeout(d.proxy(b.refresh,b),10)})},_setButtonWidth:function(){var a=this.element.outerWidth(),b=this.options;/\d/.test(b.minWidth)&&a<b.minWidth&&(a=b.minWidth);this.button.width(a)},_setMenuWidth:function(){var a=this.menu,b=this.button.outerWidth()-parseInt(a.css("padding-left"),10)-parseInt(a.css("padding-right"),10)-parseInt(a.css("border-right-width"),10)-parseInt(a.css("border-left-width"),10);a.width(b||this.button.outerWidth())},_traverse:function(a,b){var c=d(b),f=38===a||37===a,c=c.parent()[f?"prevAll":"nextAll"]("li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)")[f?"last":"first"]();c.length?c.find("label").trigger("mouseover"):(c=this.menu.find("ul").last(),this.menu.find("label")[f? "last":"first"]().trigger("mouseover"),c.scrollTop(f?c.height():0))},_toggleState:function(a,b){return function(){this.disabled||(this[a]=b);b?this.setAttribute("aria-selected",!0):this.removeAttribute("aria-selected")}},_toggleChecked:function(a,b){var c=b&&b.length?b:this.inputs,f=this;c.each(this._toggleState("checked",a));c.eq(0).focus();this.update();var h=c.map(function(){return this.value}).get();this.element.find("option").each(function(){!this.disabled&&-1<d.inArray(this.value,h)&&f._toggleState("selected",a).call(this)});c.length&&this.element.trigger("change")},_toggleDisabled:function(a){this.button.attr({disabled:a,"aria-disabled":a})[a?"addClass":"removeClass"]("ui-state-disabled");var b=this.menu.find("input"),b=a?b.filter(":enabled").data("ech-multiselect-disabled",!0):b.filter(function(){return!0===d.data(this,"ech-multiselect-disabled")}).removeData("ech-multiselect-disabled");b.attr({disabled:a,"arial-disabled":a}).parent()[a?"addClass":"removeClass"]("ui-state-disabled");this.element.attr({disabled:a,"aria-disabled":a})},open:function(){var a=this.button,b=this.menu,c=this.speed,f=this.options,h=[];if(!(!1===this._trigger("beforeopen")||a.hasClass("ui-state-disabled")||this._isOpen)){var g=b.find("ul").last(),e=f.show,i=a.offset();d.isArray(f.show)&&(e=f.show[0],c=f.show[1]||this.speed);e&&(h=[e,c]);g.scrollTop(0).height(f.height);d.ui.position&&!d.isEmptyObject(f.position)?(f.position.of=f.position.of||a,b.show().position(f.position).hide()):b.css({top:i.top+a.outerHeight(),left:i.left});d.fn.show.apply(b,h);this.labels.eq(0).trigger("mouseover").trigger("mouseenter").find("input").trigger("focus");a.addClass("ui-state-active");this._isOpen=!0;this._trigger("open")}},close:function(){if(!1!==this._trigger("beforeclose")){var a=this.options,b=a.hide,c=this.speed,f=[];d.isArray(a.hide)&&(b=a.hide[0],c=a.hide[1]||this.speed);b&&(f=[b,c]);d.fn.hide.apply(this.menu,f);this.button.removeClass("ui-state-active").trigger("blur").trigger("mouseleave");this._isOpen=!1;this._trigger("close")}},enable:function(){this._toggleDisabled(!1)},disable:function(){this._toggleDisabled(!0)},checkAll:function(){this._toggleChecked(!0);this._trigger("checkAll")},uncheckAll:function(){this._toggleChecked(!1);this._trigger("uncheckAll")},getChecked:function(){return this.menu.find("input").filter(":checked")},destroy:function(){d.Widget.prototype.destroy.call(this);this.button.remove();this.menu.remove();this.element.show();return this},isOpen:function(){return this._isOpen},widget:function(){return this.menu},getButton:function(){return this.button},_setOption:function(a,b){var c=this.menu;switch(a){case "header":c.find("div.ui-multiselect-header")[b?"show":"hide"]();break;case "checkAllText":c.find("a.ui-multiselect-all span").eq(-1).text(b);break;case "uncheckAllText":c.find("a.ui-multiselect-none span").eq(-1).text(b);break;case "height":c.find("ul").last().height(parseInt(b,10));break;case "minWidth":this.options[a]=parseInt(b,10);this._setButtonWidth();this._setMenuWidth();break;case "selectedText":case "selectedList":case "noneSelectedText":this.options[a]=b;this.update();break;case "classes":c.add(this.button).removeClass(this.options.classes).addClass(b);break;case "multiple":c.toggleClass("ui-multiselect-single",!b),this.options.multiple=b,this.element[0].multiple=b,this.refresh()}d.Widget.prototype._setOption.apply(this,arguments)}})})(jQuery);
diff --git a/scp/ajax.php b/scp/ajax.php
index 4ca0abf850e615d3f09479db47a904ecd511169c..dc0d35e34821a50a57ff886ff52377c806781846 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -145,7 +145,16 @@ $dispatcher = patterns('',
         url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'),
         url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'),
         url_get('^lookup', 'lookup'),
-        url_get('^search', 'search')
+        url('^search', patterns('ajax.search.php:SearchAjaxAPI',
+            url_get('^$', 'getAdvancedSearchDialog'),
+            url_post('^$', 'doSearch'),
+            url_get('^quick$', 'doQuickSearch'),
+            url_get('^/(?P<id>\d+)$', 'loadSearch'),
+            url_post('^/(?P<id>\d+)$', 'saveSearch'),
+            url_delete('^/(?P<id>\d+)$', 'deleteSearch'),
+            url_post('^/create$', 'createSearch'),
+            url_get('^/field/(?P<id>[\w_!:]+)$', 'addField')
+        ))
     )),
     url('^/collaborators/', patterns('ajax.tickets.php:TicketsAjaxAPI',
         url_get('^(?P<cid>\d+)/view$', 'viewCollaborator'),
diff --git a/scp/css/dropdown.css b/scp/css/dropdown.css
index 4fb664178aaa2ab61f663d6b9219ad00740bf64d..6105ea27fc4098cc3ba0fce3ae3467af0e8e7afb 100644
--- a/scp/css/dropdown.css
+++ b/scp/css/dropdown.css
@@ -105,6 +105,7 @@
   text-decoration: none !important;
   line-height:18px;
   margin-left:5px;
+  vertical-align: bottom;
 }
 .action-button span,
 .action-button a {
@@ -131,3 +132,21 @@
   color: #777;
   text-decoration: none;
 }
+.action-buttons {
+    display: inline-block;
+    vertical-align: middle;
+}
+.action-buttons .action-button + .action-button {
+    margin-left: 0;
+    padding-left: 0;
+    border-left: none;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+}
+.action-buttons .action-button:not(:last-of-type) {
+    margin-right: 0;
+    padding-right: 0;
+    border-right: none;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+}
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 38f3484ec7a97eeb396196469ec6ee2a7d586e09..6bbdcc8abcd61d9e67e4f5fbf7a19ec4aff505a0 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -39,6 +39,9 @@ div#header a {
 .full-width {
     width: 100%;
 }
+.headline {
+    margin-bottom: 15px;
+}
 
 .search-input {
     height: 20px;
@@ -421,7 +424,7 @@ a.Icon:hover {
     background:#fff;
 }
 
-#content a:not(.re-icon) {
+a:not(.re-icon) {
     color:#184E81;
 }
 
@@ -456,7 +459,7 @@ table.list thead th {
     color:#000;
     text-align:left;
     vertical-align:top;
-    padding: 0 4px;
+    padding: 2px 4px;
 }
 
 table.list th a {
@@ -553,7 +556,7 @@ a.print {
     color:#000;
 }
 
-#actions button, .button { padding:2px 5px 3px; margin-right:10px;  color:#777;}
+#actions button, .button { padding:2px 5px 3px; margin-right:10px;  color:#333;}
 
 .btn_sm {
     padding:2px 5px;
@@ -1484,7 +1487,6 @@ time {
     margin:0;
     padding:0 0;
     border:none;
-    overflow:hidden;
 }
 
 .dialog .custom-field .field-label {
@@ -1504,6 +1506,7 @@ time {
 .dialog fieldset input {
     border:1px solid #ccc;
     background:#fff;
+    padding: 3px;
 }
 
 .dialog fieldset span.between {
@@ -1524,18 +1527,15 @@ time {
     cursor: move;
 }
 
-#advanced-search fieldset.span6 {
-    display: inline-block;
-    width: 49%;
-    margin-bottom: 5px;
+.row {
+    display: table-row;
 }
-#advanced-search fieldset label {
-    display: block;
-}
-#advanced-search fieldset.span6 select,
-#advanced-search fieldset.span6 input {
-    max-width: 100%;
-    min-width: 75%;
+
+.row .span6 {
+    display: table-cell;
+    width: 48%;
+    padding: 5px 10px;
+    vertical-align: top;
 }
 
 #advanced-search .query input {
@@ -1570,9 +1570,14 @@ time {
     text-align:center;
 }
 
+.search-dropdown {
+    padding-left: 19px;
+}
+
 .dialog input[type="submit"],
 .dialog input[type="reset"],
-.dialog input[type="button"] {
+.dialog input[type="button"],
+.dialog button.button {
     display:inline-block;
     margin:0;
     height:24px;
@@ -2072,3 +2077,57 @@ button a:hover {
 td.indented {
     padding-left: 20px;
 }
+.secondary_lang {
+    padding:3px 0;
+    margin: 3px 0;
+    border-bottom: 1px dotted #ccc;
+}
+.saved-search {
+    padding: 5px;
+}
+
+.saved-search + .saved-search {
+    border-top: 1px dotted #ccc;
+}
+
+.accordian {
+  margin-bottom: 10px;
+}
+.accordian dt {
+    border-radius: 4px;
+    border: 1px solid #ccc;
+}
+.accordian dt.active {
+    border-bottom-left-radius: 0;
+    border-bottom-right-radius: 0;
+}
+.accordian dt, dd {
+  padding: 5px;
+}
+.accordian dt a {
+  color: black;
+  font-weight: bold;
+  display: block;
+}
+.accordian dt.active a {
+  color: #184E81;
+}
+.accordian dt:not(.active) a i {
+  display: none;
+}
+.accordian dd {
+  border-top: 0;
+  font-size: 12px;
+  margin-left: 0;
+  border: 1px solid #ccc;
+  border-top: none;
+  box-shadow: inset 0px 10px 5px -10px rgba(0,0,0,0.1);
+  background-color:rgba(42,103,172,0.1);
+}
+.accordian dt ~ dt {
+  margin-top: 5px;
+}
+.accordian dd:last-of-type {
+   position: relative;
+   top: -1px;
+}
diff --git a/scp/departments.php b/scp/departments.php
index 999380219ca49a9293089fb51a6a7272d6af93d5..db9b259bf27539cd23f97da833dad5d926646a21 100644
--- a/scp/departments.php
+++ b/scp/departments.php
@@ -33,7 +33,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Dept::create($_POST,$errors))){
+            $dept = Dept::create();
+            if(($dept->update($_POST,$errors))){
                 $msg=sprintf(__('Successfully added "%s"'),Format::htmlchars($_POST['name']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
diff --git a/scp/groups.php b/scp/groups.php
index c3f17f9c22e7f66b3e239bdf5943e623ccafb5e5..8f273e1b11db8bf69320f2f94f4d209f0f9d5af0 100644
--- a/scp/groups.php
+++ b/scp/groups.php
@@ -33,7 +33,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Group::create($_POST,$errors))){
+            $group = Group::create();
+            if(($group->update($_POST,$errors))){
                 $msg=sprintf(__('Successfully added %s'),Format::htmlchars($_POST['name']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
diff --git a/scp/js/jquery.dropdown.js b/scp/js/jquery.dropdown.js
index b885042086efee07eeb228c60abefd86ec278c0b..802a856c95e00041425a2be13178998b39de5bb7 100644
--- a/scp/js/jquery.dropdown.js
+++ b/scp/js/jquery.dropdown.js
@@ -37,7 +37,11 @@ if(jQuery) (function($) {
 		var trigger = $(this),
 			dropdown = $( $(this).attr('data-dropdown') ),
 			isOpen = trigger.hasClass('dropdown-open'),
-            rtl = $('html').hasClass('rtl');
+            rtl = $('html').hasClass('rtl'),
+            relative = trigger.offsetParent(),
+            offset = relative.offset();
+        if (relative.get(0) !== document.body)
+            offset.top -= relative.scrollTop();
 
 		event.preventDefault();
 		event.stopPropagation();
@@ -50,9 +54,9 @@ if(jQuery) (function($) {
             dropdown.removeClass('anchor-right');
 
 		dropdown.css({
-				left: dropdown.hasClass('anchor-right') ?
-				trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left,
-				top: trigger.offset().top + trigger.outerHeight()
+				left: -offset.left + (dropdown.hasClass('anchor-right') ?
+				trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left),
+				top: -offset.top + trigger.offset().top + trigger.outerHeight()
 			}).show();
 		trigger.addClass('dropdown-open');
 	}
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 74061b615bf10aca5cc6a603bd196e7a20d77bd1..46d4d318a59409082c609a26e8d8c9f3544b81b7 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -338,26 +338,12 @@ var scp_prep = function() {
         return false;
     });
 
-    /* advanced search */
-    $('.dialog#advanced-search').css({
-        top  : ($(window).height() / 6),
-        left : ($(window).width() / 2 - 300)
-    });
-
     /* loading ... */
     $("#loading").css({
         top  : ($(window).height() / 3),
         left : ($(window).width() - $("#loading").outerWidth()) / 2
     });
 
-    $('#go-advanced').click(function(e) {
-        e.preventDefault();
-        $('#result-count').html('');
-        $.toggleOverlay(true);
-        $('#advanced-search').show();
-    });
-
-
     $('#advanced-search').delegate('#statusId, #flag', 'change', function() {
         switch($(this).children('option:selected').data('state')) {
             case 'closed':
@@ -587,6 +573,11 @@ $.dialog = function (url, codes, cb, options) {
                         $('div.body', $popup).empty();
                         if(cb) cb(xhr);
                     } else {
+                        try {
+                            var json = $.parseJSON(resp);
+                            if (json.redirect) return window.location.href = json.redirect;
+                        }
+                        catch (e) { }
                         $('div.body', $popup).html(resp);
                         $popup.effect('shake');
                         $('#msg_notice, #msg_error', $popup).delay(5000).slideUp();
@@ -849,3 +840,23 @@ function __(s) {
     return $.oststrings[s];
   return s;
 }
+
+// Thanks, http://stackoverflow.com/a/487049
+function addSearchParam(key, value) {
+    key = encodeURI(key); value = encodeURI(value);
+
+    var kvp = document.location.search.substr(1).split('&');
+    var i=kvp.length; var x;
+    while (i--) {
+        x = kvp[i].split('=');
+        if (x[0]==key) {
+            x[1] = value;
+            kvp[i] = x.join('=');
+            break;
+        }
+    }
+    if(i<0) {kvp[kvp.length] = [key,value].join('=');}
+
+    //this will reload the page, it's likely better to store this until finished
+    document.location.search = kvp.join('&');
+}
diff --git a/scp/orgs.php b/scp/orgs.php
index 22cd2aeca213b7b6452b577d49390588fe87d822..bb16dcce7e2f8da7ea949d7c1a2b9f9c1fc9bf6f 100644
--- a/scp/orgs.php
+++ b/scp/orgs.php
@@ -13,6 +13,7 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 require('staff.inc.php');
+require_once INCLUDE_DIR . 'class.organization.php';
 require_once INCLUDE_DIR . 'class.note.php';
 
 $org = null;
diff --git a/scp/staff.php b/scp/staff.php
index 0ad72c2a1cc518be449f69902044dc860e712711..384af7df0771efdeafd05a8684aa03114950b899 100644
--- a/scp/staff.php
+++ b/scp/staff.php
@@ -33,7 +33,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Staff::create($_POST,$errors))){
+            $staff = Staff::create();
+            if ($staff->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully added %s'),Format::htmlchars($_POST['firstname']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
diff --git a/scp/teams.php b/scp/teams.php
index 215e9fb8788154f34ed6947a05bbf051292e690a..42fb2cbdd1bd8d38b6b215f4220db5c395558ff8 100644
--- a/scp/teams.php
+++ b/scp/teams.php
@@ -33,7 +33,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Team::create($_POST,$errors))){
+            $team = Team::create();
+            if ($team->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully added %s'),Format::htmlchars($_POST['team']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
diff --git a/scp/tickets.php b/scp/tickets.php
index 33300cb44131eaa48136d958f1160478fa20bd28..36f075bc15c125cbb4ab2332ab9a7f1e340c8e80 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -368,6 +368,10 @@ endif;
 /*... Quick stats ...*/
 $stats= $thisstaff->getTicketsStats();
 
+// Clear advanced search upon request
+if (isset($_GET['clear_filter']))
+    unset($_SESSION['advsearch']);
+
 //Navigation
 $nav->setTabActive('tickets');
 $open_name = _P('queue-name',
@@ -376,18 +380,18 @@ $open_name = _P('queue-name',
 if($cfg->showAnsweredTickets()) {
     $nav->addSubMenu(array('desc'=>$open_name.' ('.number_format($stats['open']+$stats['answered']).')',
                             'title'=>__('Open Tickets'),
-                            'href'=>'tickets.php',
+                            'href'=>'tickets.php?status=open',
                             'iconclass'=>'Ticket'),
-                        (!$_REQUEST['status'] || $_REQUEST['status']=='open'));
+                        ((!$_REQUEST['status'] && !isset($_SESSION['advsearch'])) || $_REQUEST['status']=='open'));
 } else {
 
     if ($stats) {
 
         $nav->addSubMenu(array('desc'=>$open_name.' ('.number_format($stats['open']).')',
                                'title'=>__('Open Tickets'),
-                               'href'=>'tickets.php',
+                               'href'=>'tickets.php?status=open',
                                'iconclass'=>'Ticket'),
-                            (!$_REQUEST['status'] || $_REQUEST['status']=='open'));
+                            ((!$_REQUEST['status'] && !isset($_SESSION['advsearch'])) || $_REQUEST['status']=='open'));
     }
 
     if($stats['answered']) {
@@ -434,6 +438,21 @@ if($thisstaff->showAssignedOnly() && $stats['closed']) {
                         ($_REQUEST['status']=='closed'));
 }
 
+if (isset($_SESSION['advsearch'])) {
+    // XXX: De-duplicate and simplify this code
+    $search = SavedSearch::create();
+    $form = $search->getFormFromSession('advsearch');
+    $form->loadState($_SESSION['advsearch']);
+    $tickets = TicketModel::objects();
+    $tickets = $search->mangleQuerySet($tickets, $form);
+    $count = $tickets->count();
+    $nav->addSubMenu(array('desc' => __('Search').' ('.number_format($count).')',
+                           'title'=>__('Advanced Search Query'),
+                           'href'=>'tickets.php?status=search',
+                           'iconclass'=>'Ticket'),
+                        (!$_REQUEST['status'] || $_REQUEST['status']=='search'));
+}
+
 if($thisstaff->canCreateTickets()) {
     $nav->addSubMenu(array('desc'=>__('New Ticket'),
                            'title'=> __('Open a New Ticket'),
@@ -448,7 +467,6 @@ $ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script
 $ost->addExtraHeader('<meta name="tip-namespace" content="tickets.queue" />',
     "$('#content').data('tipNamespace', 'tickets.queue');");
 
-$inc = 'tickets.inc.php';
 if($ticket) {
     $ost->setPageTitle(sprintf(__('Ticket #%s'),$ticket->getNumber()));
     $nav->setActiveSubMenu(-1);
@@ -466,9 +484,7 @@ if($ticket) {
         $inc = 'ticket-open.inc.php';
     elseif($_REQUEST['a'] == 'export') {
         $ts = strftime('%Y%m%d');
-        if (!($token=$_REQUEST['h']))
-            $errors['err'] = __('Query token required');
-        elseif (!($query=$_SESSION['search_'.$token]))
+        if (!($query=$_SESSION[':Q:tickets']))
             $errors['err'] = __('Query token not found');
         elseif (!Export::saveTickets($query, "tickets-$ts.csv", 'csv'))
             $errors['err'] = __('Internal error: Unable to dump query results');
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 58aba311cac8c0317e8a1aae9e1c3ab1a615b417..b10118f2fb5dc49d4f42b74df3e0e5b01b147bc4 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -609,10 +609,12 @@ CREATE TABLE `%TABLE_PREFIX%ticket` (
   `isoverdue` tinyint(1) unsigned NOT NULL default '0',
   `isanswered` tinyint(1) unsigned NOT NULL default '0',
   `duedate` datetime default NULL,
+  `est_duedate` datetime default NULL,
   `reopened` datetime default NULL,
   `closed` datetime default NULL,
   `lastmessage` datetime default NULL,
   `lastresponse` datetime default NULL,
+  `lastupdate` datetime default NULL,
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
   PRIMARY KEY  (`ticket_id`),
@@ -774,6 +776,20 @@ CREATE TABLE `%TABLE_PREFIX%plugin` (
   primary key (`id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%queue`;
+CREATE TABLE `%TABLE_PREFIX%queue` (
+  `id` int(11) unsigned not null auto_increment,
+  `parent_id` int(11) unsigned not null default 0,
+  `flags` int(11) unsigned not null default 0,
+  `staff_id` int(11) unsigned not null default 0,
+  `sort` int(11) unsigned not null default 0,
+  `title` varchar(60),
+  `config` text,
+  `created` datetime not null,
+  `updated` datetime not null,
+  primary key (`id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`;
 CREATE TABLE `%TABLE_PREFIX%translation` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,