[Rt-commit] rt branch, 5.0-trunk, updated. rt-5.0.0alpha1-261-g9a7e3e2dbc

Jim Brandt jbrandt at bestpractical.com
Wed May 6 10:57:51 EDT 2020


The branch, 5.0-trunk has been updated
       via  9a7e3e2dbc51b5f82abe69a241b7431667908e81 (commit)
       via  6dd0e50ac5a43b2f0679d062c044880d11780000 (commit)
       via  412706a7a12775d3de66f37219a4b3bf666fe224 (commit)
       via  ba040fedf42a2e88aff65481e2d9bb321976ea5f (commit)
       via  1de7733dc381b0b92f49d0beb9bee200f6881166 (commit)
       via  3703add97f4d07faf3c8ae7fd2ab561d8ff6af3c (commit)
       via  b89b8c63df072746bf25c05f81f851d1f034f651 (commit)
       via  f59f56470c8c9e2f0bc9b7dfc56c75e4292b9e47 (commit)
       via  b1b368d5cf91c6a396ad6f9393f54060b331cea1 (commit)
       via  da6d1a98b59bd1e44e519c0717b674ccc010873f (commit)
       via  ab70a56c8cd88b1bb92169cb010597ba782efdbd (commit)
       via  352d0ce7f8c5ed1303f92f803d5f085c4c7178ba (commit)
       via  f6c3e4874216940631defaa2bf0600046a8efa5a (commit)
       via  93ccb9844398f6eda63ac05d1c6a1b91d1db0827 (commit)
      from  34aae6b5e89d8153fc17380ef09c2db553e34cfe (commit)

Summary of changes:
 etc/initialdata                                    |  8 +++++
 etc/upgrade/4.5.6/content                          | 13 ++++++++
 lib/RT/Interface/Web/MenuBuilder.pm                | 35 +++++++++++++++-------
 lib/RT/System.pm                                   |  2 ++
 .../SelectRoleType => Elements/SelectLifecycle}    | 22 ++++++++------
 share/html/Search/Build.html                       | 29 ++++++++++++++----
 share/html/Search/Bulk.html                        | 11 +++++++
 share/html/Search/Chart.html                       | 23 +++++++++++++-
 share/html/Search/Edit.html                        | 13 ++++++++
 share/html/Search/Elements/EditSearches            | 26 ++++++++++++++--
 share/html/Search/Elements/PickBasics              | 13 ++++++++
 share/html/Search/Results.html                     | 12 +++++++-
 12 files changed, 177 insertions(+), 30 deletions(-)
 create mode 100644 etc/upgrade/4.5.6/content
 copy share/html/{Asset/Elements/SelectRoleType => Elements/SelectLifecycle} (80%)

- Log -----------------------------------------------------------------
commit 93ccb9844398f6eda63ac05d1c6a1b91d1db0827
Author: Aaron Trevena <ast at bestpractical.com>
Date:   Fri Mar 20 15:56:42 2020 +0000

    Schema updates for tracking db configuration changes in transactions

diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 611e37a927..2b366299cf 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -112,7 +112,7 @@ CREATE TABLE Transactions (
         ObjectId                NUMBER(11,0) DEFAULT 0 NOT NULL,
         TimeTaken               NUMBER(11,0) DEFAULT 0 NOT NULL,
         Type                    VARCHAR2(20),
-        Field                   VARCHAR2(40),
+        Field                   VARCHAR2(255),
         OldValue                VARCHAR2(255),
         NewValue                VARCHAR2(255),
         ReferenceType           VARCHAR2(255),
@@ -552,6 +552,5 @@ CREATE TABLE Configurations (
     LastUpdated     DATE
 );
 
-CREATE UNIQUE INDEX Configurations1 ON Configurations (LOWER(Name));
+CREATE INDEX Configurations1 ON Configurations (LOWER(Name), Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
-
diff --git a/etc/schema.Pg b/etc/schema.Pg
index a027d35056..c51be5dff3 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -189,7 +189,7 @@ CREATE TABLE Transactions (
   ObjectId integer NOT NULL DEFAULT 0  ,
   TimeTaken integer NOT NULL DEFAULT 0  ,
   Type varchar(20) NULL  ,
-  Field varchar(40) NULL  ,
+  Field varchar(255) NULL  ,
   OldValue varchar(255) NULL  ,
   NewValue varchar(255) NULL  ,
   ReferenceType varchar(255) NULL,
@@ -793,6 +793,6 @@ CREATE TABLE Configurations (
     PRIMARY KEY (id)
 );
 
-CREATE UNIQUE INDEX Configurations1 ON Configurations (LOWER(Name));
+CREATE INDEX Configurations1 ON Configurations (LOWER(Name), Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
 
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index b6e0166345..c51070aa87 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -121,7 +121,7 @@ CREATE TABLE Transactions (
   ObjectId integer NULL DEFAULT 0 ,
   TimeTaken integer NULL DEFAULT 0 ,
   Type varchar(20) collate NOCASE NULL  ,
-  Field varchar(40) collate NOCASE NULL  ,
+  Field varchar(255) collate NOCASE NULL  ,
   OldValue varchar(255) collate NOCASE NULL  ,
   NewValue varchar(255) collate NOCASE NULL  ,
   ReferenceType varchar(255) collate NOCASE NULL  ,
@@ -582,6 +582,6 @@ CREATE TABLE Configurations (
     LastUpdated       timestamp                DEFAULT NULL
 );
 
-CREATE UNIQUE INDEX Configurations1 ON Configurations (Name);
+CREATE INDEX Configurations1 ON Configurations (Name, Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
 
diff --git a/etc/schema.mysql b/etc/schema.mysql
index 706ba25137..ca90073340 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -108,7 +108,7 @@ CREATE TABLE Transactions (
   ObjectId integer NOT NULL DEFAULT 0  ,
   TimeTaken integer NOT NULL DEFAULT 0  ,
   Type varchar(20) CHARACTER SET ascii NULL,
-  Field varchar(40) CHARACTER SET ascii NULL,
+  Field varchar(255) CHARACTER SET ascii NULL,
   OldValue varchar(255) NULL  ,
   NewValue varchar(255) NULL  ,
   ReferenceType varchar(255) CHARACTER SET ascii NULL,
@@ -573,6 +573,6 @@ CREATE TABLE Configurations (
     PRIMARY KEY (id)
 ) ENGINE=InnoDB CHARACTER SET utf8mb4;
 
-CREATE UNIQUE INDEX Configurations1 ON Configurations (Name);
+CREATE INDEX Configurations1 ON Configurations (Name, Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
 
diff --git a/etc/upgrade/4.5.3/schema.Oracle b/etc/upgrade/4.5.3/schema.Oracle
new file mode 100644
index 0000000000..c4e069a4a7
--- /dev/null
+++ b/etc/upgrade/4.5.3/schema.Oracle
@@ -0,0 +1,5 @@
+-- Add transaction support for new config in database feature
+ALTER TABLE Transactions MODIFY Field VARCHAR2(255);
+
+DROP INDEX Configurations1;
+CREATE INDEX Configurations1 ON Configurations (LOWER(Name), Disabled);
diff --git a/etc/upgrade/4.5.3/schema.Pg b/etc/upgrade/4.5.3/schema.Pg
new file mode 100644
index 0000000000..3df15eacad
--- /dev/null
+++ b/etc/upgrade/4.5.3/schema.Pg
@@ -0,0 +1,5 @@
+-- Add transaction support for new config in database feature
+ALTER TABLE Transactions ALTER COLUMN Field TYPE varchar(255);
+
+DROP INDEX IF EXISTS Configurations1;
+CREATE INDEX Configurations1 ON Configurations (LOWER(Name), Disabled);
diff --git a/etc/upgrade/4.5.3/schema.SQLite b/etc/upgrade/4.5.3/schema.SQLite
new file mode 100644
index 0000000000..88d1ca8dc6
--- /dev/null
+++ b/etc/upgrade/4.5.3/schema.SQLite
@@ -0,0 +1,2 @@
+DROP INDEX IF EXISTS Configurations1;
+CREATE INDEX Configurations1 ON Configurations (Name, Disabled);
diff --git a/etc/upgrade/4.5.3/schema.mysql b/etc/upgrade/4.5.3/schema.mysql
new file mode 100644
index 0000000000..8c3134f25d
--- /dev/null
+++ b/etc/upgrade/4.5.3/schema.mysql
@@ -0,0 +1,5 @@
+-- Add transaction support for new config in database feature
+ALTER TABLE Transactions MODIFY Field VARCHAR(255) CHARACTER SET ascii DEFAULT NULL;
+
+DROP INDEX Configurations1 ON Configurations;
+CREATE INDEX Configurations1 ON Configurations (Name, Disabled);
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index dd14ae9f95..edf223b4f5 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -2013,7 +2013,7 @@ sub _CoreAccessible {
         Type =>
                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
         Field =>
-                {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         OldValue =>
                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         NewValue =>

commit f6c3e4874216940631defaa2bf0600046a8efa5a
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Mar 31 06:10:17 2020 +0800

    Clean up RT::Configuration::_SerializeContent calls
    
    _SerializeContent doesn't need config name and always returns serialized
    value without error messages

diff --git a/lib/RT/Configuration.pm b/lib/RT/Configuration.pm
index 0d7c3ddf68..f79d9eb086 100644
--- a/lib/RT/Configuration.pm
+++ b/lib/RT/Configuration.pm
@@ -120,10 +120,7 @@ sub Create {
     return ( 0, $msg ) unless $id;
 
     if (ref ($args{'Content'}) ) {
-        ($args{'Content'}, my $error) = $self->_SerializeContent($args{'Content'}, $args{'Name'});
-        if ($error) {
-            return (0, $error);
-        }
+        $args{'Content'} = $self->_SerializeContent( $args{'Content'} );
         $args{'ContentType'} = 'perl';
     }
 
@@ -290,10 +287,7 @@ sub SetContent {
     }
 
     if (ref $value) {
-        ($value, my $error) = $self->_SerializeContent($value);
-        if ($error) {
-            return (0, $error);
-        }
+        $value = $self->_SerializeContent($value);
         $content_type = 'perl';
     }
 

commit 352d0ce7f8c5ed1303f92f803d5f085c4c7178ba
Author: Aaron Trevena <ast at bestpractical.com>
Date:   Mon Mar 23 13:21:36 2020 +0000

    Log DB config changes as transactions
    
    Implementation of storing Configuration change history in transactions

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 96e90eeeeb..9dc3c1e0e3 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -2586,7 +2586,7 @@ sub LoadConfigFromDatabase {
     $database_config_cache_time = time;
 
     my $settings = RT::Configurations->new(RT->SystemUser);
-    $settings->UnLimit;
+    $settings->LimitToEnabled;
 
     my %seen;
 
diff --git a/lib/RT/Configuration.pm b/lib/RT/Configuration.pm
index f79d9eb086..c8448acbb2 100644
--- a/lib/RT/Configuration.pm
+++ b/lib/RT/Configuration.pm
@@ -104,59 +104,50 @@ sub Create {
     return (0, $self->loc("Permission Denied"))
         unless $self->CurrentUserHasRight('SuperUser');
 
-    unless ( $args{'Name'} ) {
-        return ( 0, $self->loc("Must specify 'Name' attribute") );
+    if ( $args{'Name'} ) {
+        my ( $ok, $msg ) = $self->ValidateName( $args{'Name'} );
+        unless ($ok) {
+            return ($ok, $msg);
+        }
     }
-
-    my ( $id, $msg ) = $self->ValidateName( $args{'Name'} );
-    return ( 0, $msg ) unless $id;
-
-    my $meta = RT->Config->Meta($args{'Name'});
-    if ($meta->{Immutable}) {
-        return ( 0, $self->loc("You cannot update [_1] using database config; you must edit your site config", $args{'Name'}) );
+    else {
+        return ( 0, $self->loc("Must specify 'Name' attribute") );
     }
 
-    ( $id, $msg ) = $self->ValidateContent( Name => $args{'Name'}, Content => $args{'Content'} );
-    return ( 0, $msg ) unless $id;
 
-    if (ref ($args{'Content'}) ) {
-        $args{'Content'} = $self->_SerializeContent( $args{'Content'} );
-        $args{'ContentType'} = 'perl';
+    $RT::Handle->BeginTransaction;
+    my ( $id, $msg ) = $self->_Create(%args);
+    unless ($id) {
+        $RT::Handle->Rollback;
+        return ($id, $msg);
     }
 
-    my $old_value = RT->Config->Get($args{Name});
-    unless (defined($old_value) && length($old_value)) {
-        $old_value = $self->loc('(no value)');
+    my ($content, $error) = $self->Content;
+    unless (defined($content) && length($content)) {
+        $content = $self->loc('(no value)');
     }
 
-    ( $id, $msg ) = $self->SUPER::Create(
-        map { $_ => $args{$_} } grep {exists $args{$_}}
-            qw(Name Content ContentType),
+    my ( $Trans, $tx_msg, $TransObj ) = $self->_NewTransaction(
+        Type => 'SetConfig',
+        Field => $self->Name,
+        ObjectType => 'RT::Configuration',
+        ObjectId => $self->id,
+        ReferenceType => ref($self),
+        NewReference => $self->id,
     );
-    unless ($id) {
-        return (0, $self->loc("Setting [_1] to [_2] failed: [_3]", $args{Name}, $args{Content}, $msg));
+    unless ($Trans) {
+        $RT::Handle->Rollback;
+        return (0, $self->loc("Setting [_1] to [_2] failed: [_3]", $args{Name}, $content, $tx_msg));
     }
 
+    $RT::Handle->Commit;
     RT->Config->ApplyConfigChangeToAllServerProcesses;
 
-    my ($content, $error) = $self->Content;
-    unless (defined($content) && length($content)) {
-        $content = $self->loc('(no value)');
-    }
-
+    my $old_value = RT->Config->Get($args{Name});
     if ( ref $old_value ) {
         $old_value = $self->_SerializeContent($old_value);
     }
-
-    RT->Logger->info(
-        sprintf(
-            '%s changed %s from "%s" to "%s"',
-            $self->CurrentUser->Name,
-            $self->Name,
-            $old_value // '',
-            $content // ''
-        )
-    );
+    RT->Logger->info($self->CurrentUser->Name . " changed " . $args{Name});
     return ( $id, $self->loc( '[_1] changed from "[_2]" to "[_3]"', $self->Name, $old_value // '', $content // '' ) );
 }
 
@@ -215,8 +206,8 @@ sub ValidateName {
 
     return ( 0, $self->loc('empty name') ) unless defined $name && length $name;
 
-    my $TempSetting = RT::Configuration->new( RT->SystemUser );
-    $TempSetting->Load($name);
+    my $TempSetting  = RT::Configuration->new( RT->SystemUser );
+    $TempSetting->LoadByCols(Name => $name, Disabled => 0);
 
     if ( $TempSetting->id && ( !$self->id || $TempSetting->id != $self->id ) ) {
         return ( 0, $self->loc('Name in use') );
@@ -236,10 +227,32 @@ processes.
 sub Delete {
     my $self = shift;
     return (0, $self->loc("Permission Denied")) unless $self->CurrentUserCanSee;
-    my ($ok, $msg) = $self->SUPER::Delete(@_);
-    return ($ok, $msg) if !$ok;
+
+    $RT::Handle->BeginTransaction;
+    my ( $ok, $msg ) = $self->SetDisabled( 1 );
+    unless ($ok) {
+        $RT::Handle->Rollback;
+        return ($ok, $msg);
+    }
+
+    my ( $Trans, $tx_msg, $TransObj ) = $self->_NewTransaction(
+        Type => 'DeleteConfig',
+        Field => $self->Name,
+        ObjectType => 'RT::Configuration',
+        ObjectId => $self->Id,
+        ReferenceType => ref($self),
+        OldReference => $self->id,
+    );
+
+    unless ($Trans) {
+        $RT::Handle->Rollback();
+        return ( 0, $self->loc( "Deleting [_1] failed: [_2]", $self->Name, $tx_msg ) );
+    }
+
+    $RT::Handle->Commit;
     RT->Config->ApplyConfigChangeToAllServerProcesses;
     RT->Logger->info($self->CurrentUser->Name . " removed database setting for " . $self->Name);
+
     return ($ok, $self->loc("Database setting removed."));
 }
 
@@ -273,54 +286,71 @@ sub DecodedContent {
 
 sub SetContent {
     my $self         = shift;
-    my $value        = shift;
+    my $raw_value    = shift;
     my $content_type = shift || '';
 
     return (0, $self->loc("Permission Denied")) unless $self->CurrentUserCanSee;
 
-    my ( $ok, $msg ) = $self->ValidateContent( Content => $value );
+    my ( $ok, $msg ) = $self->ValidateContent( Content => $raw_value );
     return ( 0, $msg ) unless $ok;
 
-    my ($old_value, $error) = $self->Content;
-    unless (defined($old_value) && length($old_value)) {
-        $old_value = $self->loc('(no value)');
-    }
-
+    my $value = $raw_value;
     if (ref $value) {
-        $value = $self->_SerializeContent($value);
+        $value = $self->_SerializeContent($value, $self->Name);
         $content_type = 'perl';
     }
+    if ($self->Content eq $value) {
+        return (0, $self->loc("[_1] update: Nothing changed", ucfirst($self->Name)));
+    }
 
     $RT::Handle->BeginTransaction;
+    ( $ok, $msg ) = $self->SetDisabled( 1 );
+    unless ($ok) {
+        $RT::Handle->Rollback;
+        return ($ok, $msg);
+    }
+
+    my ($old_value, $error) = $self->Content;
+    my $old_id = $self->id;
+    my ( $new_id, $new_msg ) = $self->_Create(
+        Name => $self->Name,
+        Content => $raw_value,
+        ContentType => $content_type,
+    );
 
-    ($ok, $msg) = $self->_Set( Field => 'Content', Value => $value );
-    if (!$ok) {
+    unless ($new_id) {
         $RT::Handle->Rollback;
-        return ($ok, $self->loc("Unable to update [_1]: [_2]", $self->Name, $msg));
+        return (0, $self->loc("Setting [_1] to [_2] failed: [_3]", $self->Name, $value, $new_msg));
     }
 
-    if ($self->ContentType ne $content_type) {
-        ($ok, $msg) = $self->_Set( Field => 'ContentType', Value => $content_type );
-        if (!$ok) {
-            $RT::Handle->Rollback;
-            return ($ok, $self->loc("Unable to update [_1]: [_2]", $self->Name, $msg));
-        }
+    unless (defined($value) && length($value)) {
+        $value = $self->loc('(no value)');
+    }
+
+    my ( $Trans, $tx_msg, $TransObj ) = $self->_NewTransaction(
+        Type => 'SetConfig',
+        Field => $self->Name,
+        ObjectType => 'RT::Configuration',
+        ObjectId => $new_id,
+        ReferenceType => ref($self),
+        OldReference => $old_id,
+        NewReference => $new_id,
+    );
+    unless ($Trans) {
+        $RT::Handle->Rollback();
+        return (0, $self->loc("Setting [_1] to [_2] failed: [_3]", $self->Name, $value, $tx_msg));
     }
 
     $RT::Handle->Commit;
     RT->Config->ApplyConfigChangeToAllServerProcesses;
 
-    unless (defined($value) && length($value)) {
-        $value = $self->loc('(no value)');
+    RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name);
+    unless (defined($old_value) && length($old_value)) {
+        $old_value = $self->loc('(no value)');
     }
 
-    if (!ref($value) && !ref($old_value)) {
-        RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name . " from " . $old_value . " to " . $value);
-        return ($ok, $self->loc('[_1] changed from "[_2]" to "[_3]"', $self->Name, $old_value, $value));
-    } else {
-        RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name);
-        return ($ok, $self->loc("[_1] changed", $self->Name));
-    }
+    return( 1, $self->loc('[_1] changed from "[_2]" to "[_3]"', $self->Name, $old_value // '', $value // '') );
+
 }
 
 =head2 ValidateContent
@@ -357,6 +387,43 @@ sub ValidateContent {
 Documented for internal use only, do not call these from outside
 RT::Configuration itself.
 
+=head2 _Create
+
+Checks that the field being created/updated is not immutable, before calling
+C<SUPER::Create> to save changes in a new row, returning id of new row on success
+ and 0, and message on failure.
+
+=cut
+
+sub _Create {
+    my $self = shift;
+    my %args = (
+        Name => '',
+        Content => '',
+        ContentType => '',
+        @_
+    );
+    my $meta = RT->Config->Meta($args{'Name'});
+    if ($meta->{Immutable}) {
+        return ( 0, $self->loc("You cannot update [_1] using database config; you must edit your site config", $args{'Name'}) );
+    }
+
+    if ( ref( $args{'Content'} ) ) {
+        $args{'Content'} = $self->_SerializeContent( $args{'Content'}, $args{'Name'} );
+        $args{'ContentType'} = 'perl';
+    }
+
+    my ( $id, $msg ) = $self->SUPER::Create(
+        map { $_ => $args{$_} } qw(Name Content ContentType),
+    );
+    unless ($id) {
+        return (0, $self->loc("Setting [_1] to [_2] failed: [_3]", $args{Name}, $args{Content}, $msg));
+    }
+
+    return ($id, $msg);
+}
+
+
 =head2 _Set
 
 Checks if the current user has I<SuperUser> before calling
@@ -397,6 +464,7 @@ sub _SerializeContent {
     my $content = shift;
     require Data::Dumper;
     local $Data::Dumper::Terse = 1;
+    local $Data::Dumper::Sortkeys = 1;
     my $frozen = Data::Dumper::Dumper($content);
     chomp $frozen;
     return $frozen;
diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index 603c46ce8c..8a88ee22c8 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -138,16 +138,8 @@ if (delete $ARGS{Update}) {
             }
 
             my $setting = RT::Configuration->new($session{CurrentUser});
-            $setting->Load($key);
+            $setting->LoadByCols(Name => $key, Disabled => 0);
             if ($setting->Id) {
-                if ($setting->Disabled) {
-                    my ($ok, $msg) = $setting->SetDisabled(0);
-                    if (!$ok) {
-                        push @results, $msg;
-                        $has_error++;
-                    }
-                }
-
                 my ($ok, $msg) = $setting->SetContent($val);
                 push @results, $msg;
                 $has_error++ if !$ok;

commit ab70a56c8cd88b1bb92169cb010597ba782efdbd
Author: Aaron Trevena <ast at bestpractical.com>
Date:   Mon Mar 23 13:23:09 2020 +0000

    Remove unused stringify function in configuration edit page

diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index 8a88ee22c8..788e1806f3 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -62,19 +62,6 @@ my $active_context = {
 
 my @results;
 
-use Data::Dumper;
-my $stringify = sub {
-    my $value = shift;
-    return "" if !defined($value);
-
-    local $Data::Dumper::Terse = 1;
-    local $Data::Dumper::Indent = 2;
-    local $Data::Dumper::Sortkeys = 1;
-    my $output = Dumper $value;
-    chomp $output;
-    return $output;
-};
-
 if (delete $ARGS{Update}) {
     RT->Config->BeginDatabaseConfigChanges;
     $RT::Handle->BeginTransaction;

commit da6d1a98b59bd1e44e519c0717b674ccc010873f
Author: Aaron Trevena <ast at bestpractical.com>
Date:   Mon Mar 23 14:39:30 2020 +0000

    Page to view DB config transaction history
    
    Added page to view transaction log or configurations

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 8d40911f17..d9f7f6944b 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -712,9 +712,10 @@ sub BuildMainNav {
         $page->child( edit => raw_html => q[<a id="page-edit" class="menu-item" href="] . RT->Config->Get('WebPath') . qq[/Prefs/MyRT.html"><span class="fas fa-cog" alt="$alt" data-toggle="tooltip" data-placement="top" data-original-title="$alt"></span></a>] );
     }
 
-    if ( $request_path =~ m{^/Admin/Tools/(Configuration|EditConfig)} ) {
+    if ( $request_path =~ m{^/Admin/Tools/(Configuration|EditConfig|ConfigHistory)} ) {
         $page->child( display => title => loc('View'), path => "/Admin/Tools/Configuration.html" );
-        $page->child( history => title => loc('Edit'), path => "/Admin/Tools/EditConfig.html" );
+        $page->child( modify => title => loc('Edit'), path => "/Admin/Tools/EditConfig.html" );
+        $page->child( history => title => loc('History'), path => "/Admin/Tools/ConfigHistory.html" );
     }
 
     # due to historical reasons of always having been in /Elements/Tabs
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index edf223b4f5..3cc939864a 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -1394,6 +1394,32 @@ sub _CanonicalizeRoleName {
         my $self = shift;
         return "Attachment content modified";
     },
+    SetConfig => sub  {
+        my $self = shift;
+        my ($new_value, $old_value);
+
+        # pull in new value from reference if exists
+        if ( $self->NewReference ) {
+            my $newobj = RT::Configuration->new($self->CurrentUser);
+            $newobj->Load($self->NewReference);
+            $new_value = $newobj->Content;
+        }
+
+        # pull in old value from reference if exists
+        if ( $self->OldReference ) {
+            my $oldobj = RT::Configuration->new($self->CurrentUser);
+            $oldobj->Load($self->OldReference);
+            $old_value = $oldobj->Content;
+            return ('[_1] changed from "[_2]" to "[_3]"', $self->Field, $old_value // '', $new_value // ''); #loc()
+        }
+        else {
+            return ('[_1] changed to "[_2]"', $self->Field, $new_value // ''); #loc()
+        }
+    },
+    DeleteConfig => sub  {
+        my $self = shift;
+        return ('[_1] deleted"', $self->Field); #loc()
+    }
 );
 
 
diff --git a/share/html/Admin/Tools/ConfigHistory.html b/share/html/Admin/Tools/ConfigHistory.html
new file mode 100644
index 0000000000..6ea2904ba0
--- /dev/null
+++ b/share/html/Admin/Tools/ConfigHistory.html
@@ -0,0 +1,79 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 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 }}}
+<%INIT>
+my $title = loc('System Configuration');
+unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'SuperUser')) {
+  Abort(loc('This feature is only available to system administrators'));
+}
+
+my $Transactions = RT::Transactions->new($session{CurrentUser});
+$Transactions->Limit(FIELD => 'ObjectType', VALUE =>  'RT::Configuration');
+$Transactions->OrderBy(FIELD => 'Created', ORDER => 'DESC');
+</%INIT>
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<div class="configuration history">
+ <& /Admin/Elements/ConfigHelp &>
+ <&|/Widgets/TitleBox, title => loc('History')  &>
+   <div class="history-container">
+% my $i = 1;
+% while (my $tx = $Transactions->Next()) {
+    <& /Elements/ShowTransaction,
+    Transaction => $tx,
+    ShowHeaders => 1,
+    ShowDisplayModes => 0,
+    ShowActions => 1,
+    DisplayPath => 'ConfigHistory.html',
+    HasTxnCFs => 0,
+    RowNum => $i
+     &>
+% $i++;
+% }
+  </div>
+ </&>
+</div>

commit b1b368d5cf91c6a396ad6f9393f54060b331cea1
Author: Aaron Trevena <ast at bestpractical.com>
Date:   Mon Mar 23 16:42:26 2020 +0000

    Tests for DB config transactions and history
    
    Added unit tests for storing and viewing Configuration history.

diff --git a/t/web/admin_tools_editconfig.t b/t/web/admin_tools_editconfig.t
index 5b4cbd43ac..c43913d6d1 100644
--- a/t/web/admin_tools_editconfig.t
+++ b/t/web/admin_tools_editconfig.t
@@ -10,6 +10,7 @@ my ( $url, $m ) = RT::Test->started_ok;
 ok( $m->login(), 'logged in' );
 
 $m->follow_link_ok( { text => 'System Configuration' }, 'followed link to "System Configuration"' );
+$m->follow_link_ok( { text => 'History' }, 'followed link to History page' );
 $m->follow_link_ok( { text => 'Edit' }, 'followed link to Edit page' );
 
 my $tests = [
@@ -41,6 +42,24 @@ my $tests = [
 
 run_test( %{$_} ) for @{$tests};
 
+# check tx log for configuration
+my $transactions = RT::Transactions->new(RT->SystemUser);
+$transactions->Limit(FIELD => 'ObjectType', VALUE =>  'RT::Configuration');
+$transactions->OrderBy(FIELD => 'Created', ORDER => 'ASC');
+my $tx_items = $transactions->ItemsArrayRef;
+
+my $i = 0;
+foreach my $change (@{$tests}) {
+    check_transaction( $tx_items->[$i++], $change );
+}
+
+# check config history page
+$m->get_ok( $url . '/Admin/Tools/ConfigHistory.html');
+$i = 0;
+foreach my $change (@{$tests}) {
+    check_history_page_item($tx_items->[$i++], $change );
+}
+
 sub run_test {
     my %args = @_;
 
@@ -74,6 +93,27 @@ sub run_test {
     cmp_deeply( $rt_config_value, $args{new_value}, 'value from RT->Config->Get matches new value' );
 }
 
+sub check_transaction {
+    my ($tx, $change) = @_;
+    is($tx->ObjectType, 'RT::Configuration', 'tx is config change');
+    is($tx->Field, $change->{setting}, 'tx matches field changed');
+    is($tx->NewValue, stringify($change->{new_value}), 'tx value matches');
+}
+
+sub check_history_page_item {
+    my ($tx, $change) = @_;
+    my $link = sprintf('ConfigHistory.html?id=%d#txn-%d', $tx->ObjectId, $tx->id);
+    ok($m->find_link(url => $link), 'found tx link in history');
+    $m->text_contains(compactify($change->{new_value}), 'fetched tx has new value');
+    $m->text_contains("$change->{setting} changed", 'fetched tx has changed field');
+}
+
+sub compactify {
+    my $value = stringify(shift);
+    $value =~ s/\s+/ /g;
+    return $value;
+}
+
 sub stringify {
     my $value = shift;
 

commit f59f56470c8c9e2f0bc9b7dfc56c75e4292b9e47
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Mar 31 04:40:05 2020 +0800

    No need to compare 2 plain strings using cmp_deeply

diff --git a/t/web/admin_tools_editconfig.t b/t/web/admin_tools_editconfig.t
index c43913d6d1..826c8adf58 100644
--- a/t/web/admin_tools_editconfig.t
+++ b/t/web/admin_tools_editconfig.t
@@ -89,7 +89,7 @@ sub run_test {
 
     my $rt_config_value = RT->Config->Get( $args{setting} );
 
-    cmp_deeply( $rt_configuration_value, stringify($args{new_value}), 'value from RT::Configuration->Load matches new value' );
+    is( $rt_configuration_value, stringify($args{new_value}), 'value from RT::Configuration->Load matches new value' );
     cmp_deeply( $rt_config_value, $args{new_value}, 'value from RT->Config->Get matches new value' );
 }
 

commit b89b8c63df072746bf25c05f81f851d1f034f651
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Apr 3 16:36:49 2020 +0800

    Add ShowSearchAdvanced/ShowSearchBulkUpdate rights
    
    Sometimes we want to hide the 2 advanced and powerful search pages from
    users, and it's quite straightforward to achieve that using rights.
    
    We still show the 2 pages by default, for initial usability and also
    back compatibility.

diff --git a/etc/initialdata b/etc/initialdata
index 74951a5b85..d428d5c472 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -840,6 +840,14 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
       GroupType => 'privileged',
       Right  => 'ShowApprovalsTab', },
 
+    {   GroupDomain => 'SystemInternal',
+        GroupType   => 'Privileged',
+        Right       => 'ShowSearchAdvanced',
+    },
+    {   GroupDomain => 'SystemInternal',
+        GroupType   => 'Privileged',
+        Right       => 'ShowSearchBulkUpdate',
+    },
 );
 
 # Predefined searches
diff --git a/etc/upgrade/4.5.6/content b/etc/upgrade/4.5.6/content
new file mode 100644
index 0000000000..92d0386f96
--- /dev/null
+++ b/etc/upgrade/4.5.6/content
@@ -0,0 +1,13 @@
+use strict;
+use warnings;
+
+our @ACL = (
+    {   GroupDomain => 'SystemInternal',
+        GroupType   => 'Privileged',
+        Right       => 'ShowSearchAdvanced',
+    },
+    {   GroupDomain => 'SystemInternal',
+        GroupType   => 'Privileged',
+        Right       => 'ShowSearchBulkUpdate',
+    },
+);
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index d9f7f6944b..630292056f 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -555,8 +555,9 @@ sub BuildMainNav {
 
         $current_search_menu->child( edit_search =>
             title => loc('Edit Search'), path => "/Search/Build.html" . ( ($has_query) ? $args : '' ) );
-        $current_search_menu->child( advanced =>
-            title => loc('Advanced'),    path => "/Search/Edit.html$args" );
+        if ( $current_user->HasRight( Right => 'ShowSearchAdvanced', Object => RT->System ) ) {
+            $current_search_menu->child( advanced => title => loc('Advanced'), path => "/Search/Edit.html$args" );
+        }
         $current_search_menu->child( custom_date_ranges =>
             title => loc('Custom Date Ranges'), path => "/Search/CustomDateRanges.html" ) if $class eq 'RT::Tickets';
         if ($has_query) {
@@ -565,7 +566,9 @@ sub BuildMainNav {
 
         if ( $has_query ) {
             if ( $class eq 'RT::Tickets' ) {
-                $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" );
+                if ( $current_user->HasRight( Right => 'ShowSearchBulkUpdate', Object => RT->System ) ) {
+                    $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" );
+                }
                 $current_search_menu->child( chart => title => loc('Chart'),       path => "/Search/Chart.html$args" );
             }
 
diff --git a/lib/RT/System.pm b/lib/RT/System.pm
index e7b761eda1..e32cb4688d 100644
--- a/lib/RT/System.pm
+++ b/lib/RT/System.pm
@@ -94,6 +94,8 @@ __PACKAGE__->AddRight( General => LoadSavedSearch     => 'Allow loading of saved
 __PACKAGE__->AddRight( General => CreateSavedSearch   => 'Allow creation of saved searches'); # loc
 __PACKAGE__->AddRight( Admin   => ExecuteCode         => 'Allow writing Perl code in templates, scrips, etc'); # loc
 __PACKAGE__->AddRight( General => SeeSelfServiceGroupTicket => 'See tickets for other group members in SelfService' ); # loc
+__PACKAGE__->AddRight( Staff   => ShowSearchAdvanced    => 'Show search "Advanced" menu' ); # loc
+__PACKAGE__->AddRight( Staff   => ShowSearchBulkUpdate  => 'Show search "Bulk Update" menu' ); # loc
 
 =head2 AvailableRights
 
diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index 884d8a328e..e7cfedf9f3 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -379,6 +379,8 @@ $cfs->SetContextObject( values %$seen_queues ) if keys %$seen_queues == 1;
 </div>
 
 <%INIT>
+Abort( loc("Permission Denied") ) unless $session{CurrentUser}->HasRight( Right => 'ShowSearchBulkUpdate', Object => RT->System );
+
 my (@results);
 
 $m->callback(CallbackName => 'Initial', ARGSRef => \%ARGS, results_ref => \@results, QueryRef => \$Query, UpdateTicketRef => \@UpdateTicket);
diff --git a/share/html/Search/Edit.html b/share/html/Search/Edit.html
index 308af2295b..fb1724a6ac 100644
--- a/share/html/Search/Edit.html
+++ b/share/html/Search/Edit.html
@@ -70,6 +70,8 @@
 </form>
 
 <%INIT>
+Abort( loc("Permission Denied") ) unless $session{CurrentUser}->HasRight( Right => 'ShowSearchAdvanced', Object => RT->System );
+
 my $title;
 if ( $Class eq 'RT::Transactions' ) {
     $title = loc('Edit Transaction Query');

commit 3703add97f4d07faf3c8ae7fd2ab561d8ff6af3c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sun Apr 5 16:57:57 2020 +0800

    Show ticket shredder link on all ticket search pages
    
    Previously we only showed it on search results page, which was a bit
    inconvenient, especially considering that people can preview ticket list
    on shredder page before shredding.

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 630292056f..79574eecd3 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -597,11 +597,8 @@ sub BuildMainNav {
                     $rss_data{Query};
                 $more->child( ical => title => loc('iCal'), path => '/NoAuth/iCal/' . $ical_path );
 
-                if ($request_path =~ m{^/Search/Results.html}
-                    &&    #XXX TODO better abstraction
-                    $current_user->HasRight( Right => 'SuperUser', Object => RT->System )
-                   )
-                {
+                #XXX TODO better abstraction of SuperUser right check
+                if ( $current_user->HasRight( Right => 'SuperUser', Object => RT->System ) ) {
                     my $shred_args = QueryString(
                         Search          => 1,
                         Plugin          => 'Tickets',

commit 1de7733dc381b0b92f49d0beb9bee200f6881166
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Apr 8 03:50:49 2020 +0800

    Support to filter lifecycle in ticket search builder

diff --git a/share/html/Elements/SelectLifecycle b/share/html/Elements/SelectLifecycle
new file mode 100644
index 0000000000..b9f3329496
--- /dev/null
+++ b/share/html/Elements/SelectLifecycle
@@ -0,0 +1,64 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 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 }}}
+<select name="<% $Name %>" class="selectpicker form-control">
+% if ( $DefaultValue ) {
+  <option value="" <% $Default ? '' : qq[selected="selected"] |n %>><% $DefaultLabel %></option>
+% }
+% for my $lifecycle ( sort { loc $a cmp loc $b } @Lifecycles ) {
+%   my $selected = defined $Default && $lifecycle eq $Default ? 'selected="selected"' : '';
+  <option value="<% $lifecycle %>" <% $selected |n %>><% $lifecycle %></option>
+% }
+</select>
+
+<%ARGS>
+$Name           => ''
+$Default        => ''
+$DefaultValue   => 1
+$DefaultLabel   => '-'
+ at Lifecycles     => RT::Lifecycle->List
+</%ARGS>
diff --git a/share/html/Search/Elements/PickBasics b/share/html/Search/Elements/PickBasics
index e9d43aca52..6f8526758b 100644
--- a/share/html/Search/Elements/PickBasics
+++ b/share/html/Search/Elements/PickBasics
@@ -255,6 +255,19 @@ else {
                 Arguments => { NamedValues => 1, },
             },
         },
+        {
+            Name => 'Lifecycle',
+            Field => loc('Lifecycle'),
+            Op => {
+                Type      => 'component',
+                Path      => '/Elements/SelectBoolean',
+                Arguments => { TrueVal => '=', FalseVal => '!=' },
+            },
+            Value => {
+                Type => 'component',
+                Path => '/Elements/SelectLifecycle',
+            },
+        },
         {
             Name => 'Status',
             Field => loc('Status'),

commit ba040fedf42a2e88aff65481e2d9bb321976ea5f
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Apr 3 04:49:45 2020 +0800

    Add BeforeDisplay callback to search builder page

diff --git a/share/html/Search/Build.html b/share/html/Search/Build.html
index 1240dd8faa..9b8d767e28 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -352,6 +352,7 @@ elsif ( $query{'Query'} ) {
     $TabArgs{QueryArgs} = \%query;
 }
 
+$m->callback( ARGSRef => \%ARGS, Query => \%query, CallbackName => 'BeforeDisplay' );
 </%INIT>
 
 <%ARGS>

commit 412706a7a12775d3de66f37219a4b3bf666fe224
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Jun 5 04:15:56 2018 +0800

    Add ExtraQueryParams parameter to search pages
    
    With this, users can add extra parameters to search pages easily. e.g.
    to render customized search builder for RTIR searches, we can pass:
    
        ExtraQueryParams => 'RTIR', RTIR => 1
    
    Then both parameters will be kept during page navigation and also form
    submission.

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 79574eecd3..5bcdabf086 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -522,6 +522,13 @@ sub BuildMainNav {
             my %final_query_args = ();
             # key => callback to avoid unnecessary work
 
+            if ( my $extra_params = $query_args->{ExtraQueryParams} ) {
+                $final_query_args{ExtraQueryParams} = $extra_params;
+                for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+                    $final_query_args{$param} = $query_args->{$param};
+                }
+            }
+
             for my $param (keys %fallback_query_args) {
                 $final_query_args{$param} = defined($query_args->{$param})
                                           ? $query_args->{$param}
diff --git a/share/html/Search/Build.html b/share/html/Search/Build.html
index 9b8d767e28..3ae6514fc1 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -75,8 +75,14 @@
 <input type="hidden" class="hidden" name="Format" value="<% $query{'Format'} %>" />
 <input type="hidden" class="hidden" name="ObjectType" value="<% $query{'ObjectType'} %>" />
 <input type="hidden" class="hidden" name="Class" value="<% $Class %>" />
-
-
+% if ( $query{ExtraQueryParams} ) {
+%   for my $input ( ref $query{ExtraQueryParams} eq 'ARRAY' ?  @{$query{ExtraQueryParams}} : $query{ExtraQueryParams} ) {
+<input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+%       if ( defined $query{$input} ) {
+<input type="hidden" class="hidden" name="<% $input %>" value="<% $query{$input} %>" />
+%       }
+%   }
+% }
 
 <div class="row">
   <div class="col-xl-7">
@@ -134,7 +140,7 @@ else {
 }
 
 my %query;
-for( qw(Query Format OrderBy Order RowsPerPage Class ObjectType) ) {
+for( qw(Query Format OrderBy Order RowsPerPage Class ObjectType ExtraQueryParams), @ExtraQueryParams ) {
     $query{$_} = $ARGS{$_} if defined $ARGS{$_};
 }
 
@@ -155,7 +161,7 @@ if ( $NewQuery ) {
 
     # Wipe all data-carrying variables clear if we want a new
     # search, or we're deleting an old one..
-    %query = ();
+    %query = map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } 'ExtraQueryParams', @ExtraQueryParams;
     %saved_search = ( Id => 'new' );
 
     # ..then wipe the session out..
@@ -195,6 +201,13 @@ if ( $NewQuery ) {
         # Clean unwanted junk from the format
         $query{'Format'} = $m->comp( '/Elements/ScrubHTML', Content => $query{'Format'} );
     }
+
+    if ( !$ARGS{SavedSearchLoad} and ( my $extra_params = $current->{ExtraQueryParams} ) ) {
+        $query{ExtraQueryParams} //= $extra_params;
+        for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+            $query{$param} //= $current->{$param};
+        }
+    }
 }
 
 my $ParseQuery = sub {
@@ -360,4 +373,5 @@ $NewQuery => 0
 @clauses => ()
 $Class => 'RT::Tickets'
 $ObjectType => 'RT::Ticket'
+ at ExtraQueryParams => ()
 </%ARGS>
diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index e7cfedf9f3..3f2c8912c4 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -53,6 +53,14 @@
 % foreach my $var (qw(Query Format OrderBy Order Rows Page SavedSearchId SavedChartSearchId Token)) {
 <input type="hidden" class="hidden" name="<%$var%>" value="<%$ARGS{$var} || ''%>" />
 %}
+
+% for my $input ( @ExtraQueryParams ) {
+<input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+%   if ( defined $ARGS{$input} ) {
+<input type="hidden" class="hidden" name="<% $input %>" value="<% $ARGS{$input} %>" />
+%   }
+% }
+
 <& /Elements/CollectionList, 
     Query => $Query,
     DisplayFormat => $DisplayFormat,
@@ -518,4 +526,5 @@ $Order => 'ASC'
 $OrderBy => 'id'
 $Query => undef
 @UpdateTicket => ()
+ at ExtraQueryParams => ()
 </%args>
diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 5ee8ceb275..fd7e63d872 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -57,7 +57,7 @@ $m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' );
 
 my $title = loc( "Grouped search results");
 
-my @search_fields = qw(Query GroupBy ChartStyle ChartFunction Width Height);
+my @search_fields = ( qw(Query GroupBy ChartStyle ChartFunction Width Height ExtraQueryParams), @ExtraQueryParams );
 my $saved_search = $m->comp( '/Widgets/SavedSearch:new',
     SearchType   => 'Chart',
     SearchFields => [@search_fields],
@@ -76,6 +76,14 @@ my %query;
         foreach my $search_field (@{ $saved_search->{'SearchFields'} }) {
             $query{$search_field} = $saved_search->{'CurrentSearch'}->{'Object'}->Content->{$search_field};
         }
+
+        my $content = $saved_search->{'CurrentSearch'}->{'Object'}->Content;
+        if ( my $extra_params = $content->{ExtraQueryParams} ) {
+            $query{ExtraQueryParams} = $extra_params;
+            for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+                $query{$param} = $content->{$param};
+            }
+        }
     }
 
     my $current = $session{'CurrentSearchHash'};
@@ -140,6 +148,15 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
 <input type="hidden" class="hidden" name="Query" value="<% $query{Query} %>" />
 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" />
 
+% if ( $query{ExtraQueryParams} ) {
+%   for my $input ( ref $query{ExtraQueryParams} eq 'ARRAY' ?  @{$query{ExtraQueryParams}} : $query{ExtraQueryParams} ) {
+<input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+%       if ( defined $query{$input} ) {
+<input type="hidden" class="hidden" name="<% $input %>" value="<% $query{$input} %>" />
+%       }
+%   }
+% }
+
     <&| /Widgets/TitleBox, title => loc('Group by'), class => "chart-group-by" &>
       <fieldset><legend><% loc('Group tickets by') %></legend>
         <& Elements/SelectGroupBy,
@@ -261,3 +278,7 @@ jQuery(".chart-picture [name=ChartStyleIncludeSQL]").change( updateChartStyle );
 
 </div>
 </div>
+
+<%ARGS>
+ at ExtraQueryParams => ()
+</%ARGS>
diff --git a/share/html/Search/Edit.html b/share/html/Search/Edit.html
index fb1724a6ac..afaa6d8b89 100644
--- a/share/html/Search/Edit.html
+++ b/share/html/Search/Edit.html
@@ -55,6 +55,12 @@
 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $SavedChartSearchId %>" />
 <input type="hidden" class="hidden" name="Class" value="<% $Class %>" />
 <input type="hidden" class="hidden" name="ObjectType" value="<% $ObjectType %>" />
+% for my $input ( @ExtraQueryParams ) {
+<input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+%   if ( defined $ARGS{$input} ) {
+<input type="hidden" class="hidden" name="<% $input %>" value="<% $ARGS{$input} %>" />
+%   }
+% }
 
 <&|/Widgets/TitleBox, title => loc('Query'), &>
 <textarea class="form-control" name="Query" rows="8" cols="72"><% $Query %></textarea>
@@ -104,4 +110,5 @@ $Class         => 'RT::Tickets'
 $ObjectType    => 'RT::Ticket'
 
 @actions       => ()
+ at ExtraQueryParams => ()
 </%ARGS>
diff --git a/share/html/Search/Elements/EditSearches b/share/html/Search/Elements/EditSearches
index 3750212e16..0b742698f0 100644
--- a/share/html/Search/Elements/EditSearches
+++ b/share/html/Search/Elements/EditSearches
@@ -116,7 +116,7 @@ my $is_dirty = sub {
     my %arg = (
         Query       => {},
         SavedSearch => {},
-        SearchFields => [qw(Query Format OrderBy Order RowsPerPage ObjectType)],
+        SearchFields => [qw(Query Format OrderBy Order RowsPerPage ObjectType ExtraQueryParams), @ExtraQueryParams],
         @_
     );
 
@@ -149,6 +149,7 @@ $CurrentSearch => {}
 @SearchFields   => ()
 $AllowCopy     => 1
 $Title         => loc('Saved searches')
+ at ExtraQueryParams => ()
 </%ARGS>
 
 <%METHOD Init>
@@ -183,6 +184,24 @@ if ( $ARGS{'SavedSearchLoad'} ) {
         $SavedSearch->{'Description'} = $search->Description;
         $Query->{$_} = $search->SubValue($_) foreach @SearchFields;
 
+        if ( my $extra_params = $search->SubValue('ExtraQueryParams') ) {
+            $Query->{ExtraQueryParams} = $extra_params;
+            for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+                $Query->{$param} = $search->SubValue($param);
+            }
+        }
+        else {
+            delete $Query->{ExtraQueryParams};
+        }
+
+        # Remove all extra params not set in saved search.
+        if ( my $extra_params = $ARGS{ExtraQueryParams} ) {
+            for my $param ( ref $extra_params eq 'ARRAY' ? @$extra_params : $extra_params ) {
+                next if defined $search->SubValue($param);
+                delete $Query->{$param};
+            }
+        }
+
         if ( $ARGS{'SavedSearchRevert'} ) {
             push @results, loc('Loaded original "[_1]" saved search', $SavedSearch->{'Description'} );
         } else {
@@ -242,7 +261,8 @@ return @results;
 <%ARGS>
 $Query        => {}
 $SavedSearch  => {}
- at SearchFields => qw(Query Format OrderBy Order RowsPerPage ObjectType)
+ at ExtraQueryParams => ()
+ at SearchFields => ( qw(Query Format OrderBy Order RowsPerPage ObjectType ExtraQueryParams), @ExtraQueryParams )
 </%ARGS>
 <%INIT>
 
@@ -254,7 +274,7 @@ my $id   = $SavedSearch->{'Id'};
 my $desc = $SavedSearch->{'Description'};
 my $privacy = $SavedSearch->{'Privacy'};
 
-my %params = map { $_ => $Query->{$_} } @SearchFields;
+my %params = map { $_ => $Query->{$_} } grep { defined $Query->{$_} } @SearchFields;
 my ($new_obj_type, $new_obj_id) = split(/\-/, ($privacy || ''));
 
 if ( $obj && $obj->id ) {
diff --git a/share/html/Search/Results.html b/share/html/Search/Results.html
index 863c761eb5..a620591ba4 100644
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@ -75,7 +75,8 @@
     SavedSearchId => $ARGS{'SavedSearchId'},
     SavedChartSearchId => $ARGS{'SavedChartSearchId'},
     ObjectType => $ObjectType,
-    PassArguments => [qw(Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId Class ObjectType)],
+    @ExtraQueryParams ? ( map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } 'ExtraQueryParams', @ExtraQueryParams ) : (),
+    PassArguments => [qw(Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId Class ObjectType ExtraQueryParams), @ExtraQueryParams],
 &>
 % }
 % $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' );
@@ -86,6 +87,14 @@
 % foreach my $key (keys(%hiddens)) {
 <input type="hidden" class="hidden" name="<%$key%>" value="<% defined($hiddens{$key})?$hiddens{$key}:'' %>" />
 % }
+
+% for my $input ( @ExtraQueryParams ) {
+<input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+%   if ( defined $ARGS{$input} ) {
+<input type="hidden" class="hidden" name="<% $input %>" value="<% $ARGS{$input} %>" />
+%   }
+% }
+
 <div class="form-row">
   <div class="col-auto">
     <& /Elements/Refresh, Name => 'TicketsRefreshInterval', Default => $session{$interval_name}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &>
@@ -277,4 +286,5 @@ $SavedSearchId => undef
 $SavedChartSearchId => undef
 $Class => 'RT::Tickets'
 $ObjectType => 'RT::Ticket'
+ at ExtraQueryParams => ()
 </%ARGS>

commit 6dd0e50ac5a43b2f0679d062c044880d11780000
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Apr 7 03:05:38 2020 +0800

    Add ResultPage parameter to redirect to customized search result page

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 5bcdabf086..f6d1567f08 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -504,7 +504,7 @@ sub BuildMainNav {
                 map {
                     my $p = $_;
                     $p => $HTML::Mason::Commands::DECODED_ARGS->{$p} || $current_search->{$p}
-                } qw(Query Format OrderBy Order Page Class ObjectType)
+                } qw(Query Format OrderBy Order Page Class ObjectType ResultPage)
             ),
             RowsPerPage => (
                 defined $HTML::Mason::Commands::DECODED_ARGS->{'RowsPerPage'}
@@ -568,7 +568,13 @@ sub BuildMainNav {
         $current_search_menu->child( custom_date_ranges =>
             title => loc('Custom Date Ranges'), path => "/Search/CustomDateRanges.html" ) if $class eq 'RT::Tickets';
         if ($has_query) {
-            $current_search_menu->child( results => title => loc('Show Results'), path => "/Search/Results.html$args" );
+            my $result_page = $HTML::Mason::Commands::DECODED_ARGS->{ResultPage};
+            if ( my $web_path = RT->Config->Get('WebPath') ) {
+                $result_page =~ s!^$web_path!!;
+            }
+
+            $result_page ||= '/Search/Results.html';
+            $current_search_menu->child( results => title => loc('Show Results'), path => "$result_page$args" );
         }
 
         if ( $has_query ) {
diff --git a/share/html/Search/Build.html b/share/html/Search/Build.html
index 3ae6514fc1..2b27f83c98 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -75,6 +75,9 @@
 <input type="hidden" class="hidden" name="Format" value="<% $query{'Format'} %>" />
 <input type="hidden" class="hidden" name="ObjectType" value="<% $query{'ObjectType'} %>" />
 <input type="hidden" class="hidden" name="Class" value="<% $Class %>" />
+% if ( $ResultPage ) {
+<input type="hidden" class="hidden" name="ResultPage" value="<% $ResultPage %>" />
+% }
 % if ( $query{ExtraQueryParams} ) {
 %   for my $input ( ref $query{ExtraQueryParams} eq 'ARRAY' ?  @{$query{ExtraQueryParams}} : $query{ExtraQueryParams} ) {
 <input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
@@ -346,7 +349,7 @@ if ( $ARGS{'DoSearch'} ) {
         SavedChartSearchId => $ARGS{'SavedChartSearchId'},
         SavedSearchId => $saved_search{'Id'},
     );
-    RT::Interface::Web::Redirect(RT->Config->Get('WebURL') . 'Search/Results.html?' . $redir_query_string);
+    RT::Interface::Web::Redirect("$ResultPage?$redir_query_string");
     $m->abort;
 }
 
@@ -374,4 +377,5 @@ $NewQuery => 0
 $Class => 'RT::Tickets'
 $ObjectType => 'RT::Ticket'
 @ExtraQueryParams => ()
+$ResultPage => RT->Config->Get('WebPath') . '/Search/Results.html'
 </%ARGS>
diff --git a/share/html/Search/Edit.html b/share/html/Search/Edit.html
index afaa6d8b89..d3ca398616 100644
--- a/share/html/Search/Edit.html
+++ b/share/html/Search/Edit.html
@@ -55,6 +55,9 @@
 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $SavedChartSearchId %>" />
 <input type="hidden" class="hidden" name="Class" value="<% $Class %>" />
 <input type="hidden" class="hidden" name="ObjectType" value="<% $ObjectType %>" />
+% if ( $ResultPage ) {
+<input type="hidden" class="hidden" name="ResultPage" value="<% $ResultPage %>" />
+% }
 % for my $input ( @ExtraQueryParams ) {
 <input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
 %   if ( defined $ARGS{$input} ) {
@@ -111,4 +114,5 @@ $ObjectType    => 'RT::Ticket'
 
 @actions       => ()
 @ExtraQueryParams => ()
+$ResultPage    => RT->Config->Get('WebPath') . '/Search/Results.html'
 </%ARGS>

commit 9a7e3e2dbc51b5f82abe69a241b7431667908e81
Merge: 34aae6b5e8 6dd0e50ac5
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 6 10:50:06 2020 -0400

    Merge branch '5.0/customize-search' into 5.0-trunk

diff --cc lib/RT/Interface/Web/MenuBuilder.pm
index cd5e5437c7,f6d1567f08..c8e58daeed
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@@ -670,12 -579,11 +684,14 @@@ sub BuildMainNav 
  
          if ( $has_query ) {
              if ( $class eq 'RT::Tickets' ) {
-                 $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" );
+                 if ( $current_user->HasRight( Right => 'ShowSearchBulkUpdate', Object => RT->System ) ) {
+                     $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Search/Bulk.html$args" );
+                 }
                  $current_search_menu->child( chart => title => loc('Chart'),       path => "/Search/Chart.html$args" );
              }
 +            elsif ( $class eq 'RT::Assets' ) {
 +                $current_search_menu->child( bulk  => title => loc('Bulk Update'), path => "/Asset/Search/Bulk.html$args" );
 +            }
  
              my $more = $current_search_menu->child( more => title => loc('Feeds') );
  
diff --cc share/html/Search/Build.html
index 30233f4d8b,2b27f83c98..cc45c7e0b7
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@@ -369,5 -375,7 +386,7 @@@ $m->callback( ARGSRef => \%ARGS, Query 
  $NewQuery => 0
  @clauses => ()
  $Class => 'RT::Tickets'
 -$ObjectType => 'RT::Ticket'
 +$ObjectType => $Class eq 'RT::Transactions' ? 'RT::Ticket' : ''
+ @ExtraQueryParams => ()
+ $ResultPage => RT->Config->Get('WebPath') . '/Search/Results.html'
  </%ARGS>
diff --cc share/html/Search/Edit.html
index 01d8c42c83,d3ca398616..e10992e501
--- a/share/html/Search/Edit.html
+++ b/share/html/Search/Edit.html
@@@ -99,10 -107,12 +110,12 @@@ $SavedChartSearchId => 'new
  $Query         => ''
  $Format        => ''
  $Rows          => '50'
 -$OrderBy       => 'id'
  $Order         => 'ASC'
  $Class         => 'RT::Tickets'
 -$ObjectType    => 'RT::Ticket'
 +$OrderBy       => $Class eq 'RT::Assets' ? 'Name' : 'id'
 +$ObjectType    => $Class eq 'RT::Transactions' ? 'RT::Ticket' : ''
  
  @actions       => ()
+ @ExtraQueryParams => ()
+ $ResultPage    => RT->Config->Get('WebPath') . '/Search/Results.html'
  </%ARGS>
diff --cc share/html/Search/Results.html
index 23e605eb19,a620591ba4..9bdf3dac3b
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@@ -86,9 -87,17 +87,17 @@@
  % foreach my $key (keys(%hiddens)) {
  <input type="hidden" class="hidden" name="<%$key%>" value="<% defined($hiddens{$key})?$hiddens{$key}:'' %>" />
  % }
+ 
+ % for my $input ( @ExtraQueryParams ) {
+ <input type="hidden" class="hidden" name="ExtraQueryParams" value="<% $input %>" />
+ %   if ( defined $ARGS{$input} ) {
+ <input type="hidden" class="hidden" name="<% $input %>" value="<% $ARGS{$input} %>" />
+ %   }
+ % }
+ 
  <div class="form-row">
    <div class="col-auto">
 -    <& /Elements/Refresh, Name => 'TicketsRefreshInterval', Default => $session{$interval_name}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &>
 +    <& /Elements/Refresh, Name => 'SearchResultsRefreshInterval', Default => $session{$interval_name}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &>
    </div>
    <div class="col-auto">
      <input type="submit" class="button btn btn-primary form-control" value="<&|/l&>Change</&>" />
@@@ -282,5 -285,6 +291,6 @@@ $Order => unde
  $SavedSearchId => undef
  $SavedChartSearchId => undef
  $Class => 'RT::Tickets'
 -$ObjectType => 'RT::Ticket'
 +$ObjectType => $Class eq 'RT::Transactions' ? 'RT::Ticket' : ''
+ @ExtraQueryParams => ()
  </%ARGS>

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


More information about the rt-commit mailing list