diff --git a/include/ajax.search.php b/include/ajax.search.php index d228018e911f963d9ec8b201aca8d2921e201c70..23e4a6bec9cef6ff24b535c4bcb7f15674186330 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -22,18 +22,12 @@ 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) @@ -50,7 +44,7 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Agent login required'); - list($type, $id) = explode('!', $name, 2); + @list($type, $id) = explode('!', $name, 2); switch (strtolower($type)) { case ':ticket': @@ -62,17 +56,25 @@ class SearchAjaxAPI extends AjaxController { list(,$id) = explode('!', $id, 2); if (!($field = DynamicFormField::lookup($id))) Http::response(404, 'No such field: ', print_r($id, true)); + + $impl = $field->getImpl(); + $impl->set('label', sprintf('%s / %s', + $field->form->getLocal('title'), $field->getLocal('label') + )); break; + default: + // The "extended" fields are build automatically and rooted from + // the base of the ID numbers + $extended = SavedSearch::getExtendedTicketFields(); + + if (isset($extended[$name])) { + $impl = $extended[$name]; + break; + } 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 SimpleForm($fields); // Check the box to search the field by default @@ -96,7 +98,6 @@ class SearchAjaxAPI extends AjaxController { global $thisstaff; $search = SavedSearch::create(); - self::ensureConsistentFormFieldIds(); $form = $search->getForm($_POST); if (!$form->isValid()) { @@ -132,7 +133,6 @@ class SearchAjaxAPI extends AjaxController { else $data[$name] = $info['value']; } - self::ensureConsistentFormFieldIds(); $form = $search->getForm($data); if (!$data || !$form->isValid()) { Http::response(422, 'Validation errors exist on form'); @@ -152,7 +152,9 @@ class SearchAjaxAPI extends AjaxController { function _getSupportedTicketMatches() { // User information - $matches = array(); + $matches = array( + __('Ticket Built-In') => SavedSearch::getExtendedTicketFields(), + ); foreach (array('ticket'=>'TicketForm', 'user'=>'UserForm', 'organization'=>'OrganizationForm') as $k=>$F) { $form = $F::objects()->one(); $fields = &$matches[$form->getLocal('title')]; @@ -203,7 +205,6 @@ class SearchAjaxAPI extends AjaxController { Http::response(404, 'No such saved search'); } - self::ensureConsistentFormFieldIds(); $form = $search->getForm(); if ($state = JsonDataParser::parse($search->config)) $form->loadState($state); diff --git a/include/class.forms.php b/include/class.forms.php index f4f5491226e4e63f36b338bb3b18a49c1174ff48..a0192de63c2affad1f24e7db991a0b6ab2a5c51b 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -388,6 +388,10 @@ class FormField { $this->ht[$field] = $value; } + function getId() { + return $this->ht['id']; + } + /** * getClean * @@ -422,7 +426,7 @@ class FormField { return $this->_clean; } function reset() { - $this->_clean = $this->_widget = null; + $this->value = $this->_clean = $this->_widget = null; } function getValue() { @@ -2918,7 +2922,9 @@ class CheckboxWidget extends Widget { $data = $this->field->getSource(); if (count($data)) { if (!isset($data[$this->name])) - return false; + // Indeterminite. Likely false, but consider current field + // value + return null; return @in_array($this->field->get('id'), $data[$this->name]); } return parent::getValue(); diff --git a/include/class.orm.php b/include/class.orm.php index eab05d3a074fa805a428a5c1ab93ad50efd07a19..cd8992ae881e1917e18ad89cd6014282f5bdc639 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -158,6 +158,11 @@ class ModelMeta implements ArrayAccess { $j['constraint'] = $constraint; } + function addJoin($name, array $join) { + $this->base['joins'][$name] = $join; + $this->processJoin($this->base['joins'][$name]); + } + function offsetGet($field) { if (!isset($this->base[$field])) $this->setupLazy($field); diff --git a/include/class.search.php b/include/class.search.php index bfc59f4c07101a850e91f92ba983457d4e44de88..43a9cb30c182d76b849ece53538812ea5abe44f5 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -327,6 +327,8 @@ class MysqlSearchBackend extends SearchBackend { function find($query, QuerySet $criteria) { global $thisstaff; + $criteria = clone $criteria; + $mode = ' IN BOOLEAN MODE'; #if (count(explode(' ', $query)) == 1) # $mode = ' WITH QUERY EXPANSION'; @@ -627,11 +629,11 @@ class SavedSearch extends VerySimpleModel { // 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)) { + if (!preg_match('/^:(\w+)(?:!(\d+))?\+search/', $k, $info)) { continue; } list($k,) = explode('+', $k, 2); - $source['fields'][] = ":{$info[1]}!{$info[2]}"; + $source['fields'][] = $k; } } return $this->getForm($source); @@ -644,6 +646,7 @@ class SavedSearch extends VerySimpleModel { $searchable = $this->getCurrentSearchFields($source); $fields = array( 'keywords' => new TextboxField(array( + 'id' => 3001, 'configuration' => array( 'size' => 40, 'length' => 400, @@ -675,36 +678,42 @@ class SavedSearch extends VerySimpleModel { function getCurrentSearchFields($source=false) { $core = array( - 'state' => new TicketStateChoiceField(array( - 'label' => __('State'), - )), 'status_id' => new TicketStatusChoiceField(array( + 'id' => 3101, 'label' => __('Status'), )), - 'flags' => new TicketFlagChoiceField(array( - 'label' => __('Flags'), - )), 'dept_id' => new DepartmentChoiceField(array( + 'id' => 3102, 'label' => __('Department'), )), 'assignee' => new AssigneeChoiceField(array( + 'id' => 3103, 'label' => __('Assignee'), )), 'topic_id' => new HelpTopicChoiceField(array( + 'id' => 3104, 'label' => __('Help Topic'), )), 'created' => new DateTimeField(array( + 'id' => 3105, 'label' => __('Created'), )), 'duedate' => new DateTimeField(array( + 'id' => 3106, 'label' => __('Due Date'), )), ); + $extended = self::getExtendedTicketFields(); + // Add 'other' fields added dynamically if (is_array($source) && isset($source['fields'])) { foreach ($source['fields'] as $f) { $info = array(); + if (isset($extended[$f])) { + $core[$f] = $extended[$f]; + continue; + } if (!preg_match('/^:(\w+)!(\d+)/', $f, $info)) { continue; } @@ -718,27 +727,56 @@ class SavedSearch extends VerySimpleModel { } } } - return $core; } + static function getExtendedTicketFields() { + return array( +# ':user' => new UserChoiceField(array( +# 'label' => __('Ticket Owner'), +# )), +# ':org' => new OrganizationChoiceField(array( +# 'label' => __('Organization'), +# )), + ':source' => new TicketSourceChoiceField(array( + 'id' => 3201, + 'label' => __('Source'), + )), + ':state' => new TicketStateChoiceField(array( + 'id' => 3202, + 'label' => __('State'), + )), + ':flags' => new TicketFlagChoiceField(array( + 'id' => 3203, + 'label' => __('Flags'), + )), + ); + } + static function getSearchField($field, $name) { + $baseId = $field->getId() * 20; $pieces = array(); $pieces["{$name}+search"] = new BooleanField(array( - 'configuration' => array('desc' => $field->get('label')) + 'id' => $baseId + 50000, + 'configuration' => array( + 'desc' => $field->get('label'), + ), )); $methods = $field->getSearchMethods(); $pieces["{$name}+method"] = new ChoiceField(array( + 'id' => $baseId + 50001, 'choices' => $methods, 'default' => key($methods), 'visibility' => new VisibilityConstraint(new Q(array( "{$name}+search__eq" => true, )), VisibilityConstraint::HIDDEN), )); + $offs = 0; foreach ($field->getSearchMethodWidgets() as $m=>$w) { if (!$w) continue; list($class, $args) = $w; + $args['id'] = $baseId + 50002 + $offs++; $args['required'] = true; $args['visibility'] = new VisibilityConstraint(new Q(array( "{$name}+method__eq" => $m, @@ -1009,6 +1047,29 @@ class TicketFlagChoiceField extends ChoiceField { } } +class TicketSourceChoiceField extends ChoiceField { + function getChoices() { + return array( + 'w' => __('Web'), + 'e' => __('Email'), + 'p' => __('Phone'), + 'a' => __('API'), + 'o' => __('Other'), + ); + } + + function getSearchMethods() { + return array( + 'includes' => __('is'), + '!includes' => __('is not'), + ); + } + + function getSearchQ($method, $value, $name=false) { + return parent::getSearchQ($method, $value, 'source'); + } +} + class TicketStatusChoiceField extends SelectionField { static $widget = 'ChoicesWidget'; diff --git a/include/class.ticket.php b/include/class.ticket.php index 35e5f1e794f01d83b20163f449cd083c1ff535ed..86634603200f535244e7bacfc261d4e6566f0d26 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -155,7 +155,8 @@ class DynamicForm{$form->id} extends DynamicForm { return \$instance; } } -class TicketCdataForm{$form->id} { +class TicketCdataForm{$form->id} +extends VerySimpleModel { static \$meta = array( 'view' => true, 'pk' => array('ticket_id'), @@ -171,13 +172,19 @@ class TicketCdataForm{$form->id} { } EOF; eval($cdata_class); - static::$meta['joins']['cdata+'.$form->id] = array( - 'reverse' => 'TicketCdataForm'.$form->id.'.ticket', - 'null' => true, + $join = array( + 'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'), + 'list' => 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]); + static::$meta->addJoin('cdata+'.$form->id, $join); + else { + static::$meta['joins']['cdata+'.$form->id] = array( + 'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'), + 'list' => true, + ); + } } } diff --git a/include/staff/tasks.inc.php b/include/staff/tasks.inc.php index 01c0be6986e239d6ac055d5559d598f9c200daee..ab826da6aeb550dd0fdcb42c2a4c62a0d86f410b 100644 --- a/include/staff/tasks.inc.php +++ b/include/staff/tasks.inc.php @@ -42,10 +42,10 @@ case 'search': 'cdata__title__contains' => $_REQUEST['query'], ))); break; - } elseif (isset($_SESSION['advsearch'])) { + } elseif (isset($_SESSION['advsearch:tasks'])) { // XXX: De-duplicate and simplify this code - $form = $search->getFormFromSession('advsearch'); - $form->loadState($_SESSION['advsearch']); + $form = $search->getFormFromSession('advsearch:tasks'); + $form->loadState($_SESSION['advsearch:tasks']); $tasks = $search->mangleQuerySet($tasks, $form); $results_type=__('Advanced Search') . '<a class="action-button" href="?clear_filter"><i class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>'; diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php index cfe1971b1e6d8325c24df867a5ecd5dd235bebca..9ce8ff2dca0baecbd775c9802195f85b9ce09f26 100644 --- a/include/staff/templates/advanced-search.tmpl.php +++ b/include/staff/templates/advanced-search.tmpl.php @@ -16,14 +16,16 @@ foreach ($form->errors(true) ?: array() as $message) { foreach ($form->getFields() as $name=>$field) { ?> <fieldset id="field<?php echo $field->getWidget()->id; - ?>" <?php if (!$field->isVisible()) echo 'style="display:none;"'; ?>> + ?>" <?php if (!$field->isVisible()) echo 'class="hidden"'; ?>> <?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 if ($name[0] == ':' && substr($name, -7) == '+search') { + list($N,) = explode('+', $name, 2); +?> + <input type="hidden" name="fields[]" value="<?php echo $N; ?>"/> <?php } } ?> @@ -38,7 +40,7 @@ foreach ($matches as $name => $fields) { ?> foreach ($fields as $id => $desc) { ?> <option value="<?php echo $id; ?>" <?php if (isset($state[$id])) echo 'disabled="disabled"'; - ?>><?php echo $desc; ?></option> + ?>><?php echo ($desc instanceof FormField ? $desc->getLocal('label') : $desc); ?></option> <?php } ?> </optgroup> <?php } ?> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index d3a57023703a7427d13d75df43df021622235d09..ff8b36ffdcae657b0ffc3c3e0f903d45345a9db4 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -97,6 +97,9 @@ case 'search': $tickets = $ost->searcher->find($_REQUEST['query'], $tickets); $keywords = array_pop($tickets->constraints); $basic_search->add($keywords); + // FIXME: The subquery technique below will crash with + // keyword search + $use_subquery = false; } $tickets->filter($basic_search); } diff --git a/scp/tasks.php b/scp/tasks.php index ab0996e8822a3370f33b8b8e8d64cb62ba297d9c..2f4791356b83b0bc46dcde1b96b4e36624828f8f 100644 --- a/scp/tasks.php +++ b/scp/tasks.php @@ -82,7 +82,7 @@ $stats= $thisstaff->getTasksStats(); // Clear advanced search upon request if (isset($_GET['clear_filter'])) - unset($_SESSION['advsearch']); + unset($_SESSION['advsearch:tasks']); //Navigation $nav->setTabActive('tasks'); @@ -94,7 +94,7 @@ $nav->addSubMenu(array('desc'=>$open_name.' ('.number_format($stats['open']).')' 'title'=>__('Open Tasks'), 'href'=>'tasks.php?status=open', 'iconclass'=>'Ticket'), - ((!$_REQUEST['status'] && !isset($_SESSION['advsearch'])) || $_REQUEST['status']=='open')); + ((!$_REQUEST['status'] && !isset($_SESSION['advsearch:tasks'])) || $_REQUEST['status']=='open')); if ($stats['assigned']) { @@ -124,11 +124,11 @@ if ($stats['closed']) { ($_REQUEST['status']=='closed')); } -if (isset($_SESSION['advsearch'])) { +if (isset($_SESSION['advsearch:tasks'])) { // XXX: De-duplicate and simplify this code $search = SavedSearch::create(); - $form = $search->getFormFromSession('advsearch'); - $form->loadState($_SESSION['advsearch']); + $form = $search->getFormFromSession('advsearch:tasks'); + $form->loadState($_SESSION['advsearch:tasks']); $tasks = Task::objects(); $tasks = $search->mangleQuerySet($tasks, $form); $count = $tasks->count();