diff --git a/include/ajax.search.php b/include/ajax.search.php index b0d99f18fac9f8b323908e62c26d9a9bb6774b3c..684a2e5a43debb86b0f6a50b7d6c182d1b1a7b91 100644 --- a/include/ajax.search.php +++ b/include/ajax.search.php @@ -22,17 +22,24 @@ 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(); - $form = $search->getForm(); - if (isset($_SESSION['advsearch'])) - $form->loadState($_SESSION['advsearch']); - $matches = Filter::getSupportedMatches(); + // 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'; } @@ -43,18 +50,49 @@ class SearchAjaxAPI extends AjaxController { if (!$thisstaff) Http::response(403, 'Agent login required'); + list($type, $id) = explode('!', $name, 2); + + switch (strtolower($type)) { + case ':ticket': + case ':user': + case ':organization': + // 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']); + $fields = SavedSearch::getSearchField($field->getImpl(), $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, + 'ff_uid' => FormField::$uid, + )); } function doSearch() { global $thisstaff; $search = SavedSearch::create(); - - // Add "other" fields (via $_POST['other'][]) + self::ensureConsistentFormFieldIds(); $form = $search->getForm($_POST); if (!$form->isValid()) { - $matches = Filter::getSupportedMatches(); + $matches = self::_getSupportedTicketMatches(); include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; return; } @@ -103,6 +141,29 @@ class SearchAjaxAPI extends AjaxController { ))); } + 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'); + } + } + */ + } + } + return $matches; + } + function createSearch() { global $thisstaff; @@ -124,11 +185,12 @@ 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); - $matches = Filter::getSupportedMatches(); + $matches = self::_getSupportedTicketMatches(); include STAFFINC_DIR . 'templates/advanced-search.tmpl.php'; } diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 60e01cce7e4b020937235fb37ad433b3173ec9fb..ebe6c6bb01138f642f03c3ac06e9350ec6935f5d 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -1319,9 +1319,9 @@ class SelectionField extends FormField { $name = $name ?: $this->get('name'); switch ($method) { case '!includes': - return Q::not(array("{$name}__in" => array_keys($value))); + return Q::not(array("{$name}__intersect" => array_keys($value))); case 'includes': - return new Q(array("{$name}__in" => array_keys($value))); + return new Q(array("{$name}__intersect" => array_keys($value))); default: return parent::getSearchQ($method, $value, $name); } diff --git a/include/class.forms.php b/include/class.forms.php index a590d84057ff493e94c927b8e18a0245cee44220..fbd136a9a1e780abb3c7d1487010e4c9c046755c 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -187,7 +187,7 @@ class Form { if (!$v) continue; - $info[$f->get('name')] = $f->to_database($v); + $info[$f->get('name') ?: $f->get('id')] = $f->to_database($v); } return $info; } @@ -245,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); @@ -280,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; @@ -504,7 +506,7 @@ class FormField { function getSearchMethods() { return array( 'set' => __('has a value'), - 'notset' => __('does not have a value'), + 'notset' => __('does not have a value'), 'equal' => __('is'), 'equal.not' => __('is not'), 'contains' => __('contains'), @@ -520,6 +522,8 @@ class FormField { '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')); @@ -1314,15 +1318,16 @@ class DatetimeField extends FormField { } function getSearchMethodWidgets() { - $config = $this->getConfiguration(); + $config_notime = $config = $this->getConfiguration(); + $config_notime['time'] = false; return array( 'set' => null, 'notset' => null, 'equal' => array('DatetimeField', array( - 'configuration' => $config, + 'configuration' => $config_notime, )), 'notequal' => array('DatetimeField', array( - 'configuration' => $config, + 'configuration' => $config_notime, )), 'before' => array('DatetimeField', array( 'configuration' => $config, @@ -2660,7 +2665,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.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.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.search.php b/include/class.search.php index 3a75ad09d950fcfefc36b53b49af9de5c2f989dc..b43798495d2847ae270737fe91a7bd54587c05f3 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -591,10 +591,27 @@ class SavedSearch extends VerySimpleModel { ))); } + 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 - FormField::$uid = 1000; $searchable = $this->getCurrentSearchFields($source); $fields = array( @@ -608,8 +625,9 @@ class SavedSearch extends VerySimpleModel { )), ); foreach ($searchable as $name=>$field) { - $fields = array_merge($fields, $this->getSearchField($field, $name)); + $fields = array_merge($fields, self::getSearchField($field, $name)); } + $form = new Form($fields, $source); $form->addValidator(function($form) { $selected = 0; @@ -621,7 +639,7 @@ class SavedSearch extends VerySimpleModel { $selected += 1; } if (!$selected) - $form->addError('No fields selected for searching'); + $form->addError(__('No fields selected for searching')); }); return $form; } @@ -654,12 +672,24 @@ class SavedSearch extends VerySimpleModel { )), ); - // TODO: Add "other" fields to the basic set + // 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))) { + $core[":{$info[1]}!{$info[2]}"] = $field->getImpl(); + } + } + } return $core; } - function getSearchField($field, $name) { + static function getSearchField($field, $name) { $pieces = array(); $pieces["{$name}+search"] = new BooleanField(array( 'configuration' => array('desc' => $field->get('label')) @@ -704,6 +734,35 @@ class SavedSearch extends VerySimpleModel { 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'); + foreach ($other_paths as $k => $OP) { + if (substr($name, 0, strlen($k)) == $k) { + // XXX: Last mile — find a better idea + switch (array($k, $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); diff --git a/include/class.staff.php b/include/class.staff.php index 77d5384befe7bc3f17dac41b6075f9b78766a2e7..1f7a3a3dcb711c34c045b97945cf663a78280192 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -608,7 +608,7 @@ implements AuthenticatedUser { return $users; } - function getAvailableStaffMembers() { + static function getAvailableStaffMembers() { return self::getStaffMembers(true); } diff --git a/include/class.user.php b/include/class.user.php index 13521ad42cf1ff2b95238767f833face6a1357cc..c1cae53106c8e75c49f2782f0017bf080236bc66 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -125,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; diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php index beb0c5cf2e3196dbcc79381c847df513b2d794ad..e9f275f027eb8093a9cf383db3b9b5a07aad354c 100644 --- a/include/staff/templates/advanced-search.tmpl.php +++ b/include/staff/templates/advanced-search.tmpl.php @@ -1,4 +1,5 @@ <?php + $ff_uid = FormField::$uid; ?> <div id="advanced-search"> <h3><?php echo __('Advanced Ticket Search');?></h3> @@ -21,17 +22,23 @@ foreach ($form->getFields() as $name=>$field) { ?> ?><div class="error"><?php echo $E; ?></div><?php } ?> </fieldset> -<?php } + <?php if ($name[0] == ':') { ?> + <input type="hidden" name="fields[]" value="<?php echo $name; ?>"/> + <?php } +} ?> +<div id="extra-fields"></div> <hr/> -<select name="new-field" style="max-width: 100%;"> +<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 echo $desc; ?></option> + <option value="<?php echo $id; ?>" <?php + if (isset($state[$id])) echo 'disabled="disabled"'; + ?>><?php echo $desc; ?></option> <?php } ?> </optgroup> <?php } ?> @@ -158,5 +165,23 @@ $(function() { 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/tickets.inc.php b/include/staff/tickets.inc.php index cb0c1011c644a881a8f187bfd8887b1f6b20a8b4..a1154dbcb564e7bd8161ad1a5ed3eb1fbfb21da4 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -31,8 +31,8 @@ default: if (isset($_GET['clear_filter'])) unset($_SESSION['advsearch']); if (isset($_SESSION['advsearch'])) { - $form = $search->getForm(); - $form->loadState($_SESSION['advsearch']); + $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>'; diff --git a/scp/ajax.php b/scp/ajax.php index d040343e28995e0601cd50836e7577939b428b92..dc0d35e34821a50a57ff886ff52377c806781846 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -153,7 +153,7 @@ $dispatcher = patterns('', url_post('^/(?P<id>\d+)$', 'saveSearch'), url_delete('^/(?P<id>\d+)$', 'deleteSearch'), url_post('^/create$', 'createSearch'), - url_get('^/field/(?P<id>\d+)$', 'getField') + url_get('^/field/(?P<id>[\w_!:]+)$', 'addField') )) )), url('^/collaborators/', patterns('ajax.tickets.php:TicketsAjaxAPI',