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; ?>">— <?php echo $def_val; ?> —</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']; ?>"/> <span class="error">* <?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> <span class="error">* <?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> - <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'); ?> + <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)? ' — '.$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> <a href="#" id="go-advanced">[<?php echo __('advanced'); ?>]</a> <i class="help-tip icon-question-sign" href="#advanced"></i></td> + <td> <a href="#" onclick="javascript: + $.dialog('ajax.php/tickets/search', 201);" + >[<?php echo __('advanced'); ?>]</a> <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"> </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> ".'<i @@ -469,32 +287,32 @@ if ($results) { echo '<i class="icon-fixed-width icon-paperclip"></i> '; ?> </td> - <td nowrap> <?php echo Format::htmlchars( - Format::truncate($row['name'], 22, strpos($row['name'], '@'))); ?> </td> + <td nowrap> <?php $un = new PersonsName($T['user__name']); echo Format::htmlchars( + Format::truncate($un, 22, strpos($un, '@'))); ?> </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> <?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');?>: <a id="selectAll" href="#ckb"><?php echo __('All');?></a> <a id="selectNone" href="#ckb"><?php echo __('None');?></a> @@ -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> '.__('Page').':'.$pageNav->getPageLinks().' '; - echo '<a class="export-csv no-pjax" href="?a=export&h=' - .$hash.'&status='.$_REQUEST['status'] .'">'.__('Export').'</a> <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> <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') . ' — ' . __('Optional'); ?>"> - </fieldset> - <fieldset class="span6"> - <label for="statusId"><?php echo __('Statuses');?>:</label> - <select id="statusId" name="statusId"> - <option value="">— <?php echo __('Any Status');?> —</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="">— <?php echo __('All Departments');?> —</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="">— <?php echo __('Any Flags');?> —</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="">— <?php echo __('Anyone');?> —</option> - <option value="0">— <?php echo __('Unassigned');?> —</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 >— <?php echo __('All Help Topics');?> —</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">— <?php echo __('Anyone');?> —</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').' — '.__('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={"<":"<",">":">",'"':""","'":"'","`":"`"},c=/&(?!\w+;)|[\<\>\"\'\`]/g,a.replace(c,function(a){return b[a]||"&"})):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,