[Rt-commit] rt branch, 4.2/autocomplete-to-lib, created. rt-4.0.5-287-g52f841d

Jim Brandt jbrandt at bestpractical.com
Wed May 16 16:46:35 EDT 2012


The branch, 4.2/autocomplete-to-lib has been created
        at  52f841d6bba51dc3239c1f81289919c8991de7c3 (commit)

- Log -----------------------------------------------------------------
commit 57da934a90afda59290a08bad4e5ba8c34e34fb0
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 15 10:01:54 2012 -0400

    Move Users autocomplete logic into library code.
    
    No change in functionality. The new modules will make it cleaner
    to implement SelfService versions of the autocomplete code.

diff --git a/lib/RT/Autocomplete.pm b/lib/RT/Autocomplete.pm
new file mode 100644
index 0000000..1e2734f
--- /dev/null
+++ b/lib/RT/Autocomplete.pm
@@ -0,0 +1,264 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+=head1 NAME
+
+  RT::Autocomplete - generic baseclass for autocomplete classes
+
+=head1 SYNOPSIS
+
+    use RT::Autocomplete;
+    my $auto = RT::Autocomplete->new(\%args);
+    my $result_obj = $auto->FetchSuggestions(%args);
+
+=head1 DESCRIPTION
+
+Create the list of suggested values for an autocomplete field.
+
+=head1 METHODS
+
+=cut
+
+package RT::Autocomplete;
+
+use strict;
+use warnings;
+
+use base qw( RT::Base Class::Accessor::Fast );
+
+__PACKAGE__->mk_accessors( qw(Return Term Max Privileged Exclude Op) );
+
+=head2 new
+
+Create an RT::Autocomplete object. Most objects will be on child classes.
+
+Valid parameters:
+
+=over
+
+=item * CurrentUser
+
+CurrentUser object. Required.
+
+=item * Term
+
+Term to search with. Required.
+
+=item * Return
+
+The field to search on. Child class should set a reasonable default based
+on the data type (i.e., EmailAddress for User autocomplete).
+
+=item * Max
+
+Maximum number of values to return. Defaults to 10.
+
+=item * Privileged
+
+Limit results to privileged users (mostly for Users).
+
+=item * Exclude
+
+Values to exclude from the autocomplete results.
+
+=item * Op
+
+Operator for the search.
+
+=back
+
+=cut
+
+sub new {
+    my $proto = shift;
+    my $class = ref($proto) || $proto;
+    my $self  = {};
+    bless( $self, $class );
+    my ( $status, $msg ) = $self->_Init(@_);
+    RT::Logger->warn($msg) unless $status;
+
+    return $status ? $self : $status;
+}
+
+=head2 _Init
+
+Base class validation and object setup.
+Put any child class-specific validation in a method called
+ValidateParams in the child class.
+
+=cut
+
+sub _Init {
+    my $self = shift;
+    my %args = (
+        Return     => '',
+        Term       => undef,
+        Delim      => undef,
+        Max        => 10,
+        Privileged => undef,
+        Exclude    => '',
+        Op         => undef,
+        @_
+    );
+
+    return ( 0, 'CurrentUser required.' )
+      unless $args{CurrentUser};
+
+    $self->CurrentUser( $args{CurrentUser} );
+
+    return ( 0, "No term provided." )
+      unless defined $args{Term}
+	and length $args{Term};
+
+    # Hook for child class validation.
+    my ($return, $msg) = $self->ValidateParams(\%args);
+    return ($return, $msg) unless $return;
+
+    $self->{'Return'} = $args{Return};
+
+   # Use our delimeter if we have one
+    if ( defined $args{Delim} and length $args{Delim} ) {
+        if ( $args{Delim} eq ',' ) {
+            $args{Delim} = qr/,\s*/;
+        } else {
+            $args{Delim} = qr/\Q$args{Delim}\E/;
+        }
+
+        # If the field handles multiple values, pop the last one off
+        $args{Term} = ( split $args{Delim}, $args{Term} )[-1]
+            if $args{Term} =~ $args{Delim};
+    }
+
+    $self->{'Term'} = $args{Term};
+    $self->{'Max'} = $args{Max};
+    $self->{'Privileged'} = $args{Privileged};
+    $self->{'Exclude'} = $args{Exclude};
+    $self->{'Op'} = $args{Op};
+
+    return ( 1, 'Object created.' );
+}
+
+=head2 ValidateParams
+
+This is a stub. The parent class does general validation in
+_Init. Child classes can implement ValidateParams to add any
+child-specific param validation before the object is
+created.
+
+The parent passes to this method a hashref with all arguments from the
+autocomplete call. Child classes can modify values or set child-specific
+defaults.
+
+If validation fails, return the failure:
+
+    return (0, 'Failure message.');
+
+In this case the object is not created and the message is logged as
+a warning.
+
+=cut
+
+sub ValidateParams{
+    # This is a stub. Validation should be applied in child
+    # classes.
+    return (1, 'Params valid.');
+}
+
+=head2 FetchSuggestions
+
+Empty in the base class. See child classes for implementations.
+
+Child classes should create a record object appropriate to the field
+they seek to autocomplete. The object provides CurrentUser and a
+set of other values that may be passed through from the
+autocomplete request. See the new method for parameters.
+
+Child classes should return a record object (RT::Users, RT::Groups, etc.)
+with limits set.
+
+=cut
+
+sub FetchSuggestions {
+    my $self = shift;
+
+    # This is a stub. Implement in child classes.
+    return 1;
+}
+
+=head2 LimitForFields
+
+Apply query values for the autocomplete query.
+Expects a record object and a hashref with keys of fields and values of
+comparison operators. Operator defaults to STARTSWITH, since that is the
+common case for autocompletion.
+
+=cut
+
+sub LimitForFields {
+    my $self = shift;
+    my $records = shift;
+    my $fields_ref = shift;
+
+    while ( my ( $name, $op ) = each %{$fields_ref} ) {
+        $op = 'STARTSWITH'
+            unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+
+        $records->Limit(
+            FIELD           => $name,
+            OPERATOR        => $op,
+            VALUE           => $self->Term,
+            ENTRYAGGREGATOR => 'OR',
+            SUBCLAUSE       => 'autocomplete',
+        );
+    }
+    return;
+}
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/lib/RT/Autocomplete/Users.pm b/lib/RT/Autocomplete/Users.pm
new file mode 100644
index 0000000..88ff5cd
--- /dev/null
+++ b/lib/RT/Autocomplete/Users.pm
@@ -0,0 +1,168 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Autocomplete::Users;
+
+use strict;
+use warnings;
+use base qw( RT::Autocomplete );
+
+=head1 NAME
+
+RT::Autocomplete:Users - Autocomplete for users
+
+=head1 DESCRIPTION
+
+Perform searches on user fields like EmailAddress and Name to find users
+to suggest in user entry fields in forms.
+
+=head1 METHODS
+
+=cut
+
+=head2 ValidateParams
+
+Validation specific to Users autocomplete. Called from parent
+_Init before the object is created. Receives a hashref of arguments
+passed to new.
+
+Defaults 'return' field for user searches to EmailAddress.
+
+=cut
+
+sub ValidateParams {
+    my $self = shift;
+    my $args_ref = shift;
+
+    return ( 0, 'Permission Denied' )
+      unless $args_ref->{CurrentUser}->UserObj->Privileged
+	or RT->Config->Get('AllowUserAutocompleteForUnprivileged');
+
+    # Only allow certain return fields for User entries
+    $args_ref->{Return} = 'EmailAddress'
+      unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName)$/;
+
+    return 1;
+}
+
+=head2 FetchSuggestions
+
+Main method to search for user suggestions.
+
+Creates an RT::Users object and searches based on Term, which should
+be the first few characters a user typed.
+
+$self->Return, from the autocomplete call, determines
+which user field to search on. Also references the RT_Config
+value UserAutocompleteFields for search terms and match methods.
+
+See parent FetchSuggestions
+for additional values that can modify the search.
+
+Returns an RT::Users object which can be passed to FormatResults.
+
+=cut
+
+sub FetchSuggestions {
+    my $self = shift;
+
+    my %fields = %{ RT->Config->Get('UserAutocompleteFields')
+		      || { EmailAddress => 1, Name => 1, RealName => 'LIKE' } };
+
+    # If an operator is provided, check against only the returned field
+    # using that operator
+    %fields = ( $self->Return => $self->Op ) if $self->Op;
+
+    my $users = RT::Users->new($self->CurrentUser);
+    $users->RowsPerPage($self->Max);
+
+    $users->LimitToPrivileged() if $self->Privileged;
+
+    $self->LimitForFields($users, \%fields);
+
+    # Exclude users we don't want
+    foreach ( split /\s*,\s*/, $self->Exclude ) {
+        $users->Limit( FIELD => 'id', VALUE => $_, OPERATOR => '!=' );
+    }
+
+    return $users;
+}
+
+=head2 FormatResults
+
+Apply final formatting to the suggestions.
+
+Accepts an RT::Users object.
+
+=cut
+
+sub FormatResults {
+    my $self = shift;
+    my $users = shift;
+
+    my @suggestions;
+
+    while ( my $user = $users->Next ) {
+        next if $user->id == RT->SystemUser->id
+          or $user->id == RT->Nobody->id;
+
+        my $formatted = $HTML::Mason::Commands::m->scomp(
+			   '/Elements/ShowUser',
+                           User => $user,
+                           NoEscape => 1 );
+	$formatted =~ s/\n//g;
+        my $return = $self->Return;
+        my $suggestion = { label => $formatted, value => $user->$return };
+
+        push @suggestions, $suggestion;
+    }
+
+    return \@suggestions;
+}
+
+1;
diff --git a/share/html/Helpers/Autocomplete/Users b/share/html/Helpers/Autocomplete/Users
index dbc2d88..b51f978 100644
--- a/share/html/Helpers/Autocomplete/Users
+++ b/share/html/Helpers/Autocomplete/Users
@@ -46,85 +46,21 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 % $r->content_type('application/json');
-<% JSON( \@suggestions ) |n %>
+<% JSON( $suggestions ) |n %>
 % $m->abort;
-<%ARGS>
-$return => ''
-$term => undef
-$delim => undef
-$max => 10
-$privileged => undef
-$exclude => ''
-$op => undef
-</%ARGS>
 <%INIT>
-# Only allow certain return fields
-$return = 'EmailAddress'
-    unless $return =~ /^(?:EmailAddress|Name|RealName)$/;
+use RT::Autocomplete::Users;
 
-$m->abort unless defined $return
-             and defined $term
-             and length $term;
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
 
-# Use our delimeter if we have one
-if ( defined $delim and length $delim ) {
-    if ( $delim eq ',' ) {
-        $delim = qr/,\s*/;
-    } else {
-        $delim = qr/\Q$delim\E/;
-    }
+my $auto = RT::Autocomplete::Users->new(
+           CurrentUser => $session{'CurrentUser'}, %ARGS);
 
-    # If the field handles multiple values, pop the last one off
-    $term = (split $delim, $term)[-1] if $term =~ $delim;
-}
-
-my $CurrentUser = $session{'CurrentUser'};
-
-# Require privileged users or overriding config
-$m->abort unless $CurrentUser->Privileged
-              or RT->Config->Get('AllowUserAutocompleteForUnprivileged');
-
-my %fields = %{ RT->Config->Get('UserAutocompleteFields')
-                || { EmailAddress => 1, Name => 1, RealName => 'LIKE' } };
-
-# If an operator is provided, check against only the returned field
-# using that operator
-%fields = ( $return => $op ) if $op;
-
-my $users = RT::Users->new( $CurrentUser );
-$users->RowsPerPage( $max );
-
-$users->LimitToPrivileged() if $privileged;
-
-while (my ($name, $op) = each %fields) {
-    $op = 'STARTSWITH'
-        unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+my $suggestions = [];
 
-    $users->Limit(
-        FIELD           => $name,
-        OPERATOR        => $op,
-        VALUE           => $term,
-        ENTRYAGGREGATOR => 'OR',
-        SUBCLAUSE       => 'autocomplete',
-    );
+if ( $auto ){
+     my $users = $auto->FetchSuggestions;
+     $suggestions = $auto->FormatResults($users);
 }
-
-# Exclude users we don't want
-foreach (split /\s*,\s*/, $exclude) {
-    $users->Limit(FIELD => 'id', VALUE => $_, OPERATOR => '!=');
-}
-
-my @suggestions;
-
-while ( my $user = $users->Next ) {
-    next if $user->id == RT->SystemUser->id
-         or $user->id == RT->Nobody->id;
-
-    my $formatted = $m->scomp('/Elements/ShowUser', User => $user, NoEscape => 1);
-    $formatted =~ s/\n//g;
-    my $suggestion = { label => $formatted, value => $user->$return };
-    $m->callback( CallbackName => "ModifySuggestion", suggestion => $suggestion, user => $user );
-    push @suggestions, $suggestion;
-}
-
 </%INIT>
diff --git a/t/api/autocomplete.t b/t/api/autocomplete.t
new file mode 100644
index 0000000..74a5e28
--- /dev/null
+++ b/t/api/autocomplete.t
@@ -0,0 +1,63 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 17;
+
+use_ok('RT::Autocomplete');
+
+my $user = RT::User->new(RT->SystemUser);
+$user->Load("root");
+ok ($user->Id, "Created root user for parent");
+
+my $auto = RT::Autocomplete->new(
+				CurrentUser => RT::CurrentUser->new($user),
+				Term => 'ro',);
+
+isa_ok($auto, 'RT::Autocomplete');
+
+use_ok('RT::Autocomplete::Users');
+
+# Test with root user
+test_user_autocomplete($user, 'root', 'ro');
+
+my $user_a = RT::Test->load_or_create_user(
+     Name => 'a_user', Password => 'password', );
+
+# Test with normal user
+test_user_autocomplete($user_a, 'a_user', 'a_us');
+
+my $user_b = RT::Test->load_or_create_user(
+     Name => 'b_user', Password => 'password', );
+
+$user_b->SetPrivileged(0);
+
+# Should fail unprivileged
+my $auto_user = RT::Autocomplete::Users->new(
+		      CurrentUser => RT::CurrentUser->new($user_b),
+		      term => 'b_us',);
+
+ok( !$auto_user, 'new fails with unprivileged user');
+
+RT->Config->Set('AllowUserAutocompleteForUnprivileged', 1);
+RT::Test->started_ok;
+
+test_user_autocomplete($user_b, 'b_user', 'b_us');
+
+sub test_user_autocomplete {
+    my $user = shift;
+    my $name = shift;
+    my $term = shift;
+
+    my $auto_user = RT::Autocomplete::Users->new(
+		       CurrentUser => RT::CurrentUser->new($user),
+		       Term        => $term,);
+
+    isa_ok($auto_user, 'RT::Autocomplete::Users');
+
+    my $users_obj = $auto_user->FetchSuggestions;
+    isa_ok($users_obj, 'RT::Users');
+
+    my $u = $users_obj->Next;
+    is( $u->Name, $name, "Found $name user.");
+}
diff --git a/t/web/privileged_autocomplete.t b/t/web/privileged_autocomplete.t
new file mode 100644
index 0000000..e5837b6
--- /dev/null
+++ b/t/web/privileged_autocomplete.t
@@ -0,0 +1,78 @@
+use strict;
+use warnings;
+use RT::Test tests => 20;
+use JSON qw(from_json);
+
+my ($url, $m) = RT::Test->started_ok;
+
+my ($ticket) =
+  RT::Test->create_ticket( Queue => 'General', Subject => 'test subject' );
+
+my $user_a = RT::Test->load_or_create_user(
+    Name => 'user_a', Password => 'password',
+);
+ok( $user_a && $user_a->id, 'loaded or created user');
+
+my $user_b = RT::Test->load_or_create_user(
+    Name => 'user_b', Password => 'password',
+);
+ok( $user_b && $user_b->id, 'loaded or created user');
+
+$m->login();
+
+$m->get_ok( '/Helpers/Autocomplete/Users',
+	    'request with no params' );
+
+$m->content_is("[]\n", 'empty JSON no params');
+
+# Check uppercase param
+$m->get_ok( '/Helpers/Autocomplete/Users?Return=Name',
+	    'request with no params' );
+
+$m->content_is("[]\n", 'empty JSON just return param');
+
+# Works with lowercase too
+$m->get_ok( '/Helpers/Autocomplete/Users?return=Name',
+	    'request with no params' );
+
+$m->content_is("[]\n", 'empty JSON just return param');
+
+autocomplete_contains('us', 'user_a', $m);
+autocomplete_contains('us', 'user_b', $m);
+
+# Shouldn't get root with a term of us.
+autocomplete_lacks('us', 'root', $m);
+
+sub autocomplete {
+    my $term = shift;
+    my $agent = shift;
+    $agent->get_ok("/Helpers/Autocomplete/Users?delim=,&term=$term&return=Name",
+		   "fetched autocomplete values");
+    return from_json($agent->content);
+}
+
+sub autocomplete_contains {
+    my $term = shift;
+    my $expected = shift;
+    my $agent = shift;
+
+    my $results = autocomplete( $term, $agent );
+
+    my %seen;
+    $seen{$_->{value}}++ for @$results;
+    $expected = [$expected] unless ref $expected eq 'ARRAY';
+    is((scalar grep { not $seen{$_} } @$expected), 0, "got all expected values");
+}
+
+sub autocomplete_lacks {
+    my $term = shift;
+    my $lacks = shift;
+    my $agent = shift;
+
+    my $results = autocomplete( $term, $agent );
+
+    my %seen;
+    $seen{$_->{value}}++ for @$results;
+    $lacks = [$lacks] unless ref $lacks eq 'ARRAY';
+    is((scalar grep { $seen{$_} } @$lacks), 0, "didn't get any unexpected values");
+}

commit 5b5cad4f1f14f05d072f79f84a0a263d548a6fea
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 15 11:33:29 2012 -0400

    Convert Groups autocomplete to library.
    
    Also moved defaulting of 'Op' param to object creation since
    both Users and Groups were setting Op values in FetchSuggestions,
    which is the wrong place to set parameters.

diff --git a/lib/RT/Autocomplete.pm b/lib/RT/Autocomplete.pm
index 1e2734f..e30a5b0 100644
--- a/lib/RT/Autocomplete.pm
+++ b/lib/RT/Autocomplete.pm
@@ -142,7 +142,7 @@ sub _Init {
         Max        => 10,
         Privileged => undef,
         Exclude    => '',
-        Op         => undef,
+        Op         => '',
         @_
     );
 
@@ -159,6 +159,11 @@ sub _Init {
     my ($return, $msg) = $self->ValidateParams(\%args);
     return ($return, $msg) unless $return;
 
+    # Reset op if an invalid option is passed in or set.
+    if( $args{Op} !~ /^(?:LIKE|(?:START|END)SWITH)$/i ){
+	$args{Op} = 'STARTSWITH';
+    }
+
     $self->{'Return'} = $args{Return};
 
    # Use our delimeter if we have one
@@ -245,9 +250,6 @@ sub LimitForFields {
     my $fields_ref = shift;
 
     while ( my ( $name, $op ) = each %{$fields_ref} ) {
-        $op = 'STARTSWITH'
-            unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
-
         $records->Limit(
             FIELD           => $name,
             OPERATOR        => $op,
diff --git a/lib/RT/Autocomplete/Users.pm b/lib/RT/Autocomplete/Groups.pm
similarity index 55%
copy from lib/RT/Autocomplete/Users.pm
copy to lib/RT/Autocomplete/Groups.pm
index 88ff5cd..5619d09 100644
--- a/lib/RT/Autocomplete/Users.pm
+++ b/lib/RT/Autocomplete/Groups.pm
@@ -46,7 +46,7 @@
 #
 # END BPS TAGGED BLOCK }}}
 
-package RT::Autocomplete::Users;
+package RT::Autocomplete::Groups;
 
 use strict;
 use warnings;
@@ -54,12 +54,12 @@ use base qw( RT::Autocomplete );
 
 =head1 NAME
 
-RT::Autocomplete:Users - Autocomplete for users
+RT::Autocomplete::Groups - Autocomplete for groups
 
 =head1 DESCRIPTION
 
-Perform searches on user fields like EmailAddress and Name to find users
-to suggest in user entry fields in forms.
+Perform searches on group fields like Name to find groups
+to suggest in group entry fields in forms.
 
 =head1 METHODS
 
@@ -67,12 +67,10 @@ to suggest in user entry fields in forms.
 
 =head2 ValidateParams
 
-Validation specific to Users autocomplete. Called from parent
+Validation specific to Groups autocomplete. Called from parent
 _Init before the object is created. Receives a hashref of arguments
 passed to new.
 
-Defaults 'return' field for user searches to EmailAddress.
-
 =cut
 
 sub ValidateParams {
@@ -80,88 +78,63 @@ sub ValidateParams {
     my $args_ref = shift;
 
     return ( 0, 'Permission Denied' )
-      unless $args_ref->{CurrentUser}->UserObj->Privileged
-	or RT->Config->Get('AllowUserAutocompleteForUnprivileged');
+      unless $args_ref->{CurrentUser}->UserObj->Privileged;
 
-    # Only allow certain return fields for User entries
-    $args_ref->{Return} = 'EmailAddress'
-      unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName)$/;
+    # Set to LIKE to allow fuzzier searching for group names
+    $args_ref->{Op} = 'LIKE'
+      unless $args_ref->{Op} =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
 
     return 1;
 }
 
 =head2 FetchSuggestions
 
-Main method to search for user suggestions.
+Main method to search for group suggestions.
 
-Creates an RT::Users object and searches based on Term, which should
+Creates an RT::Groups object and searches based on Term, which should
 be the first few characters a user typed.
 
-$self->Return, from the autocomplete call, determines
-which user field to search on. Also references the RT_Config
-value UserAutocompleteFields for search terms and match methods.
-
 See parent FetchSuggestions
 for additional values that can modify the search.
 
-Returns an RT::Users object which can be passed to FormatResults.
-
 =cut
 
 sub FetchSuggestions {
     my $self = shift;
 
-    my %fields = %{ RT->Config->Get('UserAutocompleteFields')
-		      || { EmailAddress => 1, Name => 1, RealName => 'LIKE' } };
-
-    # If an operator is provided, check against only the returned field
-    # using that operator
-    %fields = ( $self->Return => $self->Op ) if $self->Op;
-
-    my $users = RT::Users->new($self->CurrentUser);
-    $users->RowsPerPage($self->Max);
-
-    $users->LimitToPrivileged() if $self->Privileged;
+    my $groups = RT::Groups->new( $self->CurrentUser );
+    $groups->RowsPerPage( $self->Max );
+    $groups->LimitToUserDefinedGroups();
 
-    $self->LimitForFields($users, \%fields);
+    $groups->Limit(
+		   FIELD           => 'Name',
+		   OPERATOR        => $self->Op,
+		   VALUE           => $self->Term,
+		  );
 
-    # Exclude users we don't want
-    foreach ( split /\s*,\s*/, $self->Exclude ) {
-        $users->Limit( FIELD => 'id', VALUE => $_, OPERATOR => '!=' );
+    # Exclude groups we don't want
+    foreach my $exclude (split /\s*,\s*/, $self->Exclude) {
+	$groups->Limit(FIELD => 'id', VALUE => $exclude, OPERATOR => '!=');
     }
 
-    return $users;
+    return $groups;
 }
 
 =head2 FormatResults
 
-Apply final formatting to the suggestions.
-
-Accepts an RT::Users object.
+Hook for applying formating to autocomplete results.
 
 =cut
 
 sub FormatResults {
     my $self = shift;
-    my $users = shift;
+    my $groups = shift;
 
     my @suggestions;
-
-    while ( my $user = $users->Next ) {
-        next if $user->id == RT->SystemUser->id
-          or $user->id == RT->Nobody->id;
-
-        my $formatted = $HTML::Mason::Commands::m->scomp(
-			   '/Elements/ShowUser',
-                           User => $user,
-                           NoEscape => 1 );
-	$formatted =~ s/\n//g;
-        my $return = $self->Return;
-        my $suggestion = { label => $formatted, value => $user->$return };
-
-        push @suggestions, $suggestion;
+    while ( my $group = $groups->Next ) {
+	# No extra formatting right now.
+        push @suggestions, $group->Name;
     }
-
     return \@suggestions;
 }
 
diff --git a/lib/RT/Autocomplete/Users.pm b/lib/RT/Autocomplete/Users.pm
index 88ff5cd..e0e0ae8 100644
--- a/lib/RT/Autocomplete/Users.pm
+++ b/lib/RT/Autocomplete/Users.pm
@@ -87,6 +87,9 @@ sub ValidateParams {
     $args_ref->{Return} = 'EmailAddress'
       unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName)$/;
 
+    $args_ref->{Op} = 'STARTSWITH'
+      unless $args_ref->{Op} =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+
     return 1;
 }
 
@@ -112,7 +115,9 @@ sub FetchSuggestions {
     my $self = shift;
 
     my %fields = %{ RT->Config->Get('UserAutocompleteFields')
-		      || { EmailAddress => 1, Name => 1, RealName => 'LIKE' } };
+		      || { EmailAddress => $self->Op,
+			   Name => $self->Op,
+			   RealName => 'LIKE' } };
 
     # If an operator is provided, check against only the returned field
     # using that operator
diff --git a/share/html/Helpers/Autocomplete/Groups b/share/html/Helpers/Autocomplete/Groups
index e8baf53..75106b4 100644
--- a/share/html/Helpers/Autocomplete/Groups
+++ b/share/html/Helpers/Autocomplete/Groups
@@ -46,43 +46,21 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 % $r->content_type('application/json');
-<% JSON( \@suggestions ) |n %>
+<% JSON( $suggestions ) |n %>
 % $m->abort;
-<%ARGS>
-$term => undef
-$max => 10
-$exclude => ''
-$op => 'LIKE'
-</%ARGS>
 <%INIT>
-$m->abort unless defined $term
-             and length $term;
+use RT::Autocomplete::Groups;
 
-my $CurrentUser = $session{'CurrentUser'};
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
 
-# Require privileged users
-$m->abort unless $CurrentUser->Privileged;
+my $auto = RT::Autocomplete::Groups->new(
+	   CurrentUser => $session{'CurrentUser'}, %ARGS);
 
-# Sanity check the operator
-$op = 'LIKE' unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+my $suggestions = [];
 
-my $groups = RT::Groups->new( $CurrentUser );
-$groups->RowsPerPage( $max );
-$groups->LimitToUserDefinedGroups();
-$groups->Limit(
-    FIELD           => 'Name',
-    OPERATOR        => $op,
-    VALUE           => $term,
-);
-
-# Exclude groups we don't want
-foreach (split /\s*,\s*/, $exclude) {
-    $groups->Limit(FIELD => 'id', VALUE => $_, OPERATOR => '!=');
-}
-
-my @suggestions;
-
-while ( my $group = $groups->Next ) {
-    push @suggestions, $group->Name;
+if( $auto ){
+    my $groups = $auto->FetchSuggestions;
+    $suggestions = $auto->FormatResults($groups);
 }
 </%INIT>
diff --git a/t/api/autocomplete.t b/t/api/autocomplete.t
index 74a5e28..7d6f3d8 100644
--- a/t/api/autocomplete.t
+++ b/t/api/autocomplete.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 use RT;
-use RT::Test tests => 17;
+use RT::Test tests => 22;
 
 use_ok('RT::Autocomplete');
 
@@ -44,6 +44,26 @@ RT::Test->started_ok;
 
 test_user_autocomplete($user_b, 'b_user', 'b_us');
 
+# Create a new group
+my $group = RT::Group->new(RT->SystemUser);
+my $group_name = 'Autocomplete' . $$;
+$group->CreateUserDefinedGroup(Name => $group_name);
+ok($group->Id, "Created a new group");
+
+use_ok('RT::Autocomplete::Groups');
+
+my $auto_group = RT::Autocomplete::Groups->new(
+		       CurrentUser => RT::CurrentUser->new($user),
+		       Term        => 'uto',);
+
+isa_ok($auto_group, 'RT::Autocomplete::Groups');
+
+my $groups = $auto_group->FetchSuggestions;
+isa_ok($groups, 'RT::Groups');
+
+my $g = $groups->Next;
+is( $g->Name, $group_name, "Found $group_name group.");
+
 sub test_user_autocomplete {
     my $user = shift;
     my $name = shift;
@@ -51,7 +71,8 @@ sub test_user_autocomplete {
 
     my $auto_user = RT::Autocomplete::Users->new(
 		       CurrentUser => RT::CurrentUser->new($user),
-		       Term        => $term,);
+		       Term        => $term,
+		       Return      => 'Name');
 
     isa_ok($auto_user, 'RT::Autocomplete::Users');
 
diff --git a/t/web/privileged_autocomplete.t b/t/web/privileged_autocomplete.t
index e5837b6..e127fc1 100644
--- a/t/web/privileged_autocomplete.t
+++ b/t/web/privileged_autocomplete.t
@@ -1,6 +1,6 @@
 use strict;
 use warnings;
-use RT::Test tests => 20;
+use RT::Test tests => 27;
 use JSON qw(from_json);
 
 my ($url, $m) = RT::Test->started_ok;
@@ -21,21 +21,21 @@ ok( $user_b && $user_b->id, 'loaded or created user');
 $m->login();
 
 $m->get_ok( '/Helpers/Autocomplete/Users',
-	    'request with no params' );
+	    'Users request with no params' );
 
 $m->content_is("[]\n", 'empty JSON no params');
 
 # Check uppercase param
 $m->get_ok( '/Helpers/Autocomplete/Users?Return=Name',
-	    'request with no params' );
+	    'Users request with just Return param' );
 
-$m->content_is("[]\n", 'empty JSON just return param');
+$m->content_is("[]\n", 'empty JSON with just Return param');
 
 # Works with lowercase too
 $m->get_ok( '/Helpers/Autocomplete/Users?return=Name',
-	    'request with no params' );
+	    'Users request with return param' );
 
-$m->content_is("[]\n", 'empty JSON just return param');
+$m->content_is("[]\n", 'empty JSON with just return param');
 
 autocomplete_contains('us', 'user_a', $m);
 autocomplete_contains('us', 'user_b', $m);
@@ -43,6 +43,28 @@ autocomplete_contains('us', 'user_b', $m);
 # Shouldn't get root with a term of us.
 autocomplete_lacks('us', 'root', $m);
 
+# Group tests
+$m->get_ok( '/Helpers/Autocomplete/Groups',
+	    'Groups request with no params' );
+
+$m->content_is("[]\n", 'empty JSON no params');
+
+$m->get_ok( '/Helpers/Autocomplete/Groups?return=Name',
+	    'Groups request with just return param' );
+
+$m->content_is("[]\n", 'empty JSON just return param');
+
+# Create a new group
+my $group = RT::Group->new(RT->SystemUser);
+my $group_name = 'Autocomplete' . $$;
+$group->CreateUserDefinedGroup(Name => $group_name);
+ok($group->Id, "Created a new group");
+
+$m->get_ok( '/Helpers/Autocomplete/Groups?return=Name&term=uto',
+	    "request for $group_name" );
+
+$m->content_contains($group_name, "Found $group_name");
+
 sub autocomplete {
     my $term = shift;
     my $agent = shift;

commit 869fbb801068a86af6fc0db84893042c010d2904
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 16 13:26:27 2012 -0400

    Added OpProvided param.

diff --git a/lib/RT/Autocomplete.pm b/lib/RT/Autocomplete.pm
index e30a5b0..38025c1 100644
--- a/lib/RT/Autocomplete.pm
+++ b/lib/RT/Autocomplete.pm
@@ -71,7 +71,7 @@ use warnings;
 
 use base qw( RT::Base Class::Accessor::Fast );
 
-__PACKAGE__->mk_accessors( qw(Return Term Max Privileged Exclude Op) );
+__PACKAGE__->mk_accessors( qw(Return Term Max Privileged Exclude Op OpProvided) );
 
 =head2 new
 
diff --git a/lib/RT/Autocomplete/Users.pm b/lib/RT/Autocomplete/Users.pm
index e0e0ae8..dce8e11 100644
--- a/lib/RT/Autocomplete/Users.pm
+++ b/lib/RT/Autocomplete/Users.pm
@@ -83,6 +83,13 @@ sub ValidateParams {
       unless $args_ref->{CurrentUser}->UserObj->Privileged
 	or RT->Config->Get('AllowUserAutocompleteForUnprivileged');
 
+    # Remember if the operator was provided to restrict the search
+    # later.
+    if( defined $args_ref->{Op}
+	and $args_ref->{Op} =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i ){
+	$self->{'OpProvided'} = $args_ref->{Op};
+    }
+
     # Only allow certain return fields for User entries
     $args_ref->{Return} = 'EmailAddress'
       unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName)$/;
@@ -121,7 +128,7 @@ sub FetchSuggestions {
 
     # If an operator is provided, check against only the returned field
     # using that operator
-    %fields = ( $self->Return => $self->Op ) if $self->Op;
+    %fields = ( $self->Return => $self->Op ) if $self->OpProvided;
 
     my $users = RT::Users->new($self->CurrentUser);
     $users->RowsPerPage($self->Max);

commit be4bd165fb155227f75460d12e1647e23171a1b8
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 16 13:30:21 2012 -0400

    Move Owners autocomplete to library.
    
    Added Limit as param to Autocomplete.pm.
    
    Touched Users and Groups because a Term value is required for them
    but Owner allows an empty Term.

diff --git a/lib/RT/Autocomplete.pm b/lib/RT/Autocomplete.pm
index 38025c1..2d6332c 100644
--- a/lib/RT/Autocomplete.pm
+++ b/lib/RT/Autocomplete.pm
@@ -71,7 +71,8 @@ use warnings;
 
 use base qw( RT::Base Class::Accessor::Fast );
 
-__PACKAGE__->mk_accessors( qw(Return Term Max Privileged Exclude Op OpProvided) );
+__PACKAGE__->mk_accessors( qw(Return Term Max Privileged Exclude Op OpProvided
+			      Limit ) );
 
 =head2 new
 
@@ -110,6 +111,10 @@ Values to exclude from the autocomplete results.
 
 Operator for the search.
 
+=item * Limit
+
+Tickets or Queues to limit the search to (mostly for Owners).
+
 =back
 
 =cut
@@ -152,8 +157,7 @@ sub _Init {
     $self->CurrentUser( $args{CurrentUser} );
 
     return ( 0, "No term provided." )
-      unless defined $args{Term}
-	and length $args{Term};
+      unless defined $args{Term};
 
     # Hook for child class validation.
     my ($return, $msg) = $self->ValidateParams(\%args);
@@ -184,6 +188,7 @@ sub _Init {
     $self->{'Privileged'} = $args{Privileged};
     $self->{'Exclude'} = $args{Exclude};
     $self->{'Op'} = $args{Op};
+    $self->{'Limit'} = $args{Limit};
 
     return ( 1, 'Object created.' );
 }
diff --git a/lib/RT/Autocomplete/Groups.pm b/lib/RT/Autocomplete/Groups.pm
index 5619d09..01b549f 100644
--- a/lib/RT/Autocomplete/Groups.pm
+++ b/lib/RT/Autocomplete/Groups.pm
@@ -80,6 +80,9 @@ sub ValidateParams {
     return ( 0, 'Permission Denied' )
       unless $args_ref->{CurrentUser}->UserObj->Privileged;
 
+    return ( 0, "Empty term provided." )
+      unless length $args_ref->{Term};
+
     # Set to LIKE to allow fuzzier searching for group names
     $args_ref->{Op} = 'LIKE'
       unless $args_ref->{Op} =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
diff --git a/lib/RT/Autocomplete/Owners.pm b/lib/RT/Autocomplete/Owners.pm
new file mode 100644
index 0000000..160ec3e
--- /dev/null
+++ b/lib/RT/Autocomplete/Owners.pm
@@ -0,0 +1,237 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Autocomplete::Owners;
+
+use strict;
+use warnings;
+use base qw( RT::Autocomplete );
+
+=head1 NAME
+
+RT::Autocomplete::Owners - Autocomplete for ticket owners
+
+=head1 DESCRIPTION
+
+Perform searches to find RT users as options for owner fields
+on tickets or for the query builder.
+
+=head1 METHODS
+
+=cut
+
+=head2 ValidateParams
+
+Validation specific to Owners autocomplete. Called from parent
+_Init before the object is created. Receives a hashref of arguments
+passed to new.
+
+=cut
+
+sub ValidateParams {
+    my $self = shift;
+    my $args_ref = shift;
+
+    return ( 0, 'No limit provided.')
+      unless defined $args_ref->{Limit};
+
+    $args_ref->{Return} = 'Name'
+      unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName|id)$/;
+
+    $args_ref->{Op} = 'STARTSWITH'
+      unless $args_ref->{Op} =~ /^(?:LIKE|(?:START|END)SWITH)$/i;
+
+    return 1;
+}
+
+=head2 FetchSuggestions
+
+Main method to search for owner suggestions.
+
+Parses Limit for tickets and queues to search on.
+Then creates RT::Users objects and searches based on Term, which should
+be the first few characters a user typed. Accepts an empty Term
+value to return all options.
+
+See parent FetchSuggestions
+for additional values that can modify the search.
+
+=cut
+
+sub FetchSuggestions {
+    my $self = shift;
+
+    my %fields = %{ RT->Config->Get('UserAutocompleteFields')
+		      || { EmailAddress => 1, Name => 1, RealName => 'LIKE' } };
+
+    my %user_uniq_hash;
+    my $isSU = $self->CurrentUser
+      ->HasRight( Right => 'SuperUser', Object => $RT::System );
+
+    # Parse the Limit param for tickets and queues
+    my $objects = $self->ParseLimit;
+
+    # Find users for each
+    foreach my $object ( @{$objects} ){
+	my $users = RT::Users->new( $self->CurrentUser );
+	$users->RowsPerPage( $self->Max );
+
+	# Limit by our autocomplete term BEFORE we limit to OwnTicket
+	# because that does a funky union hack
+	$self->LimitForFields(\%fields);
+
+	$users->WhoHaveRight(
+	   Right               => 'OwnTicket',
+           Object              => $object,
+           IncludeSystemRights => 1,
+           IncludeSuperusers   => $isSU );
+
+	while ( my $user = $users->Next() ) {
+	    next if $user_uniq_hash{ $user->Id };
+	    $user_uniq_hash{ $user->Id() } = [
+	       $user,
+               $HTML::Mason::Commands::m->scomp(
+                         '/Elements/ShowUser',
+                          User => $user,
+                          NoEscape => 1 )
+               ];
+	}
+    }
+
+    # Add Nobody if we don't already have it
+    $self->AddNobody(\%user_uniq_hash);
+
+    my @users = sort { lc $a->[1] cmp lc $b->[1] }
+                 values %user_uniq_hash;
+
+    return \@users;
+}
+
+=head2 FormatResults
+
+Hook for applying formating to autocomplete results.
+
+=cut
+
+sub FormatResults {
+    my $self = shift;
+    my $users = shift;
+    my $count = 1;
+    my @suggestions;
+
+    for my $tuple ( @{$users} ) {
+	last if $count > $self->Max;
+	my $formatted = $tuple->[1];
+	$formatted =~ s/\n//g;
+	my $return = $self->Return;
+	push @suggestions, {
+                label => $formatted,
+                value => $tuple->[0]->$return };
+	$count++;
+    }
+
+    return \@suggestions;
+}
+
+=head2 ParseLimit
+
+The Limit param contains tickets or queues on which to find possible
+Owners. This method parses that list and returns an array of
+objects for the user search.
+
+=cut
+
+sub ParseLimit {
+    my $self = shift;
+    my @objects;
+
+    # Turn RT::Ticket-1|RT::Queue-2 into ['RT::Ticket', 1], ['RT::Queue', 2]
+    foreach my $spec (map { [split /\-/, $_, 2] } split /\|/, $self->Limit) {
+	next unless $spec->[0] =~ /^RT::(Ticket|Queue)$/;
+
+	my $object = $spec->[0]->new( $self->CurrentUser );
+
+	if ( $spec->[1] ) {
+	    $object->Load( $spec->[1] );
+
+	    # Warn if we couldn't load an object
+	    unless ( $object->id ) {
+		$RT::Logger->warn("Owner autocomplete couldn't load an '$spec->[0]' with id '$spec->[1]'");
+		next;
+	    }
+	    push @objects, $object;
+	}
+    }
+    return \@objects;
+}
+
+=head2 AddNobody
+
+Add the Nobody user to the user list.
+
+=cut
+
+sub AddNobody {
+    my $self = shift;
+    my $user_uniq_hash = shift;
+
+    my $nobody = qr/^n(?:o(?:b(?:o(?:d(?:y)?)?)?)?)?$/i;
+    if ( not $user_uniq_hash->{RT->Nobody->id} and $self->Term =~ $nobody ) {
+	$user_uniq_hash->{RT->Nobody->id} = [
+              RT->Nobody,
+              $HTML::Mason::Commands::m->scomp(
+                    '/Elements/ShowUser',
+                    User => RT->Nobody,
+                    NoEscape => 1 )
+              ];
+    }
+    return;
+}
+
+
+1;
diff --git a/lib/RT/Autocomplete/Users.pm b/lib/RT/Autocomplete/Users.pm
index dce8e11..d958de3 100644
--- a/lib/RT/Autocomplete/Users.pm
+++ b/lib/RT/Autocomplete/Users.pm
@@ -83,6 +83,13 @@ sub ValidateParams {
       unless $args_ref->{CurrentUser}->UserObj->Privileged
 	or RT->Config->Get('AllowUserAutocompleteForUnprivileged');
 
+    return ( 0, "Empty term provided." )
+      unless length $args_ref->{Term};
+
+    # Only allow certain return fields for User entries
+    $args_ref->{Return} = 'EmailAddress'
+      unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName)$/;
+
     # Remember if the operator was provided to restrict the search
     # later.
     if( defined $args_ref->{Op}
@@ -90,10 +97,6 @@ sub ValidateParams {
 	$self->{'OpProvided'} = $args_ref->{Op};
     }
 
-    # Only allow certain return fields for User entries
-    $args_ref->{Return} = 'EmailAddress'
-      unless $args_ref->{Return} =~ /^(?:EmailAddress|Name|RealName)$/;
-
     $args_ref->{Op} = 'STARTSWITH'
       unless $args_ref->{Op} =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
 
diff --git a/share/html/Helpers/Autocomplete/Owners b/share/html/Helpers/Autocomplete/Owners
index 1d065f2..5620e6b 100644
--- a/share/html/Helpers/Autocomplete/Owners
+++ b/share/html/Helpers/Autocomplete/Owners
@@ -46,105 +46,21 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 % $r->content_type('application/json');
-<% JSON( \@suggestions ) |n %>
+<% JSON( $suggestions ) |n %>
 % $m->abort;
-<%ARGS>
-$return => 'Name'
-$limit => undef
-$term => undef
-$max => 10
-</%ARGS>
 <%INIT>
-# Only allow certain return fields
-$return = 'Name'
-    unless $return =~ /^(?:EmailAddress|Name|RealName|id)$/;
+use RT::Autocomplete::Owners;
 
-$m->abort unless defined $return
-             and defined $term
-             and defined $limit;
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
 
-my $CurrentUser = $session{'CurrentUser'};
+my $auto = RT::Autocomplete::Owners->new(
+           CurrentUser => $session{'CurrentUser'}, %ARGS);
 
-my %fields = %{ RT->Config->Get('UserAutocompleteFields')
-                || { EmailAddress => 1, Name => 1, RealName => 'LIKE' } };
+my $suggestions = [];
 
-my %user_uniq_hash;
-my $isSU = $session{CurrentUser}
-    ->HasRight( Right => 'SuperUser', Object => $RT::System );
-
-# Turn RT::Ticket-1|RT::Queue-2 into ['RT::Ticket', 1], ['RT::Queue', 2]
-foreach my $spec (map { [split /\-/, $_, 2] } split /\|/, $limit) {
-    next unless $spec->[0] =~ /^RT::(Ticket|Queue)$/;
-
-    my $object = $spec->[0]->new( $session{'CurrentUser'} );
-
-    if ( $spec->[1] ) {
-        $object->Load( $spec->[1] );
-
-        # Warn if we couldn't load an object
-        unless ( $object->id ) {
-            $RT::Logger->warn("Owner autocomplete couldn't load an '$spec->[0]' with id '$spec->[1]'");
-            next;
-        }
-    }
-
-    my $Users = RT::Users->new( $session{CurrentUser} );
-    $Users->RowsPerPage( $max );
-
-    # Limit by our autocomplete term BEFORE we limit to OwnTicket because that
-    # does a funky union hack
-    while (my ($name, $op) = each %fields) {
-        $op = 'STARTSWITH'
-            unless $op =~ /^(?:LIKE|(?:START|END)SWITH)$/i;
-
-        $Users->Limit(
-            FIELD           => $name,
-            OPERATOR        => $op,
-            VALUE           => $term,
-            ENTRYAGGREGATOR => 'OR',
-            SUBCLAUSE       => 'autocomplete',
-        );
-    }
-
-    $Users->WhoHaveRight(
-        Right               => 'OwnTicket',
-        Object              => $object,
-        IncludeSystemRights => 1,
-        IncludeSuperusers   => $isSU
-    );
-
-    while ( my $User = $Users->Next() ) {
-        next if $user_uniq_hash{ $User->Id };
-        $user_uniq_hash{ $User->Id() } = [
-            $User,
-            $m->scomp('/Elements/ShowUser', User => $User, NoEscape => 1)
-        ];
-    }
-}
-
-# Make sure we add Nobody if we don't already have it
-my $nobody = qr/^n(?:o(?:b(?:o(?:d(?:y)?)?)?)?)?$/i;
-if ( not $user_uniq_hash{RT->Nobody->id} and $term =~ $nobody ) {
-    $user_uniq_hash{RT->Nobody->id} = [
-        RT->Nobody,
-        $m->scomp('/Elements/ShowUser', User => RT->Nobody, NoEscape => 1)
-    ];
-}
-
-my @users = sort { lc $a->[1] cmp lc $b->[1] }
-                 values %user_uniq_hash;
-
-my @suggestions;
-my $count = 1;
-
-for my $tuple ( @users ) {
-    last if $count > $max;
-    my $formatted = $tuple->[1];
-    $formatted =~ s/\n//g;
-    push @suggestions, {
-        label => $formatted,
-        value => $tuple->[0]->$return
-    };
-    $count++;
-}
+if ( $auto ){
+     my $users = $auto->FetchSuggestions;
+     $suggestions = $auto->FormatResults($users);
+ }
 </%INIT>
diff --git a/t/api/autocomplete.t b/t/api/autocomplete.t
index 7d6f3d8..b03fafa 100644
--- a/t/api/autocomplete.t
+++ b/t/api/autocomplete.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 use RT;
-use RT::Test tests => 22;
+use RT::Test tests => 27;
 
 use_ok('RT::Autocomplete');
 
@@ -64,6 +64,21 @@ isa_ok($groups, 'RT::Groups');
 my $g = $groups->Next;
 is( $g->Name, $group_name, "Found $group_name group.");
 
+# Owner autocomplete tests
+use_ok( 'RT::Autocomplete::Owners' );
+
+my $ticket = RT::Test->create_ticket(
+    Subject => 'Test owner autocomplete',
+    Queue   => 'General',
+);
+
+my $auto_owner = RT::Autocomplete::Owners->new(
+		       CurrentUser => RT::CurrentUser->new($user),
+                       Term        => 'roo',
+                       Limit       => 'RT::Queue-' . $ticket->id,);
+
+isa_ok($auto_owner, 'RT::Autocomplete::Owners');
+
 sub test_user_autocomplete {
     my $user = shift;
     my $name = shift;

commit fc44a5bf8430149421f11ccaf25f408acba1e12c
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 16 13:43:00 2012 -0400

    Move CustomFieldValues autocomplete to library.

diff --git a/lib/RT/Autocomplete.pm b/lib/RT/Autocomplete.pm
index 2d6332c..235f75c 100644
--- a/lib/RT/Autocomplete.pm
+++ b/lib/RT/Autocomplete.pm
@@ -72,7 +72,7 @@ use warnings;
 use base qw( RT::Base Class::Accessor::Fast );
 
 __PACKAGE__->mk_accessors( qw(Return Term Max Privileged Exclude Op OpProvided
-			      Limit ) );
+			      Limit CustomField) );
 
 =head2 new
 
diff --git a/lib/RT/Autocomplete/CustomFieldValues.pm b/lib/RT/Autocomplete/CustomFieldValues.pm
new file mode 100644
index 0000000..af8dd10
--- /dev/null
+++ b/lib/RT/Autocomplete/CustomFieldValues.pm
@@ -0,0 +1,159 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Autocomplete::CustomFieldValues;
+
+use strict;
+use warnings;
+use base qw( RT::Autocomplete );
+
+=head1 NAME
+
+RT::Autocomplete::CustomFieldValues - Autocomplete for CF values
+
+=head1 DESCRIPTION
+
+Perform searches on valid values for CF autocomplete fields.
+
+=head1 METHODS
+
+=cut
+
+=head2 ValidateParams
+
+Validation specific to CFValues autocomplete. Called from parent
+_Init before the object is created. Receives a hashref of arguments
+passed to new.
+
+=cut
+
+sub ValidateParams {
+    my $self = shift;
+    my $args_ref = shift;
+
+    # Only autocomplete the last value
+    $args_ref->{Term} = (split /\n/, $args_ref->{Term})[-1];
+
+    # Find CFs in args
+    # TODO: make this a param, maybe use Limit?
+    my $CustomField;
+    for my $k ( keys %{$args_ref} ) {
+	next unless $k =~ /^Object-.*?-\d*-CustomField-(\d+)-Values?$/;
+	$CustomField = $1;
+	last;
+    }
+
+    return ( 0, 'No Custom Field Values param provided.')
+      unless defined $CustomField;
+
+    $self->{'CustomField'} = $CustomField;
+
+    return 1;
+}
+
+=head2 FetchSuggestions
+
+Main method to search for CF values.
+
+Creates an RT::CustomField object and searches the selected CF
+based on Term, which should be the first few characters a user typed.
+
+=cut
+
+sub FetchSuggestions {
+    my $self = shift;
+
+    my $CustomFieldObj = RT::CustomField->new( $self->CurrentUser );
+    $CustomFieldObj->Load( $self->CustomField );
+
+    my $values = $CustomFieldObj->Values;
+
+    $values->Limit(
+	FIELD           => 'Name',
+        OPERATOR        => 'LIKE',
+        VALUE           => $self->Term,
+        SUBCLAUSE       => 'autocomplete',
+        CASESENSITIVE   => 0,
+     );
+
+    $values->Limit(
+        ENTRYAGGREGATOR => 'OR',
+        FIELD           => 'Description',
+        OPERATOR        => 'LIKE',
+        VALUE           => $self->Term,
+        SUBCLAUSE       => 'autocomplete',
+        CASESENSITIVE   => 0,
+     );
+
+    return $values;
+}
+
+=head2 FormatResults
+
+Hook for applying formating to autocomplete results.
+
+=cut
+
+sub FormatResults {
+    my $self = shift;
+    my $values = shift;
+
+    my @suggestions;
+    while( my $value = $values->Next ) {
+	push @suggestions,
+	  {
+	   value => $value->Name,
+	   label => $value->Description
+	   ? $value->Name . ' (' . $value->Description . ')'
+	   : $value->Name,
+	  };
+    }
+    return \@suggestions;
+}
+
+1;
diff --git a/share/html/Helpers/Autocomplete/CustomFieldValues b/share/html/Helpers/Autocomplete/CustomFieldValues
index b8b21e4..bcafde0 100644
--- a/share/html/Helpers/Autocomplete/CustomFieldValues
+++ b/share/html/Helpers/Autocomplete/CustomFieldValues
@@ -46,49 +46,21 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 % $r->content_type('application/json');
-<% JSON( \@suggestions ) |n %>
+<% JSON( $suggestions ) |n %>
 % $m->abort;
 <%INIT>
-# Only autocomplete the last value
-my $term = (split /\n/, $ARGS{term} || '')[-1];
+use RT::Autocomplete::CustomFieldValues;
 
-my $CustomField;
-for my $k ( keys %ARGS ) {
-    next unless $k =~ /^Object-.*?-\d*-CustomField-(\d+)-Values?$/;
-    $CustomField = $1;
-    last;
-}
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
 
-$m->abort unless $CustomField;
-my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} );
-$CustomFieldObj->Load( $CustomField );
+my $auto = RT::Autocomplete::CustomFieldValues->new(
+           CurrentUser => $session{'CurrentUser'}, %ARGS);
 
-my $values = $CustomFieldObj->Values;
-$values->Limit(
-    FIELD           => 'Name',
-    OPERATOR        => 'LIKE',
-    VALUE           => $term,
-    SUBCLAUSE       => 'autocomplete',
-    CASESENSITIVE   => 0,
-);
-$values->Limit(
-    ENTRYAGGREGATOR => 'OR',
-    FIELD           => 'Description',
-    OPERATOR        => 'LIKE',
-    VALUE           => $term,
-    SUBCLAUSE       => 'autocomplete',
-    CASESENSITIVE   => 0,
-);
+my $suggestions = [];
 
-my @suggestions;
-
-while( my $value = $values->Next ) {
-    push @suggestions,
-      {
-        value => $value->Name,
-        label => $value->Description
-        ? $value->Name . ' (' . $value->Description . ')'
-        : $value->Name,
-      };
-}
+if ( $auto ){
+     my $values = $auto->FetchSuggestions;
+     $suggestions = $auto->FormatResults($values);
+ }
 </%INIT>
diff --git a/t/api/autocomplete.t b/t/api/autocomplete.t
index b03fafa..b007828 100644
--- a/t/api/autocomplete.t
+++ b/t/api/autocomplete.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 use RT;
-use RT::Test tests => 27;
+use RT::Test tests => 29;
 
 use_ok('RT::Autocomplete');
 
@@ -79,6 +79,16 @@ my $auto_owner = RT::Autocomplete::Owners->new(
 
 isa_ok($auto_owner, 'RT::Autocomplete::Owners');
 
+# CustomFieldValues autocomplete tests
+use_ok( 'RT::Autocomplete::CustomFieldValues' );
+
+my $auto_cfv = RT::Autocomplete::CustomFieldValues->new(
+		       CurrentUser => RT::CurrentUser->new($user),
+                       Term        => 'foo',
+		       'Object-RT::Ticket-1-CustomField-1-Value' => 1,);
+
+isa_ok($auto_cfv, 'RT::Autocomplete::CustomFieldValues');
+
 sub test_user_autocomplete {
     my $user = shift;
     my $name = shift;
diff --git a/t/web/privileged_autocomplete.t b/t/web/privileged_autocomplete.t
index e127fc1..63eecc4 100644
--- a/t/web/privileged_autocomplete.t
+++ b/t/web/privileged_autocomplete.t
@@ -1,6 +1,6 @@
 use strict;
 use warnings;
-use RT::Test tests => 27;
+use RT::Test tests => 55;
 use JSON qw(from_json);
 
 my ($url, $m) = RT::Test->started_ok;
@@ -65,6 +65,81 @@ $m->get_ok( '/Helpers/Autocomplete/Groups?return=Name&term=uto',
 
 $m->content_contains($group_name, "Found $group_name");
 
+# CF Values tests
+$m->get_ok( '/Helpers/Autocomplete/CustomFieldValues',
+	    'CFV request with no params' );
+
+$m->content_is("[]\n", 'empty JSON no params');
+
+my $cf_name = 'test enter one value with autocompletion';
+my $cfid;
+$m->get_ok(RT::Test::Web->rt_base_url);
+diag "Create a CF";
+{
+    $m->follow_link_ok( {id => 'tools-config-custom-fields-create'} );
+    $m->submit_form(
+        form_name => "ModifyCustomField",
+        fields => {
+            Name          => $cf_name,
+            TypeComposite => 'Autocomplete-1',
+            LookupType    => 'RT::Queue-RT::Ticket',
+        },
+    );
+    $m->content_contains('Object created', 'created CF sucessfully' );
+    $cfid = $m->form_name('ModifyCustomField')->value('id');
+    ok $cfid, "found id of the CF in the form, it's #$cfid";
+}
+
+diag "add 'qwe', 'ASD', '0' and 'foo bar' as values to the CF";
+{
+    foreach my $value(qw(qwe ASD 0), 'foo bar') {
+        $m->submit_form(
+            form_name => "ModifyCustomField",
+            fields => {
+                "CustomField-". $cfid ."-Value-new-Name" => $value,
+            },
+            button => 'Update',
+        );
+        $m->content_contains('Object created', 'added a value to the CF' ); # or diag $m->content;
+        my $v = $value;
+        $v =~ s/^\s+$//;
+        $v =~ s/\s+$//;
+        $m->content_contains("value=\"$v\"", 'the added value is right' );
+    }
+}
+
+diag "apply the CF to General queue";
+{
+    $m->follow_link( id => 'tools-config-queues');
+    $m->follow_link( text => 'General' );
+    $m->title_is(q/Configuration for queue General/, 'admin-queue: general');
+    $m->follow_link( id => 'page-ticket-custom-fields');
+    $m->title_is(q/Custom Fields for queue General/, 'admin-queue: general cfid');
+
+    $m->form_name('EditCustomFields');
+    $m->tick( "AddCustomField" => $cfid );
+    $m->click('UpdateCFs');
+
+    $m->content_contains('Object created', 'TCF added to the queue' );
+}
+
+my $ticket = RT::Test->create_ticket(
+    Subject => 'Test CF value autocomplete',
+    Queue   => 'General',
+);
+
+foreach my $term ( qw(qw AS 0), 'foo bar') {
+
+    my $url = 'CustomFieldValues?Object-RT::Ticket-'
+      . $ticket->id . '-CustomField-' . $cfid
+	. '-Value&term=' . $term;
+
+    $m->get_ok( "/Helpers/Autocomplete/$url",
+		"request for values on CF $cfid" );
+
+    $m->content_contains($term, "Found $term");
+}
+
 sub autocomplete {
     my $term = shift;
     my $agent = shift;

commit 52f841d6bba51dc3239c1f81289919c8991de7c3
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 16 16:36:24 2012 -0400

    Add Autocomplete functionality to SelfService
    
    See also the original bug report, at:
    http://issues.bestpractical.com/Ticket/Display.html?id=16946

diff --git a/share/html/Elements/EditCustomFieldAutocomplete b/share/html/Elements/EditCustomFieldAutocomplete
index aaf5517..a50dbcd 100644
--- a/share/html/Elements/EditCustomFieldAutocomplete
+++ b/share/html/Elements/EditCustomFieldAutocomplete
@@ -52,7 +52,7 @@
 var id = '<% $name . '-Values' %>';
 id = id.replace(/:/g,'\\:');
 jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Values' %>",
+    source: "<%RT->Config->Get('WebPath') . $AutocompleteComp %>/CustomFieldValues?<% $name . '-Values' %>",
     focus: function () {
         // prevent value inserted on focus
         return false;
@@ -76,7 +76,7 @@ jQuery('#'+id).autocomplete( {
 var id = '<% $name . '-Value' %>';
 id = id.replace(/:/g,'\\:');
 jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Value' %>"
+    source: "<%RT->Config->Get('WebPath') . $AutocompleteComp %>/CustomFieldValues?<% $name . '-Value' %>"
 }
 );
 % }
@@ -92,6 +92,16 @@ if ( $Multiple and $Values ) {
         $Default .= $value->Content ."\n";
     }
 }
+
+# Use /SelfService/Autocomplete if called from SelfService
+my $AutocompleteComp = '/Helpers/Autocomplete';
+my @callers = $m->callers;
+foreach my $caller (@callers){
+    if( $caller->path =~ /^\/SelfService/ ){
+        $AutocompleteComp = '/SelfService/Autocomplete';
+        last;
+    }
+}
 </%INIT>
 <%ARGS>
 $CustomField => undef
diff --git a/share/html/NoAuth/js/userautocomplete.js b/share/html/NoAuth/js/userautocomplete.js
index db244d1..694810c 100644
--- a/share/html/NoAuth/js/userautocomplete.js
+++ b/share/html/NoAuth/js/userautocomplete.js
@@ -69,8 +69,17 @@ jQuery(function() {
         if (!inputName || !inputName.match(applyto))
             continue;
 
+	var path_re = /SelfService/;
+	var autocomplete_path;
+	if (path_re.test(window.location.pathname)){
+	    autocomplete_path = "<% RT->Config->Get('WebPath')%>/SelfService/Autocomplete/Users";
+	}
+	else{
+	    autocomplete_path = "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Users";
+	}
+
         var options = {
-            source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Users"
+            source: autocomplete_path
         };
 
         var queryargs = [];
diff --git a/share/html/Elements/EditCustomFieldAutocomplete b/share/html/SelfService/Autocomplete/CustomFieldValues
similarity index 54%
copy from share/html/Elements/EditCustomFieldAutocomplete
copy to share/html/SelfService/Autocomplete/CustomFieldValues
index aaf5517..bcafde0 100644
--- a/share/html/Elements/EditCustomFieldAutocomplete
+++ b/share/html/SelfService/Autocomplete/CustomFieldValues
@@ -45,60 +45,22 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-% if ( $Multiple ) {
-<textarea cols="<% $Cols %>" rows="<% $Rows %>" name="<% $name %>-Values" id="<% $name %>-Values" class="CF-<%$CustomField->id%>-Edit"><% $Default || '' %></textarea>
-
-<script type="text/javascript">
-var id = '<% $name . '-Values' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Values' %>",
-    focus: function () {
-        // prevent value inserted on focus
-        return false;
-    },
-    select: function(event, ui) {
-        var terms = this.value.split(/\n/);
-        // remove the current input
-        terms.pop();
-        // add the selected item
-        terms.push( ui.item.value );
-        // add placeholder to get the comma-and-space at the end
-        terms.push("");
-        this.value = terms.join("\n");
-        return false;
-    }
-}
-);
-% } else {
-<input type="text" id="<% $name %>-Value" name="<% $name %>-Value" class="CF-<%$CustomField->id%>-Edit" value="<% $Default || '' %>"/>
-<script type="text/javascript">
-var id = '<% $name . '-Value' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Value' %>"
-}
-);
-% }
-</script>
+% $r->content_type('application/json');
+<% JSON( $suggestions ) |n %>
+% $m->abort;
 <%INIT>
-my $name = $NamePrefix . $CustomField->Id;
-if ( $Default && !$Multiple ) {
-    $Default =~ s/\s*\r*\n\s*/ /g;
-}
-if ( $Multiple and $Values ) {
-    $Default = '';
-    while (my $value = $Values->Next ) {
-        $Default .= $value->Content ."\n";
-    }
-}
+use RT::Autocomplete::CustomFieldValues;
+
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
+
+my $auto = RT::Autocomplete::CustomFieldValues->new(
+           CurrentUser => $session{'CurrentUser'}, %ARGS);
+
+my $suggestions = [];
+
+if ( $auto ){
+     my $values = $auto->FetchSuggestions;
+     $suggestions = $auto->FormatResults($values);
+ }
 </%INIT>
-<%ARGS>
-$CustomField => undef
-$NamePrefix  => undef
-$Default     => undef
-$Values      => undef
-$Multiple    => undef
-$Rows        => undef
-$Cols        => undef
-</%ARGS>
diff --git a/share/html/Elements/EditCustomFieldAutocomplete b/share/html/SelfService/Autocomplete/Groups
similarity index 54%
copy from share/html/Elements/EditCustomFieldAutocomplete
copy to share/html/SelfService/Autocomplete/Groups
index aaf5517..75106b4 100644
--- a/share/html/Elements/EditCustomFieldAutocomplete
+++ b/share/html/SelfService/Autocomplete/Groups
@@ -45,60 +45,22 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-% if ( $Multiple ) {
-<textarea cols="<% $Cols %>" rows="<% $Rows %>" name="<% $name %>-Values" id="<% $name %>-Values" class="CF-<%$CustomField->id%>-Edit"><% $Default || '' %></textarea>
-
-<script type="text/javascript">
-var id = '<% $name . '-Values' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Values' %>",
-    focus: function () {
-        // prevent value inserted on focus
-        return false;
-    },
-    select: function(event, ui) {
-        var terms = this.value.split(/\n/);
-        // remove the current input
-        terms.pop();
-        // add the selected item
-        terms.push( ui.item.value );
-        // add placeholder to get the comma-and-space at the end
-        terms.push("");
-        this.value = terms.join("\n");
-        return false;
-    }
-}
-);
-% } else {
-<input type="text" id="<% $name %>-Value" name="<% $name %>-Value" class="CF-<%$CustomField->id%>-Edit" value="<% $Default || '' %>"/>
-<script type="text/javascript">
-var id = '<% $name . '-Value' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Value' %>"
-}
-);
-% }
-</script>
+% $r->content_type('application/json');
+<% JSON( $suggestions ) |n %>
+% $m->abort;
 <%INIT>
-my $name = $NamePrefix . $CustomField->Id;
-if ( $Default && !$Multiple ) {
-    $Default =~ s/\s*\r*\n\s*/ /g;
-}
-if ( $Multiple and $Values ) {
-    $Default = '';
-    while (my $value = $Values->Next ) {
-        $Default .= $value->Content ."\n";
-    }
+use RT::Autocomplete::Groups;
+
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
+
+my $auto = RT::Autocomplete::Groups->new(
+	   CurrentUser => $session{'CurrentUser'}, %ARGS);
+
+my $suggestions = [];
+
+if( $auto ){
+    my $groups = $auto->FetchSuggestions;
+    $suggestions = $auto->FormatResults($groups);
 }
 </%INIT>
-<%ARGS>
-$CustomField => undef
-$NamePrefix  => undef
-$Default     => undef
-$Values      => undef
-$Multiple    => undef
-$Rows        => undef
-$Cols        => undef
-</%ARGS>
diff --git a/share/html/Elements/EditCustomFieldAutocomplete b/share/html/SelfService/Autocomplete/Owners
similarity index 54%
copy from share/html/Elements/EditCustomFieldAutocomplete
copy to share/html/SelfService/Autocomplete/Owners
index aaf5517..5620e6b 100644
--- a/share/html/Elements/EditCustomFieldAutocomplete
+++ b/share/html/SelfService/Autocomplete/Owners
@@ -45,60 +45,22 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-% if ( $Multiple ) {
-<textarea cols="<% $Cols %>" rows="<% $Rows %>" name="<% $name %>-Values" id="<% $name %>-Values" class="CF-<%$CustomField->id%>-Edit"><% $Default || '' %></textarea>
-
-<script type="text/javascript">
-var id = '<% $name . '-Values' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Values' %>",
-    focus: function () {
-        // prevent value inserted on focus
-        return false;
-    },
-    select: function(event, ui) {
-        var terms = this.value.split(/\n/);
-        // remove the current input
-        terms.pop();
-        // add the selected item
-        terms.push( ui.item.value );
-        // add placeholder to get the comma-and-space at the end
-        terms.push("");
-        this.value = terms.join("\n");
-        return false;
-    }
-}
-);
-% } else {
-<input type="text" id="<% $name %>-Value" name="<% $name %>-Value" class="CF-<%$CustomField->id%>-Edit" value="<% $Default || '' %>"/>
-<script type="text/javascript">
-var id = '<% $name . '-Value' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Value' %>"
-}
-);
-% }
-</script>
+% $r->content_type('application/json');
+<% JSON( $suggestions ) |n %>
+% $m->abort;
 <%INIT>
-my $name = $NamePrefix . $CustomField->Id;
-if ( $Default && !$Multiple ) {
-    $Default =~ s/\s*\r*\n\s*/ /g;
-}
-if ( $Multiple and $Values ) {
-    $Default = '';
-    while (my $value = $Values->Next ) {
-        $Default .= $value->Content ."\n";
-    }
-}
+use RT::Autocomplete::Owners;
+
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
+
+my $auto = RT::Autocomplete::Owners->new(
+           CurrentUser => $session{'CurrentUser'}, %ARGS);
+
+my $suggestions = [];
+
+if ( $auto ){
+     my $users = $auto->FetchSuggestions;
+     $suggestions = $auto->FormatResults($users);
+ }
 </%INIT>
-<%ARGS>
-$CustomField => undef
-$NamePrefix  => undef
-$Default     => undef
-$Values      => undef
-$Multiple    => undef
-$Rows        => undef
-$Cols        => undef
-</%ARGS>
diff --git a/share/html/Elements/EditCustomFieldAutocomplete b/share/html/SelfService/Autocomplete/Users
similarity index 54%
copy from share/html/Elements/EditCustomFieldAutocomplete
copy to share/html/SelfService/Autocomplete/Users
index aaf5517..b51f978 100644
--- a/share/html/Elements/EditCustomFieldAutocomplete
+++ b/share/html/SelfService/Autocomplete/Users
@@ -45,60 +45,22 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-% if ( $Multiple ) {
-<textarea cols="<% $Cols %>" rows="<% $Rows %>" name="<% $name %>-Values" id="<% $name %>-Values" class="CF-<%$CustomField->id%>-Edit"><% $Default || '' %></textarea>
-
-<script type="text/javascript">
-var id = '<% $name . '-Values' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Values' %>",
-    focus: function () {
-        // prevent value inserted on focus
-        return false;
-    },
-    select: function(event, ui) {
-        var terms = this.value.split(/\n/);
-        // remove the current input
-        terms.pop();
-        // add the selected item
-        terms.push( ui.item.value );
-        // add placeholder to get the comma-and-space at the end
-        terms.push("");
-        this.value = terms.join("\n");
-        return false;
-    }
-}
-);
-% } else {
-<input type="text" id="<% $name %>-Value" name="<% $name %>-Value" class="CF-<%$CustomField->id%>-Edit" value="<% $Default || '' %>"/>
-<script type="text/javascript">
-var id = '<% $name . '-Value' %>';
-id = id.replace(/:/g,'\\:');
-jQuery('#'+id).autocomplete( {
-    source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Value' %>"
-}
-);
-% }
-</script>
+% $r->content_type('application/json');
+<% JSON( $suggestions ) |n %>
+% $m->abort;
 <%INIT>
-my $name = $NamePrefix . $CustomField->Id;
-if ( $Default && !$Multiple ) {
-    $Default =~ s/\s*\r*\n\s*/ /g;
-}
-if ( $Multiple and $Values ) {
-    $Default = '';
-    while (my $value = $Values->Next ) {
-        $Default .= $value->Content ."\n";
-    }
+use RT::Autocomplete::Users;
+
+# Constructor wants uppercase first letter params.
+%ARGS = map { ucfirst ($_) => $ARGS{$_} } keys %ARGS;
+
+my $auto = RT::Autocomplete::Users->new(
+           CurrentUser => $session{'CurrentUser'}, %ARGS);
+
+my $suggestions = [];
+
+if ( $auto ){
+     my $users = $auto->FetchSuggestions;
+     $suggestions = $auto->FormatResults($users);
 }
 </%INIT>
-<%ARGS>
-$CustomField => undef
-$NamePrefix  => undef
-$Default     => undef
-$Values      => undef
-$Multiple    => undef
-$Rows        => undef
-$Cols        => undef
-</%ARGS>
diff --git a/t/web/self_service_autocomplete.t b/t/web/self_service_autocomplete.t
new file mode 100644
index 0000000..ada6ca0
--- /dev/null
+++ b/t/web/self_service_autocomplete.t
@@ -0,0 +1,170 @@
+use strict;
+use warnings;
+use RT::Test tests => 49;
+use JSON qw(from_json);
+
+my ($ticket) =
+  RT::Test->create_ticket( Queue => 'General', Subject => 'test subject' );
+
+my $user_a = RT::Test->load_or_create_user(
+    Name => 'user_a', Password => 'password',
+);
+ok $user_a && $user_a->id, 'loaded or created user';
+
+my $user_b = RT::Test->load_or_create_user(
+    Name => 'user_b', Password => 'password',
+);
+ok $user_b && $user_b->id, 'loaded or created user';
+
+$user_a->SetPrivileged(0);
+
+RT->Config->Set('AllowUserAutocompleteForUnprivileged', 0);
+my ($url, $m) = RT::Test->started_ok;
+$m->login('user_a');
+
+# Should be empty with AllowUserAutocompleteForUnprivileged turned off.
+$m->get_ok("/SelfService/Autocomplete/Users?delim=,&term=root&return=Name", "fetched with unpriv turned off");
+$m->content_is("[]\n", 'empty JSON no params');
+
+$m->get_ok( '/SelfService/Autocomplete/CustomFieldValues',
+            'CFV unpriv request with no params' );
+$m->content_is("[]\n", 'empty JSON no params');
+
+RT::Test->stop_server;
+
+RT->Config->Set('AllowUserAutocompleteForUnprivileged', 1);
+($url, $m) = RT::Test->started_ok;
+
+$m->login('user_a');
+
+$m->get_ok( '/SelfService/Autocomplete/Users',
+	    'request with no params' );
+
+$m->content_is("[]\n", 'empty JSON no params');
+
+$m->get_ok( '/SelfService/Autocomplete/Users?return=Name',
+	    'request with no params' );
+
+$m->content_is("[]\n", 'empty JSON no params');
+
+autocomplete_contains('us', ['user_a', 'user_b'], $m);
+
+# Shouldn't get root with a term of us.
+autocomplete_lacks('us', 'root', $m);
+
+# CF Values tests
+
+$m->get_ok( '/SelfService/Autocomplete/CustomFieldValues',
+            'CFV request with no params' );
+
+$m->content_is("[]\n", 'empty JSON no params');
+
+$m->logout;
+$m->login('root');
+my $cf_name = 'test enter one value with autocompletion';
+my $cfid;
+$m->get_ok(RT::Test::Web->rt_base_url);
+diag "Create a CF";
+{
+    $m->follow_link_ok( {id => 'tools-config-custom-fields-create'} );
+    $m->submit_form(
+        form_name => "ModifyCustomField",
+        fields => {
+            Name          => $cf_name,
+            TypeComposite => 'Autocomplete-1',
+            LookupType    => 'RT::Queue-RT::Ticket',
+        },
+    );
+    $m->content_contains('Object created', 'created CF sucessfully' );
+    $cfid = $m->form_name('ModifyCustomField')->value('id');
+    ok $cfid, "found id of the CF in the form, it's #$cfid";
+}
+
+diag "add 'qwe', 'ASD', '0' and 'foo bar' as values to the CF";
+{
+    foreach my $value(qw(qwe ASD 0), 'foo bar') {
+        $m->submit_form(
+            form_name => "ModifyCustomField",
+            fields => {
+                "CustomField-". $cfid ."-Value-new-Name" => $value,
+            },
+            button => 'Update',
+        );
+        $m->content_contains('Object created', 'added a value to the CF' );
+        my $v = $value;
+        $v =~ s/^\s+$//;
+        $v =~ s/\s+$//;
+        $m->content_contains("value=\"$v\"", 'the added value is right' );
+    }
+}
+
+diag "apply the CF to General queue";
+{
+    $m->follow_link( id => 'tools-config-queues');
+    $m->follow_link( text => 'General' );
+    $m->title_is(q/Configuration for queue General/, 'admin-queue: general');
+    $m->follow_link( id => 'page-ticket-custom-fields');
+    $m->title_is(q/Custom Fields for queue General/, 'admin-queue: general cfid');
+
+    $m->form_name('EditCustomFields');
+    $m->tick( "AddCustomField" => $cfid );
+    $m->click('UpdateCFs');
+
+    $m->content_contains('Object created', 'TCF added to the queue' );
+}
+
+#$user_a->PrincipalObj->GrantRight(Right => 'ModifyCustomField', Object => $RT::System);
+
+my $cf_ticket = RT::Test->create_ticket(
+    Subject => 'Test CF value autocomplete',
+    Queue   => 'General',
+);
+
+$m->logout;
+$m->login('user_a');
+
+foreach my $term ( qw(qw AS 0), 'foo bar') {
+
+    my $url = 'CustomFieldValues?Object-RT::Ticket-'
+      . $cf_ticket->id . '-CustomField-' . $cfid
+        . '-Value&term=' . $term;
+
+    $m->get_ok( "/SelfService/Autocomplete/$url",
+                "request for values on CF $cfid" );
+
+    $m->content_contains($term, "Found $term");
+}
+
+
+sub autocomplete {
+    my $term = shift;
+    my $agent = shift;
+    $agent->get_ok("/SelfService/Autocomplete/Users?delim=,&term=$term&return=Name", "fetched autocomplete values");
+    return from_json($agent->content);
+}
+
+sub autocomplete_contains {
+    my $term = shift;
+    my $expected = shift;
+    my $agent = shift;
+
+    my $results = autocomplete( $term, $agent );
+
+    my %seen;
+    $seen{$_->{value}}++ for @$results;
+    $expected = [$expected] unless ref $expected eq 'ARRAY';
+    is((scalar grep { not $seen{$_} } @$expected), 0, "got all expected values");
+}
+
+sub autocomplete_lacks {
+    my $term = shift;
+    my $lacks = shift;
+    my $agent = shift;
+
+    my $results = autocomplete( $term, $agent );
+
+    my %seen;
+    $seen{$_->{value}}++ for @$results;
+    $lacks = [$lacks] unless ref $lacks eq 'ARRAY';
+    is((scalar grep { $seen{$_} } @$lacks), 0, "didn't get any unexpected values");
+}

-----------------------------------------------------------------------


More information about the Rt-commit mailing list