[Rt-commit] rt branch, 4.2/autocomplete-links, created. rt-4.1.13-282-g6375cfb

? sunnavy sunnavy at bestpractical.com
Mon Jul 8 12:01:16 EDT 2013


The branch, 4.2/autocomplete-links has been created
        at  6375cfbc6ae423336347c484468229a64723b380 (commit)

- Log -----------------------------------------------------------------
commit d43c6cfe83ce9dfcc0df1ec099f8858471c709d9
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Jul 8 20:33:07 2013 +0800

    allow text-like searching of integer fields for tickets
    
    initial reason to do this is to autocomplete ticket links.
    note that we need to "cast" int to text for Pg

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 326ba3e..c261465 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -395,8 +395,18 @@ Meta Data:
 sub _IntLimit {
     my ( $sb, $field, $op, $value, @rest ) = @_;
 
-    die "Invalid Operator $op for $field"
-        unless $op =~ /^(=|!=|>|<|>=|<=)$/;
+    my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
+
+    # We want to support <id LIKE '1%'> for ticket autocomplete,
+    # but we need to explicitly typecast on Postgres
+    if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
+        return $sb->_SQLLimit(
+            FUNCTION => "CAST(main.$field AS TEXT)",
+            OPERATOR => $op,
+            VALUE    => $value,
+            @rest,
+        );
+    }
 
     $sb->Limit(
         FIELD    => $field,

commit 6375cfbc6ae423336347c484468229a64723b380
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Jul 8 20:37:22 2013 +0800

    autocomplete tickets support
    
    note that we use FromSQL to avoid the *not working* multiple ->Limit calls
    with "ENTRYAGGREGATOR => 'OR'"

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 41a3996..36cfa3d 100755
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1503,6 +1503,23 @@ your users.
 
 Set($AllowUserAutocompleteForUnprivileged, 0);
 
+=item C<$TicketAutocompleteFields>
+
+Specifies which fields of L<RT::Ticket> to match against and how to match each
+field when autocompleting users.  Valid match methods are LIKE, STARTSWITH,
+ENDSWITH, C<=>, and C<!=>.
+
+Not all Ticket fields are publically accessible and hence won't work for
+autocomplete unless you override their accessibility using a local overlay or a
+plugin.  Out of the box the following fields are public: id, Subject.
+
+=cut
+
+Set( $TicketAutocompleteFields, {
+    id      => 'STARTSWITH',
+    Subject => 'LIKE',
+});
+
 =item C<$DisplayTicketAfterQuickCreate>
 
 Enable this to redirect to the created ticket display page
diff --git a/share/html/Elements/AddLinks b/share/html/Elements/AddLinks
index a859e8a..b26ba47 100644
--- a/share/html/Elements/AddLinks
+++ b/share/html/Elements/AddLinks
@@ -71,32 +71,32 @@ my $id = ($Object and $Object->id)
 % if ($Merge) {
   <tr>
     <td class="label"><&|/l&>Merge into</&>:</td>
-    <td class="entry"><input name="<%$id%>-MergeInto" value="<% $ARGSRef->{"$id-MergeInto"} || '' %>" /> <i><&|/l&>(only one ticket)</&></i></td>
+    <td class="entry"><input name="<%$id%>-MergeInto" value="<% $ARGSRef->{"$id-MergeInto"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" /> <i><&|/l&>(only one ticket)</&></i></td>
   </tr>
 % }
   <tr>
     <td class="label"><& ShowRelationLabel, Object => $Object, Label => loc('Depends on').':', Relation => 'DependsOn' &></td>
-    <td class="entry"><input name="<%$id%>-DependsOn" value="<% $ARGSRef->{"$id-DependsOn"} || '' %>" /></td>
+    <td class="entry"><input name="<%$id%>-DependsOn" value="<% $ARGSRef->{"$id-DependsOn"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" data-autocomplete-multiple="1" /></td>
   </tr>
   <tr>
     <td class="label"><& ShowRelationLabel, Object => $Object, Label => loc('Depended on by').':', Relation => 'DependedOnBy' &></td>
-    <td class="entry"><input name="DependsOn-<%$id%>" value="<% $ARGSRef->{"DependsOn-$id"} || '' %>" /></td>
+    <td class="entry"><input name="DependsOn-<%$id%>" value="<% $ARGSRef->{"DependsOn-$id"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" data-autocomplete-multiple="1" /></td>
   </tr>
   <tr>
     <td class="label"><& ShowRelationLabel, Object => $Object, Label => loc('Parents').':', Relation => 'Parents' &></td>
-    <td class="entry"><input name="<%$id%>-MemberOf" value="<% $ARGSRef->{"$id-MemberOf"} || '' %>" /></td>
+    <td class="entry"><input name="<%$id%>-MemberOf" value="<% $ARGSRef->{"$id-MemberOf"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" data-autocomplete-multiple="1" /></td>
   </tr>
   <tr>
     <td class="label"><& ShowRelationLabel, Object => $Object, Label => loc('Children').':', Relation => 'Children' &></td>
-    <td class="entry"> <input name="MemberOf-<%$id%>" value="<% $ARGSRef->{"MemberOf-$id"} || '' %>" /></td>
+    <td class="entry"> <input name="MemberOf-<%$id%>" value="<% $ARGSRef->{"MemberOf-$id"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" data-autocomplete-multiple="1" /></td>
   </tr>
   <tr>
     <td class="label"><& ShowRelationLabel, Object => $Object, Label => loc('Refers to').':', Relation => 'RefersTo' &></td>
-    <td class="entry"><input name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" /></td>
+    <td class="entry"><input name="<%$id%>-RefersTo" value="<% $ARGSRef->{"$id-RefersTo"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" data-autocomplete-multiple="1" /></td>
   </tr>
   <tr>
     <td class="label"><& ShowRelationLabel, Object => $Object, Label => loc('Referred to by').':', Relation => 'ReferredToBy' &></td>
-    <td class="entry"> <input name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" /></td>
+    <td class="entry"> <input name="RefersTo-<%$id%>" value="<% $ARGSRef->{"RefersTo-$id"} || '' %>" data-autocomplete="Tickets" data-autocomplete-exclude="<% $id %>" data-autocomplete-multiple="1" /></td>
   </tr>
   <& /Elements/EditCustomFields,
         Object          => $Object,
diff --git a/share/html/Helpers/Autocomplete/Tickets b/share/html/Helpers/Autocomplete/Tickets
new file mode 100644
index 0000000..251bf64
--- /dev/null
+++ b/share/html/Helpers/Autocomplete/Tickets
@@ -0,0 +1,114 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+%#                                          <jesse 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 }}}
+% $r->content_type('application/json; charset=utf-8');
+<% JSON::to_json( \@suggestions ) |n %>
+% $m->abort;
+<%ARGS>
+$return => ''
+$term => undef
+$max => 10
+$exclude => ''
+</%ARGS>
+<%INIT>
+require JSON;
+
+# Only allow certain return fields
+$return = 'id'
+    unless $return =~ /^(?:id|Subject)$/;
+
+$m->abort unless defined $return
+             and defined $term
+             and length $term;
+
+my $CurrentUser = $session{'CurrentUser'};
+
+# Require privileged users
+$m->abort unless $CurrentUser->Privileged;
+
+$term =~ s/(['\\])/\\$1/g;
+
+my @excludes;
+
+if ( $term =~ /[^\d\s]/ ) { # to search subject, which may contain spaces
+    (my $prev, $term) = $term =~ /(.*?)\s*\b([^\d\s].*)/;
+    @excludes = split /\s+/, $prev if $prev;
+}
+else {
+    my @terms = split /\s+/, $term;
+    $term = pop @terms;
+    @excludes = @terms;
+}
+push @excludes, split /\s+/, $exclude if $exclude;
+
+my %fields = %{ RT->Config->Get('TicketAutocompleteFields')
+                || { id => 'STARTSWITH', Subject => 'LIKE' } };
+
+my $tickets = RT::Tickets->new( $CurrentUser );
+
+my @clauses;
+while (my ($name, $op) = each %fields) {
+    $op = 'STARTSWITH'
+        unless $op =~ /^(?:LIKE|(?:START|END)SWITH|=|!=)$/i;
+    push @clauses, qq{$name $op '$term'};
+}
+my $sql = join ' OR ', @clauses;
+if ( @excludes ) { # exclude ids already these
+    $sql = join ' AND ', "($sql)", map { qq{id != '$_'} } @excludes;
+}
+
+$tickets->FromSQL($sql);
+$tickets->RowsPerPage( $max );
+
+my @suggestions;
+
+while ( my $ticket = $tickets->Next ) {
+    my $formatted = loc("#[_1]: [_2]", $ticket->Id, $ticket->Subject);
+    push @suggestions, { label => $formatted, value => $ticket->$return };
+}
+
+</%INIT>
diff --git a/share/static/js/autocomplete.js b/share/static/js/autocomplete.js
index f000df0..21099c3 100644
--- a/share/static/js/autocomplete.js
+++ b/share/static/js/autocomplete.js
@@ -10,7 +10,7 @@ jQuery(function() {
         var what  = input.attr("data-autocomplete");
         var wants = input.attr("data-autocomplete-return");
 
-        if (!what || !what.match(/^(Users|Groups)$/))
+        if (!what || !what.match(/^(Users|Groups|Tickets)$/))
             return;
 
         var queryargs = [];
@@ -27,7 +27,9 @@ jQuery(function() {
         }
 
         if (input.is('[data-autocomplete-multiple]')) {
-            queryargs.push("delim=,");
+            if ( what != 'Tickets' ) {
+                queryargs.push("delim=,");
+            }
 
             options.focus = function () {
                 // prevent value inserted on focus
@@ -35,10 +37,18 @@ jQuery(function() {
             }
 
             options.select = function(event, ui) {
-                var terms = this.value.split(/,\s*/);
+                var terms = this.value.split(what == 'Tickets' ? /\s+/ : /,\s*/);
                 terms.pop();                    // remove current input
                 terms.push( ui.item.value );    // add selected item
-                this.value = terms.join(", ");
+                if ( what == 'Tickets' ) {
+                    // remove non-integers in case subject search with spaces in (like "foo bar")
+                    terms = jQuery.grep(terms, function(term) {
+                        var str = term + ''; // stringify integers to call .match
+                        return str.match(/^\d+$/);
+                    } );
+                    terms.push(''); // add trailing space so user can input another ticket id directly
+                }
+                this.value = terms.join(what == 'Tickets' ? ' ' : ", ");
                 return false;
             }
         }

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


More information about the Rt-commit mailing list