[Rt-commit] rt branch, 4.4/cf-sort-order-inputs, created. rt-4.4.4-201-gb800a92fff

? sunnavy sunnavy at bestpractical.com
Fri Jan 15 15:13:16 EST 2021


The branch, 4.4/cf-sort-order-inputs has been created
        at  b800a92fff98ce8a7f821a3ecee849d6d274d945 (commit)

- Log -----------------------------------------------------------------
commit 97a320ada7316473c7dc8b178e2fcfc66793ed99
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 26 06:45:37 2018 +0800

    Support SortOrder edit on ocf admin pages
    
    Since we don't support move up/down for global cfs on queue-specific
    pages, global cfs' SortOrder inputs are disabled there accordingly.

diff --git a/share/html/Admin/Elements/EditCustomFields b/share/html/Admin/Elements/EditCustomFields
index af4eacbcba..a362d1b49a 100644
--- a/share/html/Admin/Elements/EditCustomFields
+++ b/share/html/Admin/Elements/EditCustomFields
@@ -148,6 +148,33 @@ if ( $UpdateCFs ) {
         my ($status, $msg) = $CF->RemoveFromObject( $Object );
         push @results, $msg;
     }
+
+    my %sort = map { /CustomField-(\d+)-SortOrder/ ? ( $1 => $ARGS{$_} ) : () } keys %ARGS;
+    for my $cf_id ( sort keys %sort ) {
+        my $sort_order = $sort{$cf_id};
+        $sort_order =~ s/^\s+//;
+        $sort_order =~ s/\s+$//;
+        next unless $sort_order =~ /^-?\d+$/;
+
+        my $record = RT::ObjectCustomField->new( $session{'CurrentUser'} );
+        $record->LoadByCols( ObjectId => $id, CustomField => $cf_id );
+        unless ( $record->id ) {
+            push @results,
+              loc("Custom field #[_1] is not applied to object #[_2]", $cf_id, $id);
+            next;
+        }
+
+        if ( $record->SortOrder != $sort_order ) {
+            if ( $id && !$record->ObjectId ) {
+                # just in case, global inputs are disabled on non-global pages
+                push @results, loc("Only global pages can update SortOrder of global objects");
+                next;
+            }
+            my ( $status, $msg ) = $record->SetSortOrder( $sort_order );
+            push @results, '#' . $record->CustomField . ': ' . $msg;
+        }
+    }
+
 }
 
 $m->callback(CallbackName => 'UpdateExtraFields', Results => \@results, Object => $Object, %ARGS);
@@ -166,8 +193,8 @@ my $format = RT->Config->Get('AdminSearchResultFormat')->{'CustomFields'};
 my $rows = RT->Config->Get('AdminSearchResultRows')->{'CustomFields'} || 50;
 
 my $display_format = $id
-            ? ("'__RemoveCheckBox.{$id}__',". $format .", '__MoveCF.{$id}__'")
-            : ("'__CheckBox.{RemoveCustomField}__',". $format .", '__MoveCF.{$id}__'");
+            ? ("'__RemoveCheckBox__', ". $format .", '__SortOrder.{$id}__', '__MoveCF.{$id}__'")
+            : ("'__CheckBox.{RemoveCustomField}__', ". $format .", '__SortOrder__', '__MoveCF.{$id}__'");
 $m->callback(CallbackName => 'EditDisplayFormat', DisplayFormat => \$display_format, id => $id);
 
 </%INIT>
diff --git a/share/html/Elements/RT__CustomField/ColumnMap b/share/html/Elements/RT__CustomField/ColumnMap
index 965f99c373..8436b22232 100644
--- a/share/html/Elements/RT__CustomField/ColumnMap
+++ b/share/html/Elements/RT__CustomField/ColumnMap
@@ -166,6 +166,28 @@ my $COLUMN_MAP = {
             return @res;
         },
     },
+    SortOrder => {
+        title => 'Sort',
+        value => sub {
+            my $id = $_[0]->id;
+            my $queue_id = $_[2] || 0;
+
+            my $record = RT::ObjectCustomField->new( $session{CurrentUser} );
+            my $applied_id = $_[0]->IsGlobal ? 0 : $queue_id;
+            my $disabled = $applied_id == $queue_id ? '' : 'disabled="disabled"';
+
+            $record->LoadByCols( CustomField => $id, ObjectId => $applied_id );
+            if ( $record->id ) {
+                my $name = "CustomField-$id-SortOrder";
+                my $value = $record->SortOrder;
+                return \qq{<input name="}, $name, \qq{" size="5" value="$value" $disabled />};
+            }
+            else {
+                RT->Logger->warning("Custom field #$id is not applied to object #$applied_id");
+                return '';
+            }
+        },
+    },
 };
 
 $COLUMN_MAP->{'AppliedTo'} = $COLUMN_MAP->{'AddedTo'};

commit 5f616510d1eda7f39900e3a05400e14cb78ee5de
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Feb 7 22:42:31 2018 +0800

    Automatically resolve duplicate SortOrders for custom fields

diff --git a/lib/RT/SearchBuilder/AddAndSort.pm b/lib/RT/SearchBuilder/AddAndSort.pm
index ae1c7b7628..06b6e51330 100644
--- a/lib/RT/SearchBuilder/AddAndSort.pm
+++ b/lib/RT/SearchBuilder/AddAndSort.pm
@@ -214,6 +214,94 @@ sub JoinTargetToThis {
     return $collection->{ $key } = $alias;
 }
 
+=head2 ResolveDuplicateSortOrders( ObjectId => VALUE )
+
+Note that it could fail if it can't find an approach to resolve.
+
+Returns (1, 'Status message 1', 'Status message 2', ... ) on success and
+(0, 'Error Message') on failure.
+
+Returns 1 if there are no duplicates found.
+
+=cut
+
+sub ResolveDuplicateSortOrders {
+    my $self = shift;
+    my %args = (
+        ObjectId => 0,
+        @_,
+    );
+
+    my ( %record, %order, %dup, %changes );
+
+    while ( my $record = $self->Next ) {
+        $record{ $record->id } = $record;
+        $order{ $record->id }  = $record->SortOrder;
+        $dup{ $record->SortOrder }++;
+    }
+
+    if ( grep { $_ > 1 } values %dup ) {
+
+        # for records having the same sort order, later updated ones win
+        my @ids =
+          sort {
+            ( $order{$a} <=> $order{$b} )
+              || ( $record{$b}->LastUpdated cmp $record{$a}->LastUpdated )
+              || ( $a <=> $b )
+          }
+          keys %record;
+        my @orders = sort { $a <=> $b } values %order;
+
+        my @new_orders;
+        my %exist;
+        for my $order ( @orders ) {
+            my $new_order = $order;
+            while ( $exist{$new_order} ) {
+                $new_order += 1;
+            }
+            push @new_orders, $new_order;
+            $exist{$new_order} = 1;
+        }
+
+        for my $id ( @ids ) {
+            my $new_order = shift @new_orders;
+            if ( $order{$id} != $new_order ) {
+                if ( !$args{ObjectId} || $record{$id}->ObjectId ) {
+                    $changes{$id} = $new_order;
+                }
+                else {
+                    return ( 0,
+                        $self->loc(
+"Failed to resolve duplicated SortOrder automatically, please resolve manually or adjust global ones first on global page"
+                          )
+                    );
+                }
+            }
+        }
+    }
+
+    if ( %changes ) {
+        my @msgs;
+        $RT::Handle->BeginTransaction;
+        for my $id ( sort { $a <=> $b } keys %changes ) {
+            my ( $ret, $msg ) =
+              $record{$id}->SetSortOrder( $changes{$id} );
+            $msg = "#" . $record{$id}->CustomField . ': ' . $msg;
+            if ( $ret ) {
+                push @msgs, $msg;
+            }
+            else {
+                $RT::Handle->Rollback;
+                return ( $ret, $msg );
+            }
+        }
+        $RT::Handle->Commit;
+        return ( 1, @msgs );
+    }
+
+    return 1;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/share/html/Admin/Elements/EditCustomFields b/share/html/Admin/Elements/EditCustomFields
index a362d1b49a..2a8bbce2fb 100644
--- a/share/html/Admin/Elements/EditCustomFields
+++ b/share/html/Admin/Elements/EditCustomFields
@@ -175,6 +175,23 @@ if ( $UpdateCFs ) {
         }
     }
 
+    {
+        my $cfs = RT::CustomFields->new( $session{'CurrentUser'} );
+        $cfs->LimitToLookupType($lookup);
+        $cfs->LimitToGlobalOrObjectId($id);
+        $cfs->SetContextObject( $Object );
+
+        my $ocfs = RT::ObjectCustomFields->new( $session{CurrentUser} );
+        $ocfs->Limit(
+            FIELD    => 'CustomField',
+            VALUE    => [ map { $_->id } @{$cfs->ItemsArrayRef } ],
+            OPERATOR => 'IN',
+        );
+        $ocfs->Limit( FIELD => 'ObjectId', VALUE => [ 0, $id ], OPERATOR => 'IN' );
+
+        my ( $status, @msgs ) = $ocfs->ResolveDuplicateSortOrders( ObjectId => $id );
+        push @results, @msgs;
+    }
 }
 
 $m->callback(CallbackName => 'UpdateExtraFields', Results => \@results, Object => $Object, %ARGS);

commit 21d82647d9ab7b87692dcccc94ae93451a9e8245
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Feb 7 22:43:30 2018 +0800

    Add config option to enable/disable SortOrder's auto-resolve approach

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 78d9ac532a..b797e9a5b6 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -4071,6 +4071,23 @@ Set(%AdminSearchResultRows,
     Assets       => 50,
 );
 
+=item C<%AdminAutoResolveDuplicateSortOrders>
+
+Use C<%AdminAutoResolveDuplicateSortOrders> to automatically resolve duplicate
+SortOrders on admin pages.
+
+Note that for objects like custom fields that support multiple lever apply
+approaches(globally or at queue level), updating SortOrder of global custom
+fields might cause new duplications at queue level, this config doesn't check
+or resolve those new duplicates.
+
+=cut
+
+
+Set(%AdminAutoResolveDuplicateSortOrders,
+    CustomFields => 0,
+);
+
 =back
 
 
diff --git a/share/html/Admin/Elements/EditCustomFields b/share/html/Admin/Elements/EditCustomFields
index 2a8bbce2fb..73da975d90 100644
--- a/share/html/Admin/Elements/EditCustomFields
+++ b/share/html/Admin/Elements/EditCustomFields
@@ -175,7 +175,8 @@ if ( $UpdateCFs ) {
         }
     }
 
-    {
+    my $config = RT->Config->Get('AdminAutoResolveDuplicateSortOrders');
+    if ( $config && $config->{CustomFields} ) {
         my $cfs = RT::CustomFields->new( $session{'CurrentUser'} );
         $cfs->LimitToLookupType($lookup);
         $cfs->LimitToGlobalOrObjectId($id);

commit b800a92fff98ce8a7f821a3ecee849d6d274d945
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Feb 8 21:46:29 2018 +0800

    Add rt-rebuild-sort-order command to rebuild SortOrder

diff --git a/.gitignore b/.gitignore
index 4b29d7bf6a..ae79ad1038 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,7 @@
 /sbin/rt-passwd
 /sbin/standalone_httpd
 /sbin/rt-munge-attachments
+/sbin/rt-rebuild-sort-order
 /var/mason_data/
 /autom4te.cache/
 /configure
diff --git a/Makefile.in b/Makefile.in
index a2d647066f..1f181ccb68 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -152,6 +152,7 @@ SYSTEM_BINARIES		=	rt-attributes-viewer \
 				rt-passwd \
 				rt-preferences-viewer \
 				rt-search-attributes \
+				rt-rebuild-sort-order \
 				rt-serializer \
 				rt-server \
 				rt-server.fcgi \
diff --git a/configure.ac b/configure.ac
index e10ccf199a..4b95d3b670 100755
--- a/configure.ac
+++ b/configure.ac
@@ -489,6 +489,7 @@ AC_CONFIG_FILES([
                  sbin/rt-importer
                  sbin/rt-passwd
                  sbin/rt-munge-attachments
+                 sbin/rt-rebuild-sort-order
                  bin/rt-crontool
                  bin/rt-mailgate
                  bin/rt],
diff --git a/sbin/rt-rebuild-sort-order.in b/sbin/rt-rebuild-sort-order.in
new file mode 100644
index 0000000000..b2b56d018a
--- /dev/null
+++ b/sbin/rt-rebuild-sort-order.in
@@ -0,0 +1,305 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2020 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 }}}
+use strict;
+use warnings;
+
+# fix lib paths, some may be relative
+BEGIN {    # BEGIN RT CMD BOILERPLATE
+    require File::Spec;
+    require Cwd;
+    my @libs = ( "@RT_LIB_PATH@", "@LOCAL_LIB_PATH@" );
+    my $bin_path;
+
+    for my $lib ( @libs ) {
+        unless ( File::Spec->file_name_is_absolute( $lib ) ) {
+            $bin_path ||=
+              ( File::Spec->splitpath( Cwd::abs_path( __FILE__ ) ) )[1];
+            $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
+        }
+        unshift @INC, $lib;
+    }
+
+}
+
+my %OPT;
+
+use RT::Interface::CLI qw(Init);
+Init( \%OPT, 'type=s', 'lookup-type=s', 'ratio=i', 'dryrun' );
+
+my %type = map { lc $_ => 1 } qw/CustomField/;
+
+my %lookup_type = map { $_->CustomFieldLookupType => 1 } qw/RT::Ticket RT::Transaction RT::Article RT::Asset/;
+
+# to support --lookup-type Ticket
+my %variant_lookup_type;
+for my $lookup_type ( keys %lookup_type ) {
+    my ( $last_word ) = $lookup_type =~ /(\w+)$/;
+    $variant_lookup_type{ lc $last_word } = $lookup_type;
+
+    # also support case insensitive
+    $variant_lookup_type{ lc $lookup_type } = $lookup_type;
+}
+
+if ( !$OPT{type} || !$type{ lc $OPT{type} } ) {
+    RT::Interface::CLI->ShowHelp(
+        Message  => 'Invalid type',
+        Sections => 'NAME|SYNOPSIS|OPTIONS',
+    );
+}
+
+my $lookup_type = $OPT{'lookup-type'};
+$lookup_type = $variant_lookup_type{ lc $lookup_type } if $lookup_type;
+
+if ( !$lookup_type ) {
+    RT::Interface::CLI->ShowHelp(
+        Message  => 'Invalid lookup-type',
+        Sections => 'NAME|SYNOPSIS|OPTIONS',
+    );
+}
+
+my $cfs = RT::CustomFields->new( RT->SystemUser );
+$cfs->LimitToLookupType( $lookup_type );
+
+$OPT{ratio} = 10 unless $OPT{ratio} >= 1;
+
+if ( $cfs->Count == 0 ) {
+    print "Nothing to change\n";
+    exit;
+}
+
+my $span = $cfs->Count * $OPT{ratio};
+
+my %order;
+my %ocf;
+
+$cfs = RT::CustomFields->new( RT->SystemUser );
+$cfs->LimitToLookupType( $lookup_type );
+$cfs->LimitToGlobalOrObjectId( 0 );
+my $ocfs = RT::ObjectCustomFields->new( RT->SystemUser );
+$ocfs->Limit(
+    FIELD    => 'CustomField',
+    VALUE    => [ map { $_->id } @{ $cfs->ItemsArrayRef } ],
+    OPERATOR => 'IN',
+);
+$ocfs->Limit( FIELD => 'ObjectId', VALUE => [0], OPERATOR => 'IN' );
+
+while ( my $ocf = $ocfs->Next ) {
+    $ocf{ $ocf->id } = $ocf;
+    push @{ $order{0} }, $ocf->id;
+}
+
+my $queues = RT::Queues->new( RT->SystemUser );
+$queues->UnLimit;
+while ( my $queue = $queues->Next ) {
+    my $cfs = RT::CustomFields->new( RT->SystemUser );
+    $cfs->LimitToLookupType( $lookup_type );
+    $cfs->LimitToGlobalOrObjectId( $queue->id );
+    next unless $cfs->Count > 1;
+    my $ocfs = RT::ObjectCustomFields->new( RT->SystemUser );
+    $ocfs->Limit(
+        FIELD    => 'CustomField',
+        VALUE    => [ map { $_->id } @{ $cfs->ItemsArrayRef } ],
+        OPERATOR => 'IN',
+    );
+    $ocfs->Limit(
+        FIELD    => 'ObjectId',
+        VALUE    => [ 0, $queue->id ],
+        OPERATOR => 'IN'
+    );
+    while ( my $ocf = $ocfs->Next ) {
+        $ocf{ $ocf->id } = $ocf;
+        push @{ $order{ $queue->id } }, $ocf->id;
+    }
+}
+
+my %sort_order;
+
+for ( my $i = 0 ; $i < @{ $order{0} } ; $i++ ) {
+    my $ocf_id = $order{0}[$i];
+    $sort_order{0}{$ocf_id} = $span * ( $i + 1 );
+}
+
+for my $q_id ( grep { $_ != 0 } keys %order ) {
+    my $base = 0;
+    my $r    = 0;    # reset every time we see global cfs
+    for ( my $i = 0 ; $i < @{ $order{$q_id} } ; $i++ ) {
+        my $ocf_id = $order{$q_id}[$i];
+        my $ocf    = $ocf{$ocf_id};
+        if ( $ocf->ObjectId == 0 ) {
+            $base = $sort_order{0}{$ocf_id};
+            $r    = 0;
+        }
+        else {
+            $sort_order{$q_id}{$ocf_id} = $base + $r;
+        }
+        $r++;
+    }
+}
+
+binmode( STDOUT, ":utf8" );
+
+$RT::Handle->BeginTransaction unless $OPT{dryrun};
+
+if ( !%sort_order ) {
+    print "Nothing need to do\n";
+    exit;
+}
+
+for my $q_id ( sort { $a <=> $b } keys %sort_order ) {
+    my $q_name;
+    if ( $q_id ) {
+        my $queue = RT::Queue->new( RT->SystemUser );
+        $queue->Load( $q_id );
+        $q_name = $queue->Name;
+    }
+    else {
+        $q_name = 'Global';
+    }
+
+    print $q_name, ":\n";
+
+    for my $ocf_id (
+        sort { $sort_order{$q_id}{$a} <=> $sort_order{$q_id}{$b} }
+        keys %{ $sort_order{$q_id} }
+      )
+    {
+        my $ocf       = $ocf{$ocf_id};
+        my $new_order = $sort_order{$q_id}{$ocf_id};
+        print "\t", $ocf->CustomFieldObj->Name, '(#' . $ocf->CustomField . '): ', $new_order, "\n";
+        if ( !$OPT{dryrun} ) {
+            if ( $ocf->SortOrder != $new_order ) {
+                my ( $ret, $msg ) =
+                  $ocf->SetSortOrder( $sort_order{$q_id}{$ocf_id} );
+                if ( !$ret ) {
+                    $RT::Handle->Rollback;
+                    RT->Logger->error( $msg );
+                    exit 1;
+                }
+            }
+        }
+    }
+}
+
+unless ( $OPT{dryrun} ) {
+    $RT::Handle->Commit;
+    print "Done.\n";
+}
+
+__END__
+
+=head1 NAME
+
+rt-rebuild-sort-order - Rebuild SortOrder of objects
+
+=head1 SYNOPSIS
+
+    rt-rebuild-sort-order --type CustomField --lookup-type RT::Queue-RT::Ticket --dryrun
+
+    rt-rebuild-sort-order --type CustomField --lookup-type RT::Queue-RT::Ticket
+    rt-rebuild-sort-order --type CustomField --lookup-type Ticket # ditto
+
+=head1 DESCRIPTION
+
+For objects like ticket custom fields that support both global and local apply
+approaches(globally or at queue level), updating SortOrder of global custom
+fields might cause duplicate SortOrders at queue level, which is annoying.
+
+This script rebuilds the whole SortOrders in a manner that makes global ones
+sparse enough so you can easily adjust local ones later without worrying about
+available positions.
+
+=head1 OPTIONS
+
+=over
+
+=item type
+
+Available values are:
+
+=over
+
+=item C<CustomField>
+
+=back
+
+=item lookup-type
+
+Available values are:
+
+=over
+
+=item C<RT::Queue-RT::Ticket> or C<Ticket>
+
+=item C<RT::Queue-RT::Ticket-RT::Transaction> or C<Transaction>
+
+=item C<RT::Class-RT::Article> or C<Article>
+
+=item C<RT::Catalog-RT::Asset> or C<Asset>
+
+=back
+
+=item ratio
+
+Use this to specify the sparseness of global sort orders.
+
+Bigger values means more sparse global sort orders.
+
+Default value is 10.
+
+=item dryrun
+
+Show re-generated values, don't actually update.
+
+=item help
+
+Print this message
+
+--help is equal to -h
+
+=back

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


More information about the rt-commit mailing list