[Rt-commit] rt branch, 5.0/custom-date-ranges-config-in-web-ui, created. rt-4.4.4-728-g85a343514

Michel Rodriguez michel at bestpractical.com
Wed Mar 18 07:18:26 EDT 2020


The branch, 5.0/custom-date-ranges-config-in-web-ui has been created
        at  85a3435142e3b0216f32f6309e0a1767dc85e432 (commit)

- Log -----------------------------------------------------------------
commit b0533ab24aebc779d5cfb1e805dd6bfd84336608
Author: michel <michel at bestpractical.com>
Date:   Tue Feb 4 17:34:01 2020 +0100

    Hide the [Custom Data Ranges] menu entry for users who don't have the rights.

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 41b6b4c2c..56d525fe1 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -547,7 +547,8 @@ sub BuildMainNav {
         $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';
+            title => loc('Custom Date Ranges'), path => "/Search/CustomDateRanges.html" )
+                if $class eq 'RT::Tickets' && $current_user->HasRight( Object=> RT->System, Right => 'SuperUser');
         if ($has_query) {
             $current_search_menu->child( results => title => loc('Show Results'), path => "/Search/Results.html$args" );
         }

commit 4c8d1768abaae51284135ed2810bde0074ed208b
Author: michel <michel at bestpractical.com>
Date:   Wed Feb 5 09:47:58 2020 +0100

    Use bootstrap for custom date ranges edit form.

diff --git a/share/html/Search/CustomDateRanges.html b/share/html/Search/CustomDateRanges.html
index 0eef09ba1..f8e9e51e6 100644
--- a/share/html/Search/CustomDateRanges.html
+++ b/share/html/Search/CustomDateRanges.html
@@ -84,61 +84,61 @@
 
 <form name="CustomDateRanges" method="POST" method="?">
   <&|/Widgets/TitleBox, title => loc('Custom Date Ranges') &>
-    <table class="collection-as-table">
-      <tr class="collection-as-table">
-        <th class="collection-as-table"><&|/l&>Name</&></th>
-        <th class="collection-as-table"><&|/l&>From</&></th>
-        <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
-        <th class="collection-as-table"><&|/l&>To</&></th>
-        <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-        <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
-        <th class="collection-as-table">
-          <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
-          <&|/l&>Delete</&>
-        </th>
-      </tr>
+  <div id="custom-date-ranges">
+    <div class="form-row input-row-header">
+      <div class="col-md-2"><&|/l&>Name</&></div>
+      <div class="col-md-2"><&|/l&>From</&></div>
+      <div class="col-md-2"><&|/l&>From Value if Unset</&></div>
+      <div class="col-md-2"><&|/l&>To</&></div>
+      <div class="col-md-2"><&|/l&>To Value if Unset</&></div>
+      <div class="col-md-1"><&|/l&>Business<br>Hours?</&></div>
+      <div class="col-md-1">
+        <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
+        <&|/l&>Delete</&>
+      </div>
+    </div>
 % my $i = 0;
 % if ( $content ) {
 % my $id = 0;
 %   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
 % $i++;
-      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
-        <td class="collection-as-table"><input type="text" name="<% $id %>-name" value="<% $name %>" /></td>
+      <div class="form-row input-row">
+        <div class="col-md-2"><input type="text" size="15" name="<% $id %>-name" value="<% $name %>" /></div>
 %       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></td>
-        <td class="collection-as-table">
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></div>
+        <div class="col-md-1">
           <select name="<% $id %>-business_time">
             <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
             <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
           </select>
-        </td>
-        <td class="collection-as-table"><input type="checkbox" name="<% $id %>-Delete" value="1" /></td>
-      </tr>
+        </div>
+        <div class="col-md-1"><input type="checkbox" name="<% $id %>-Delete" value="1" /></div>
+      </div>
 %     $id++;
 %   }
 % }
 
 % for ( 1 .. 3 ) {
 % $i++;
-      <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
-        <td class="collection-as-table"><input type="text" name="name" value="" /></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></td>
-        <td class="collection-as-table"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></td>
-        <td class="collection-as-table">
+      <div class="form-row input-row">
+        <div class="col-md-2"><input type="text" size="15" name="name" value="" /></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></div>
+        <div class="col-md-1">
           <select name="business_time">
             <option value="1"><&|/l&>Yes</&></option>
             <option value="0" selected="selected"><&|/l&>No</&></option>
           </select>
-        </td>
-        <td class="collection-as-table"></td>
-      </tr>
+        </div>
+        <div class="col-md-1"></div>
+      </div>
 % }
-    </table>
+    </div>
     <& /Elements/Submit, Name => 'Save', Label => loc('Save Changes') &>
   </&>
 </form>
diff --git a/share/static/css/elevator-light/forms.css b/share/static/css/elevator-light/forms.css
index e422e40ba..d9720afb3 100644
--- a/share/static/css/elevator-light/forms.css
+++ b/share/static/css/elevator-light/forms.css
@@ -54,6 +54,7 @@ div.results .titlebox-title .left {
   border-width: 1px 1px 0 1px;
 }
 
+div.input-row-header div,
 div.results .titlebox,
 div.error-titlebox {
   border: none;
@@ -61,6 +62,10 @@ div.error-titlebox {
   font-weight: bold;
 }
 
+div.input-row-header div input {
+    vertical-align: middle;
+}
+
 div.results .titlebox-content {
   border: 1px solid #aa9;
   border-bottom: 2px solid #990;

commit 70a2c2bfd4560abf28d0af074ab464ae76f23167
Author: michel <michel at bestpractical.com>
Date:   Tue Feb 4 17:28:33 2020 +0100

    Create option CustomDateRangesUI.
    
    CustomDateRangesUI allows creation of custom date ranges through the
    configuration UI.
    
    The option uses a custom widget and some %ARGS massaging to turn
    the fields from the custom date ranges form into a single hash
    that can then be processed like the other options.

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 7d85eea92..b94ff97c1 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -4541,6 +4541,10 @@ user.
 
 =back
 
+=item C<%CustomDateRangesUI>
+
+Custom Date Ranges can also be created through the Web GUI.
+
 =back
 
 =cut
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 9a71a48b8..ac47babbf 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1190,6 +1190,8 @@ our %META;
     },
     CustomDateRanges => {
         Type            => 'HASH',
+        Overridable => 0,
+        Hidden => 1,
         PostLoadCheck   => sub {
             my $config = shift;
             # use scalar context intentionally to avoid not a hash error
@@ -1219,6 +1221,10 @@ our %META;
             }
         },
     },
+    CustomDateRangesUI => {
+        Type            => 'HASH',
+        Widget => '/Widgets/Form/CustomDateRange',
+    },
     ExternalStorage => {
         Type            => 'HASH',
         PostLoadCheck   => sub {
@@ -2488,6 +2494,128 @@ sub EnableExternalAuth {
     return;
 }
 
+sub BuildCustomDateRangesUI {
+    my $self = shift;
+    my $args = shift;
+
+    my $config = $self->Get( 'CustomDateRanges' );
+    my $content = $self->Get( 'CustomDateRangesUI' );
+    my @results;
+
+    my %label = (
+        from => 'From', # loc
+        to => 'To', # loc
+        from_fallback => 'From Value if Unset', # loc
+        to_fallback => 'To Value if Unset', # loc
+    );
+
+    my $ok = 1;
+    my $need_save;
+    if ($content) {
+        my @current_names = sort keys %{ $content->{'RT::Ticket'} };
+        for my $id ( 0 .. $#current_names ) {
+            my $current_name = $current_names[$id];
+            my $spec         = $content->{'RT::Ticket'}{$current_name};
+            my $name         = $args->{"$id-name"};
+
+            if ( $config && $config->{'RT::Ticket'}{$name} ) {
+                push @results, loc( "[_1] already exists", $name );
+                $ok = 0;
+                next;
+            }
+
+            if ( $args->{"$id-Delete"} ) {
+                delete $content->{'RT::Ticket'}{$current_name};
+                push @results, loc( 'Deleted [_1]', $current_name );
+                $need_save ||= 1;
+                next;
+            }
+
+            my $updated;
+            for my $field (qw/from from_fallback to to_fallback/) {
+                next if ( $spec->{$field} // '' ) eq $args->{"$id-$field"};
+                if ((   $args->{"$id-$field"}
+                        && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $args->{"$id-$field"} )
+                    )
+                    || ( !$args->{"$id-$field"} && $field =~ /fallback/ )
+                  )
+                {
+                    $spec->{$field} = $args->{"$id-$field"};
+                    $updated ||= 1;
+                }
+                else {
+                    push @results, loc( 'Invalid [_1] for [_2]', loc( $label{$field} ), $name );
+                    $ok = 0;
+                    next;
+                }
+            }
+
+            if ( $spec->{business_time} != $args->{"$id-business_time"} ) {
+                $spec->{business_time} = $args->{"$id-business_time"};
+                $updated ||= 1;
+            }
+
+            $content->{'RT::Ticket'}{$name} = $spec;
+            if ( $name ne $current_name ) {
+                delete $content->{'RT::Ticket'}{$current_name};
+                $updated   ||= 1;
+            }
+
+            if ( $updated ) {
+                push @results, loc( 'Updated [_1]', $name );
+                $need_save ||= 1;
+            }
+        }
+    }
+
+    if ( $args->{name} ) {
+        for my $field (qw/from from_fallback to to_fallback business_time/) {
+            $args->{$field} = [ $args->{$field} ] unless ref $args->{$field};
+        }
+
+        my $i = 0;
+        for my $name ( @{ $args->{name} } ) {
+            if ($name) {
+                if ( $config && $config->{'RT::Ticket'}{$name} || $content && $content->{'RT::Ticket'}{$name} ) {
+                    push @results, loc( "[_1] already exists", $name );
+                    $ok = 0;
+                    $i++;
+                    next;
+                }
+            }
+            else {
+                $i++;
+                next;
+            }
+
+            my $spec = { business_time => $args->{business_time}[$i] };
+            for my $field ( qw/from from_fallback to to_fallback/ ) {
+                if ( ($args->{$field}[$i] && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $args->{$field}[$i] ))
+                    || ( !$args->{$field}[$i] && $field =~ /fallback/  )
+                ) {
+                    $spec->{$field} = $args->{$field}[$i];
+                }
+                else {
+                    push @results, loc( 'Invalid [_1] for [_2]', loc($field), $name );
+                    $ok = 0;
+                    $i++;
+                    next;
+                }
+            }
+
+            $content->{'RT::Ticket'}{$name} = $spec;
+            push @results, loc( 'Created [_1]', $name );
+            $need_save ||= 1;
+            $i++;
+        }
+    }
+
+    $args->{CustomDateRangesUI}= $content;
+    return ( $ok, \@results);
+}
+
+sub loc { return RT->SystemUser->loc( @_ ) }
+
 my $database_config_cache_time = 0;
 my %original_setting_from_files;
 my $in_config_change_txn = 0;
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 4f3004df4..6fea220fb 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2651,11 +2651,9 @@ sub CustomDateRanges {
         %ranges = %{ $config->{$type} } if $config->{$type};
     }
 
-    if ( my $attribute = RT->System->FirstAttribute('CustomDateRanges') ) {
-        if ( my $content = $attribute->Content ) {
-            for my $name ( keys %{ $content->{$type} || {} } ) {
-                $ranges{$name} ||= $content->{$type}{$name};
-            }
+    if ( my $configui =  RT->Config->Get('CustomDateRangesUI') ) {
+        for my $name ( keys %{ $configui->{$type} || {} } ) {
+            $ranges{$name} ||= $configui->{$type}{$name};
         }
     }
     return %ranges;
diff --git a/share/html/Admin/Tools/Config/Elements/Option b/share/html/Admin/Tools/Config/Elements/Option
index 41617d0b4..dc5dbe976 100644
--- a/share/html/Admin/Tools/Config/Elements/Option
+++ b/share/html/Admin/Tools/Config/Elements/Option
@@ -66,7 +66,7 @@ $doc_version =~ s/\.\d+-\d+-g\w+$//;  # 4.4.3-1-g123 -> 4.4
 
 my $name = $option->{Name};
 my $meta = RT->Config->Meta( $name );
-return if $meta->{Invisible} || $meta->{Deprecated};
+return if $meta->{Invisible} || $meta->{Deprecated} || $meta->{Hidden};
 return if $name =~ /Password/i && $name !~ /MinimumPasswordLength/;
 
 my $has_execute_code = $session{CurrentUser}->HasRight(Right => 'ExecuteCode', Object => RT->System);
diff --git a/share/html/Admin/Tools/Configuration.html b/share/html/Admin/Tools/Configuration.html
index 1cc1ed789..0b7a37cd3 100644
--- a/share/html/Admin/Tools/Configuration.html
+++ b/share/html/Admin/Tools/Configuration.html
@@ -85,7 +85,7 @@ foreach my $key ( RT->Config->Options( Overridable => undef, Sorted => 0 ) ) {
     }
     else {
         $description = loc("core config");
-    }
+    } 
     $index_conf++;
 </%PERL>
   <div class="form-row <% $index_conf%2 ? 'oddline' : 'evenline'%>">
@@ -229,6 +229,7 @@ for my $type (qw/Tickets Queues Transactions Groups PrivilegedUsers Unprivileged
 % my $index_size = 0;
 % while ( my $attr = $attrs->Next ) {
 % next if $attr->Name eq 'UpgradeHistory';
+% my $meta = RT->Config->Meta( $attr->Name );
 <div class="<% $index_size%2 ? 'oddline' : 'evenline'%> form-row">
 % if ($attr->Name eq 'UserLogo') {
 %   my $content = $attr->Content;
diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index 603c46ce8..0078534dc 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -80,6 +80,12 @@ if (delete $ARGS{Update}) {
     $RT::Handle->BeginTransaction;
     my $has_error;
 
+    if( exists $ARGS{CustomDateRangeUI} ) {
+        my( $ok, $results ) = RT::Config->BuildCustomDateRangesUI( \%ARGS );
+        if( ! $ok ) { $has_error++; }
+        push @results, @$results;
+    }
+
     eval {
         for my $key (keys %ARGS) {
             next if $key =~ /-Current$/;
@@ -91,6 +97,7 @@ if (delete $ARGS{Update}) {
 
             my $val = $ARGS{$key};
             $val = '' if $val eq '__empty_value__';
+            next if ! exists $ARGS{$key . '-Current'};;
             my $prev = $ARGS{$key . '-Current'};
             next if $val eq $prev;
 
@@ -216,4 +223,4 @@ my $nav_type='tab'; # 'tab' or 'pill'
 % }
   </div><!-- content-all -->
 </div><!-- titlebox-content -->
-</div><!-- configuration -->
\ No newline at end of file
+</div><!-- configuration -->
diff --git a/share/html/Widgets/Form/CustomDateRange b/share/html/Widgets/Form/CustomDateRange
new file mode 100644
index 000000000..d5112dab2
--- /dev/null
+++ b/share/html/Widgets/Form/CustomDateRange
@@ -0,0 +1,161 @@
+%# 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 }}}
+
+<&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files') &>
+% if ( $config && keys %{$config->{'RT::Ticket'}} ) {
+  <table class="collection-as-table">
+    <tr class="collection-as-table">
+      <th class="collection-as-table"><&|/l&>Name</&></th>
+      <th class="collection-as-table"><&|/l&>From</&></th>
+      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>To</&></th>
+      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
+      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
+    </tr>
+% my $i = 0;
+% for my $name ( sort keys %{$config->{'RT::Ticket'}} ) {
+% $i++;
+    <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
+      <td class="collection-as-table"><% $name %></td>
+%     my $spec = $config->{'RT::Ticket'}{$name};
+%     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
+      <td class="collection-as-table"><% $date_range_spec{from} %></td>
+      <td class="collection-as-table"><% $date_range_spec{from_fallback} || '' %></td>
+      <td class="collection-as-table"><% $date_range_spec{to} %></td>
+      <td class="collection-as-table"><% $date_range_spec{to_fallback} || '' %></td>
+      <td class="collection-as-table"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></td>
+    </tr>
+% }
+  </table>
+% }
+% else {
+  <p><&|/l&>No custom date ranges in config files</&></p>
+% }
+</&>
+
+<div id="custom-date-range-input">
+  <input id="CustomDateRangesUI" type="hidden" name="CustomDateRangeUI" value="" />
+  <input id="CustomDateRangesUI-updated" type="hidden" name="updated" value="0" />
+  <input type="hidden" name="CustomDateRangesUI-Current" value="<% $CurrentValue %>" />
+  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges') &>
+  <div id="custom-date-ranges">
+    <div class="form-row input-row-header">
+      <div class="col-md-2"><&|/l&>Name</&></div>
+      <div class="col-md-2"><&|/l&>From</&></div>
+      <div class="col-md-2"><&|/l&>From Value if Unset</&></div>
+      <div class="col-md-2"><&|/l&>To</&></div>
+      <div class="col-md-2"><&|/l&>To Value if Unset</&></div>
+      <div class="col-md-1"><&|/l&>Business<br>Hours?</&></div>
+      <div class="col-md-1">
+        <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
+        <&|/l&>Delete</&>
+      </div>
+    </div>
+% my $i = 0;
+% if ( $content ) {
+% my $id = 0;
+%   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
+% $i++;
+      <div class="form-row input-row">
+        <div class="col-md-2"><input type="text" size="15" name="<% $id %>-name" value="<% $name %>" /></div>
+%       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></div>
+        <div class="col-md-1">
+          <select name="<% $id %>-business_time">
+            <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
+            <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
+          </select>
+        </div>
+        <div class="col-md-1"><input type="checkbox" name="<% $id %>-Delete" value="1" /></div>
+      </div>
+%     $id++;
+%   }
+% }
+
+% for ( 1 .. 3 ) {
+% $i++;
+      <div class="form-row input-row">
+        <div class="col-md-2"><input type="text" size="15" name="name" value="" /></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></div>
+        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></div>
+        <div class="col-md-1">
+          <select name="business_time">
+            <option value="1"><&|/l&>Yes</&></option>
+            <option value="0" selected="selected"><&|/l&>No</&></option>
+          </select>
+        </div>
+        <div class="col-md-1"></div>
+      </div>
+% }
+    </div>
+  </&>
+</div>
+<%INIT>
+
+Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');
+
+my $config   = RT->Config->Get('CustomDateRanges');
+my $content = RT->Config->Get('CustomDateRangesUI');
+</%INIT>
+
+<%ARGS>
+$Save => $ARGS{updated};
+$Name
+
+$Arguments    => {},
+
+$Default      => 0,
+$DefaultValue => '',
+$CurrentValue => '',
+
+</%ARGS>

commit c7b5c8a6512d86f98f6d5399b8ca598cad035f5e
Author: michel <michel at bestpractical.com>
Date:   Thu Mar 12 14:52:53 2020 +0100

    Added Custom Date Ranges as a user preference.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index ac47babbf..f4b173974 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -56,6 +56,7 @@ use File::Spec ();
 use Symbol::Global::Name;
 use List::MoreUtils 'uniq';
 use Clone ();
+use Data::Dumper;
 
 # Store log messages generated before RT::Logger is available
 our @PreInitLoggerMessages;
@@ -1224,6 +1225,10 @@ our %META;
     CustomDateRangesUI => {
         Type            => 'HASH',
         Widget => '/Widgets/Form/CustomDateRange',
+        WidgetArguments => {},
+        Section => 'Custom date ranges',
+        SortOrder => 1,
+        Overridable     => 1,
     },
     ExternalStorage => {
         Type            => 'HASH',
@@ -2494,12 +2499,72 @@ sub EnableExternalAuth {
     return;
 }
 
+sub AllCustomDateRanges {
+    my ( $self, $include_users ) = @_;
+    my $config =  $self->Get( 'CustomDateRanges' );
+    my $cdr_origin = {};
+    _CustomDateRangesSpecOrigin( $config, 'config files', $cdr_origin );
+
+    if ( $include_users ) {
+        my $db_config = $self->Get( 'CustomDateRangesUI' );
+        _CustomDateRangesSpecOrigin( $db_config, 'database', $cdr_origin );
+        $config = _MergeCustomDateRangesSpecs( $config, $db_config );
+    }
+    return ( $config, $cdr_origin );
+}
+
+# stores the origin of the custom date ranges specs, for error reporting
+# $spec is a CDR config (from CustomDateRanges, CustomDateRangesUI or a user preferences)
+# $origin is a string that gets recorded
+# $cdr_origin is a hash to which the origin gets added
+sub _CustomDateRangesSpecOrigin {
+    my( $spec, $origin, $cdr_origin ) = @_;
+    return {} if ! CustomDateRangesSpecHasContent( $spec );
+    foreach my $type ( keys %$spec ) {
+        my $spec_type =  $spec->{$type};
+        next if ref( $spec->{$type} ) ne 'HASH';
+        foreach my $name ( keys %{$spec->{$type}} ) {
+            $cdr_origin->{$type}->{$name} = $origin;
+        }
+    }
+}
+
+# merge all custom date ranges definitions into one
+sub _MergeCustomDateRangesSpecs {
+    my @specs = @_;
+    my $merged = {};
+    foreach my $spec ( @specs ) {
+        foreach my $type ( keys %$spec ) {
+            my $spec_type =  $spec->{$type};
+            foreach my $name ( keys %$spec_type ) {
+                $merged->{$type}->{$name} ||= $spec_type->{$name};
+            }
+        }
+    }
+    return $merged;
+}
+
+# returns true if one of the categories of custom date ranges (RT::Ticket...)
+# has any content, false otherwise
+sub CustomDateRangesSpecHasContent {
+    my $spec = shift;
+    return if ref( $spec ) ne 'HASH';
+    foreach my $type ( keys %$spec ) {
+        my $spec_type =  $spec->{$type};
+        next if ref( $spec_type ) ne 'HASH';
+        return 1 if scalar keys %$spec_type;
+    }
+    return;
+}
+
 sub BuildCustomDateRangesUI {
     my $self = shift;
     my $args = shift;
 
-    my $config = $self->Get( 'CustomDateRanges' );
-    my $content = $self->Get( 'CustomDateRangesUI' );
+    my $user_config = $args->{UserConfig} || 0;
+
+    my( $config, $origin ) = $self->AllCustomDateRanges( $user_config );
+    my $current = $user_config ? $HTML::Mason::Commands::session{'CurrentUser'}->UserObj->Preferences("CustomDateRanges") : $self->Get( 'CustomDateRangesUI' );
     my @results;
 
     my %label = (
@@ -2511,21 +2576,22 @@ sub BuildCustomDateRangesUI {
 
     my $ok = 1;
     my $need_save;
-    if ($content) {
-        my @current_names = sort keys %{ $content->{'RT::Ticket'} };
+    if ($current) {
+        my @current_names = sort keys %{ $current->{'RT::Ticket'} };
         for my $id ( 0 .. $#current_names ) {
             my $current_name = $current_names[$id];
-            my $spec         = $content->{'RT::Ticket'}{$current_name};
+            my $spec         = $current->{'RT::Ticket'}{$current_name};
             my $name         = $args->{"$id-name"};
 
             if ( $config && $config->{'RT::Ticket'}{$name} ) {
-                push @results, loc( "[_1] already exists", $name );
+                my $defined_in = $origin->{'RT::Ticket'}->{$name};
+                push @results, loc( "[_1] already exists in [_2]", $name, $defined_in );
                 $ok = 0;
                 next;
             }
 
             if ( $args->{"$id-Delete"} ) {
-                delete $content->{'RT::Ticket'}{$current_name};
+                delete $current->{'RT::Ticket'}{$current_name};
                 push @results, loc( 'Deleted [_1]', $current_name );
                 $need_save ||= 1;
                 next;
@@ -2555,9 +2621,9 @@ sub BuildCustomDateRangesUI {
                 $updated ||= 1;
             }
 
-            $content->{'RT::Ticket'}{$name} = $spec;
+            $current->{'RT::Ticket'}{$name} = $spec;
             if ( $name ne $current_name ) {
-                delete $content->{'RT::Ticket'}{$current_name};
+                delete $current->{'RT::Ticket'}{$current_name};
                 $updated   ||= 1;
             }
 
@@ -2576,8 +2642,9 @@ sub BuildCustomDateRangesUI {
         my $i = 0;
         for my $name ( @{ $args->{name} } ) {
             if ($name) {
-                if ( $config && $config->{'RT::Ticket'}{$name} || $content && $content->{'RT::Ticket'}{$name} ) {
-                    push @results, loc( "[_1] already exists", $name );
+                if ( $config && $config->{'RT::Ticket'}{$name} || $current && $current->{'RT::Ticket'}{$name} ) {
+                    my $defined_in = $origin->{'RT::Ticket'}->{$name};
+                    push @results, loc( "[_1] already exists in [_2]", $name, $defined_in );
                     $ok = 0;
                     $i++;
                     next;
@@ -2603,19 +2670,31 @@ sub BuildCustomDateRangesUI {
                 }
             }
 
-            $content->{'RT::Ticket'}{$name} = $spec;
+            $current->{'RT::Ticket'}{$name} = $spec;
             push @results, loc( 'Created [_1]', $name );
             $need_save ||= 1;
             $i++;
         }
     }
 
-    $args->{CustomDateRangesUI}= $content;
+    $args->{CustomDateRangesUI}= $current;
     return ( $ok, \@results);
 }
 
 sub loc { return RT->SystemUser->loc( @_ ) }
 
+sub Stringify {
+    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;
+};
+
 my $database_config_cache_time = 0;
 my %original_setting_from_files;
 my $in_config_change_txn = 0;
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 56d525fe1..a63425252 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -308,6 +308,12 @@ sub BuildMainNav {
         }
     }
 
+    if( $request_path  eq '/Prefs/SearchOptions.html' ) {
+        $page->child( custom_date_ranges => title => loc('Custom Date Ranges'), path => "/Prefs/CustomDateRanges.html" );
+    }
+    if( $request_path  eq '/Prefs/CustomDateRanges.html' ) {
+        $page->child( custom_date_ranges => title => loc('Search Preferences'), path => "/Prefs/SearchOptions.html" );
+    }
 
     if ( $request_path =~ m{^/Ticket/} ) {
         if ( ( $HTML::Mason::Commands::DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
diff --git a/share/html/Admin/Tools/Config/Elements/Option b/share/html/Admin/Tools/Config/Elements/Option
index dc5dbe976..cd06d150d 100644
--- a/share/html/Admin/Tools/Config/Elements/Option
+++ b/share/html/Admin/Tools/Config/Elements/Option
@@ -82,7 +82,7 @@ my $is_immutable = $meta->{Immutable}
                 || ($is_code && $val =~ s/sub \{ "DUMMY" \}/sub { ... }/g)
                 || ($is_code && !$has_execute_code);
 
-my $current_value = $is_code ? $val : $raw_value;
+my $current_value = $is_code || $widget eq '/Widgets/Form/CustomDateRange' ? $val : $raw_value;
 my $args   = $meta->{'WidgetArguments'} || {};
 if ($widget eq '/Widgets/Form/Boolean') {
     %$args = (
diff --git a/share/html/Elements/CustomDateRangesSpecs b/share/html/Elements/CustomDateRangesSpecs
new file mode 100644
index 000000000..6f810588c
--- /dev/null
+++ b/share/html/Elements/CustomDateRangesSpecs
@@ -0,0 +1,92 @@
+%# 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 }}}
+
+<&|/Widgets/TitleBox, title => loc( "Custom date ranges in [_1]", $Location) &>
+% if ( $Config && keys %{$Config->{'RT::Ticket'}} ) {
+  <div class="">
+    <div class="form-row input-row-header">
+      <div class="col-md-2"><&|/l&>Name</&></div>
+      <div class="col-md-2"><&|/l&>From</&></div>
+      <div class="col-md-2"><&|/l&>From Value if Unset</&></div>
+      <div class="col-md-2"><&|/l&>To</&></div>
+      <div class="col-md-2"><&|/l&>To Value if Unset</&></div>
+      <div class="col-md-1"><&|/l&>Business<br>Hours?</&></div>
+    </div>
+% my $i = 0;
+% for my $name ( sort keys %{$Config->{'RT::Ticket'}} ) {
+% $i++;
+    <div class="<% $i % 2 ? 'oddline' : 'evenline' %> form-row input-row">
+      <div class="col-md-2"><% $name %></div>
+%     my $spec = $Config->{'RT::Ticket'}{$name};
+%     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
+      <div class="col-md-2"><% $date_range_spec{from} %></div>
+      <div class="col-md-2"><% $date_range_spec{from_fallback} || '' %></div>
+      <div class="col-md-2"><% $date_range_spec{to} %></div>
+      <div class="col-md-2"><% $date_range_spec{to_fallback} || '' %></div>
+      <div class="col-md-1"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></div>
+      <div class="col-md-1 evenline" style="background-color:white;"></div>
+    </div>
+% }
+  </div>
+% }
+% else {
+  <p><&|/l&>No custom date ranges in <% $Location %></&></p>
+% }
+</&>
+
+<%INIT>
+
+Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser')
+    || $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'ModifySelf' );
+
+</%INIT>
+
+<%ARGS>
+$Location
+$Config
+</%ARGS>
diff --git a/share/html/Prefs/CustomDateRanges.html b/share/html/Prefs/CustomDateRanges.html
new file mode 100644
index 000000000..5de2526d8
--- /dev/null
+++ b/share/html/Prefs/CustomDateRanges.html
@@ -0,0 +1,80 @@
+%# 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 }}}
+<& /Elements/Header, Title => loc("Custom Date Ranges") &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@actions &>
+
+<form method="post" action="CustomDateRanges.html">
+    <& /Widgets/Form/CustomDateRange, UserPreferences => 1, %ARGS &>
+    <input type="hidden" name="UserConfig" value="1" />
+<& /Elements/Submit, Name => 'SavePreferences', Label => loc('Save Changes') &>
+</form>
+<form>
+<%INIT>
+my @actions;
+
+if( $ARGS{SavePreferences} ) {
+    my( $ok, $results) = RT::Config->BuildCustomDateRangesUI( \%ARGS );
+    push @actions, @$results;
+    if( $ok ) {
+        my $CustomDateRangesSpec = $ARGS{CustomDateRangesUI};
+        if( values %{$CustomDateRangesSpec->{'RT::Ticket'}} ) {
+            my ($set_ok, $msg) = $session{'CurrentUser'}->UserObj->SetPreferences("CustomDateRanges", $CustomDateRangesSpec );
+            push @actions, $set_ok ? loc("Custom date ranges saved.") : $msg;
+        }
+        else {
+            my ($delete_ok, $msg) = $session{'CurrentUser'}->UserObj->DeletePreferences("CustomDateRanges");
+            push @actions, $delete_ok ? loc("Custom date ranges deleted.") : $msg;
+        }
+    }
+}
+
+</%INIT>
+
+<%ARGS>
+</%ARGS>
diff --git a/share/html/Prefs/Other.html b/share/html/Prefs/Other.html
index b1fb9a539..48e15eef0 100644
--- a/share/html/Prefs/Other.html
+++ b/share/html/Prefs/Other.html
@@ -54,6 +54,7 @@
 <&|/Widgets/TitleBox, title => loc( $section ) &>
 % foreach my $option( RT->Config->Options( Section => $section ) ) {
 % next if $option eq 'EmailFrequency' && !RT->Config->Get('RecordOutgoingEmail');
+% next if $option eq 'CustomDateRangesUI';
 % my $meta = RT->Config->Meta( $option );
 <& $meta->{'Widget'},
     Default      => 1,
@@ -98,7 +99,7 @@ if ( $Update ) {
             },
             Store => $preferences,
             Types => [RT->Config->Options], Default => 1, Arguments => \%ARGS,
-            DefaultValue => { map { $_ => RT->Config->Get($_) }
+            DefaultValue => { map { $_ => RT->Config->Get($_) // '' }
                 RT->Config->Options
             }, );
 
diff --git a/share/html/Widgets/Form/CustomDateRange b/share/html/Widgets/Form/CustomDateRange
index d5112dab2..4918e83b4 100644
--- a/share/html/Widgets/Form/CustomDateRange
+++ b/share/html/Widgets/Form/CustomDateRange
@@ -46,116 +46,33 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 
-<&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files') &>
-% if ( $config && keys %{$config->{'RT::Ticket'}} ) {
-  <table class="collection-as-table">
-    <tr class="collection-as-table">
-      <th class="collection-as-table"><&|/l&>Name</&></th>
-      <th class="collection-as-table"><&|/l&>From</&></th>
-      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>To</&></th>
-      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
-    </tr>
-% my $i = 0;
-% for my $name ( sort keys %{$config->{'RT::Ticket'}} ) {
-% $i++;
-    <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
-      <td class="collection-as-table"><% $name %></td>
-%     my $spec = $config->{'RT::Ticket'}{$name};
-%     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
-      <td class="collection-as-table"><% $date_range_spec{from} %></td>
-      <td class="collection-as-table"><% $date_range_spec{from_fallback} || '' %></td>
-      <td class="collection-as-table"><% $date_range_spec{to} %></td>
-      <td class="collection-as-table"><% $date_range_spec{to_fallback} || '' %></td>
-      <td class="collection-as-table"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></td>
-    </tr>
-% }
-  </table>
+<& /Elements/CustomDateRangesSpecs, Location => "config files", Config => $from_config  &>
+% if ( $UserPreferences ) {
+    <& /Elements/CustomDateRangesSpecs, Location => "database", Config => $from_db &>
+    <& /Widgets/Form/CustomDateRangesInput, Location => "user preferences", Config => $from_user &>
 % }
 % else {
-  <p><&|/l&>No custom date ranges in config files</&></p>
+    <& /Widgets/Form/CustomDateRangesInput, Location => "database", Config => $from_db &>
 % }
-</&>
+<%INIT>
 
-<div id="custom-date-range-input">
-  <input id="CustomDateRangesUI" type="hidden" name="CustomDateRangeUI" value="" />
-  <input id="CustomDateRangesUI-updated" type="hidden" name="updated" value="0" />
-  <input type="hidden" name="CustomDateRangesUI-Current" value="<% $CurrentValue %>" />
-  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges') &>
-  <div id="custom-date-ranges">
-    <div class="form-row input-row-header">
-      <div class="col-md-2"><&|/l&>Name</&></div>
-      <div class="col-md-2"><&|/l&>From</&></div>
-      <div class="col-md-2"><&|/l&>From Value if Unset</&></div>
-      <div class="col-md-2"><&|/l&>To</&></div>
-      <div class="col-md-2"><&|/l&>To Value if Unset</&></div>
-      <div class="col-md-1"><&|/l&>Business<br>Hours?</&></div>
-      <div class="col-md-1">
-        <input type="checkbox" name="DeleteAll" value="1" onclick="setCheckbox(this, /^\d+-Delete$/)" />
-        <&|/l&>Delete</&>
-      </div>
-    </div>
-% my $i = 0;
-% if ( $content ) {
-% my $id = 0;
-%   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
-% $i++;
-      <div class="form-row input-row">
-        <div class="col-md-2"><input type="text" size="15" name="<% $id %>-name" value="<% $name %>" /></div>
-%       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to_fallback", Default => $date_range_spec{to_fallback} &></div>
-        <div class="col-md-1">
-          <select name="<% $id %>-business_time">
-            <option value="1" <% $date_range_spec{business_time} ? 'selected="selected"' : '' |n%>><&|/l&>Yes</&></option>
-            <option value="0" <% $date_range_spec{business_time} ? '': 'selected="selected"' |n%>><&|/l&>No</&></option>
-          </select>
-        </div>
-        <div class="col-md-1"><input type="checkbox" name="<% $id %>-Delete" value="1" /></div>
-      </div>
-%     $id++;
-%   }
-% }
+Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser')
+    || ( $UserPreferences && $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'ModifySelf' ) );
 
-% for ( 1 .. 3 ) {
-% $i++;
-      <div class="form-row input-row">
-        <div class="col-md-2"><input type="text" size="15" name="name" value="" /></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'from' &></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'from_fallback' &></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'to' &></div>
-        <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => 'to_fallback' &></div>
-        <div class="col-md-1">
-          <select name="business_time">
-            <option value="1"><&|/l&>Yes</&></option>
-            <option value="0" selected="selected"><&|/l&>No</&></option>
-          </select>
-        </div>
-        <div class="col-md-1"></div>
-      </div>
-% }
-    </div>
-  </&>
-</div>
-<%INIT>
+my $from_config   = RT->Config->Get('CustomDateRanges');
+my $from_db       = RT->Config->Get('CustomDateRangesUI');
 
-Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');
+my $from_user     = $UserPreferences ? $session{'CurrentUser'}->UserObj->Preferences("CustomDateRanges" ) : undef;
 
-my $config   = RT->Config->Get('CustomDateRanges');
-my $content = RT->Config->Get('CustomDateRangesUI');
 </%INIT>
-
 <%ARGS>
-$Save => $ARGS{updated};
-$Name
+$Save => $ARGS{updated},
 
 $Arguments    => {},
 
 $Default      => 0,
 $DefaultValue => '',
 $CurrentValue => '',
+$UserPreferences => 0,
 
 </%ARGS>
diff --git a/share/html/Widgets/Form/CustomDateRange b/share/html/Widgets/Form/CustomDateRangesInput
similarity index 71%
copy from share/html/Widgets/Form/CustomDateRange
copy to share/html/Widgets/Form/CustomDateRangesInput
index d5112dab2..5301642ed 100644
--- a/share/html/Widgets/Form/CustomDateRange
+++ b/share/html/Widgets/Form/CustomDateRangesInput
@@ -46,43 +46,11 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 
-<&|/Widgets/TitleBox, title => loc('Custom Date Ranges In Config Files') &>
-% if ( $config && keys %{$config->{'RT::Ticket'}} ) {
-  <table class="collection-as-table">
-    <tr class="collection-as-table">
-      <th class="collection-as-table"><&|/l&>Name</&></th>
-      <th class="collection-as-table"><&|/l&>From</&></th>
-      <th class="collection-as-table"><&|/l&>From Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>To</&></th>
-      <th class="collection-as-table"><&|/l&>To Value if Unset</&></th>
-      <th class="collection-as-table"><&|/l&>Business<br>Hours?</&></th>
-    </tr>
-% my $i = 0;
-% for my $name ( sort keys %{$config->{'RT::Ticket'}} ) {
-% $i++;
-    <tr class="<% $i % 2 ? 'oddline' : 'evenline' %>">
-      <td class="collection-as-table"><% $name %></td>
-%     my $spec = $config->{'RT::Ticket'}{$name};
-%     my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $spec);
-      <td class="collection-as-table"><% $date_range_spec{from} %></td>
-      <td class="collection-as-table"><% $date_range_spec{from_fallback} || '' %></td>
-      <td class="collection-as-table"><% $date_range_spec{to} %></td>
-      <td class="collection-as-table"><% $date_range_spec{to_fallback} || '' %></td>
-      <td class="collection-as-table"><% $date_range_spec{business_time} ? loc('Yes') : loc('No') %></td>
-    </tr>
-% }
-  </table>
-% }
-% else {
-  <p><&|/l&>No custom date ranges in config files</&></p>
-% }
-</&>
-
 <div id="custom-date-range-input">
   <input id="CustomDateRangesUI" type="hidden" name="CustomDateRangeUI" value="" />
   <input id="CustomDateRangesUI-updated" type="hidden" name="updated" value="0" />
-  <input type="hidden" name="CustomDateRangesUI-Current" value="<% $CurrentValue %>" />
-  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges') &>
+  <input type="hidden" name="CustomDateRangesUI-Current" value="<% RT::Config::Stringify( $Config ) %>" />
+  <&|/Widgets/TitleBox, title => loc('Custom Date Ranges in [_1]', $Location ) &>
   <div id="custom-date-ranges">
     <div class="form-row input-row-header">
       <div class="col-md-2"><&|/l&>Name</&></div>
@@ -97,13 +65,13 @@
       </div>
     </div>
 % my $i = 0;
-% if ( $content ) {
+% if ( $Config ) {
 % my $id = 0;
-%   for my $name ( sort keys %{$content->{'RT::Ticket'}} ) {
+%   for my $name ( sort keys %{$Config->{'RT::Ticket'}} ) {
 % $i++;
       <div class="form-row input-row">
         <div class="col-md-2"><input type="text" size="15" name="<% $id %>-name" value="<% $name %>" /></div>
-%       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $content->{'RT::Ticket'}{$name});
+%       my %date_range_spec = RT::Ticket->_ParseCustomDateRangeSpec($name, $Config->{'RT::Ticket'}{$name});
         <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from", Default => $date_range_spec{from} &></div>
         <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-from_fallback", Default => $date_range_spec{from_fallback} &></div>
         <div class="col-md-2"><& /Elements/SelectCustomDateRangeField, Name => "$id-to", Default => $date_range_spec{to} &></div>
@@ -140,22 +108,8 @@
     </div>
   </&>
 </div>
-<%INIT>
-
-Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object=> RT->System, Right => 'SuperUser');
-
-my $config   = RT->Config->Get('CustomDateRanges');
-my $content = RT->Config->Get('CustomDateRangesUI');
-</%INIT>
-
 <%ARGS>
-$Save => $ARGS{updated};
-$Name
-
-$Arguments    => {},
-
-$Default      => 0,
-$DefaultValue => '',
-$CurrentValue => '',
-
+$Location
+$Config
 </%ARGS>
+

commit cf92b7c36bb6b5258b769bb8cb39e3c553ab01c1
Author: michel <michel at bestpractical.com>
Date:   Thu Mar 12 19:46:26 2020 +0100

    Added display of other users custom date ranges in user setting page.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index f4b173974..761dd6a92 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -2508,7 +2508,7 @@ sub AllCustomDateRanges {
     if ( $include_users ) {
         my $db_config = $self->Get( 'CustomDateRangesUI' );
         _CustomDateRangesSpecOrigin( $db_config, 'database', $cdr_origin );
-        $config = _MergeCustomDateRangesSpecs( $config, $db_config );
+        $config = MergeCustomDateRangesSpecs( $config, $db_config );
     }
     return ( $config, $cdr_origin );
 }
@@ -2530,7 +2530,7 @@ sub _CustomDateRangesSpecOrigin {
 }
 
 # merge all custom date ranges definitions into one
-sub _MergeCustomDateRangesSpecs {
+sub MergeCustomDateRangesSpecs {
     my @specs = @_;
     my $merged = {};
     foreach my $spec ( @specs ) {
diff --git a/share/html/Widgets/Form/CustomDateRange b/share/html/Widgets/Form/CustomDateRange
index 4918e83b4..5b3a7ce18 100644
--- a/share/html/Widgets/Form/CustomDateRange
+++ b/share/html/Widgets/Form/CustomDateRange
@@ -48,8 +48,9 @@
 
 <& /Elements/CustomDateRangesSpecs, Location => "config files", Config => $from_config  &>
 % if ( $UserPreferences ) {
-    <& /Elements/CustomDateRangesSpecs, Location => "database", Config => $from_db &>
-    <& /Widgets/Form/CustomDateRangesInput, Location => "user preferences", Config => $from_user &>
+      <& /Elements/CustomDateRangesSpecs, Location => "database", Config => $from_db &>
+      <& /Elements/CustomDateRangesSpecs, Location => "other users settings", Config => $from_other_users &>
+      <& /Widgets/Form/CustomDateRangesInput, Location => "user preferences", Config => $from_user &>
 % }
 % else {
     <& /Widgets/Form/CustomDateRangesInput, Location => "database", Config => $from_db &>
@@ -62,7 +63,20 @@ Abort(loc("Permission Denied")) unless $session{'CurrentUser'}->HasRight( Object
 my $from_config   = RT->Config->Get('CustomDateRanges');
 my $from_db       = RT->Config->Get('CustomDateRangesUI');
 
-my $from_user     = $UserPreferences ? $session{'CurrentUser'}->UserObj->Preferences("CustomDateRanges" ) : undef;
+my ( $from_other_users, $from_user );
+if( $UserPreferences ) {
+    my $current_user_id = $session{'CurrentUser'}->Id;
+
+    # get the content of all Pref-CustomDateRanges attributes for other users
+    my $attributes = RT::Attributes->new( RT->SystemUser );
+    $attributes->Limit( FIELD => 'Name', VALUE => 'Pref-CustomDateRanges' );
+    $attributes->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $current_user_id );
+    $attributes->OrderBy( FIELD => 'Creator' );
+    my @from_other_users = map { $_->Content } @{$attributes->ItemsArrayRef};
+    $from_other_users = RT::Config::MergeCustomDateRangesSpecs( @from_other_users );
+
+    $from_user = $session{'CurrentUser'}->UserObj->Preferences("CustomDateRanges" );
+}
 
 </%INIT>
 <%ARGS>

commit b9679cdbf50bc4a47ab475f3424c851a5ccc4225
Author: michel <michel at bestpractical.com>
Date:   Mon Mar 16 22:11:55 2020 +0100

    Functioning version of Custom Date Ranges in UI.
    
    If a custom date range is already defined by an other user, doesn't
    display the name of the user.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 761dd6a92..c4b6b3fc0 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -2499,24 +2499,65 @@ sub EnableExternalAuth {
     return;
 }
 
+# Custom Date Ranges methods
+
+# return a single CDR config, plus a structure that gives the origin for each definition 
+# meaning of the $include_level parameter
+# 0 => return only the config file definitions (CustomDateRanges option)
+#          used when editing the definitions in the DB
+# 1 => return the config file + the global DB definitions (CustomDateRangesUI option)
+#          + definition from other users
+#          used when editing the definitions for a single user
+# 2 => return all definitions
+#          used when actually using the CDR
 sub AllCustomDateRanges {
-    my ( $self, $include_users ) = @_;
+    my ( $self, $include_level ) = @_;
     my $config =  $self->Get( 'CustomDateRanges' );
     my $cdr_origin = {};
     _CustomDateRangesSpecOrigin( $config, 'config files', $cdr_origin );
 
-    if ( $include_users ) {
+    if ( $include_level > 0 ) {
         my $db_config = $self->Get( 'CustomDateRangesUI' );
         _CustomDateRangesSpecOrigin( $db_config, 'database', $cdr_origin );
         $config = MergeCustomDateRangesSpecs( $config, $db_config );
+        my @from_other_users = $self->CustomDateRangesFromOtherUsers;
+        foreach my $cdr ( @from_other_users ) {
+            _CustomDateRangesSpecOrigin( $cdr, 'other user config', $cdr_origin );
+        }
+        $config = MergeCustomDateRangesSpecs( $config, $db_config,  @from_other_users );
+    }
+    if ( $include_level > 1 ) {
+        my $current_user = $HTML::Mason::Commands::session{'CurrentUser'} || RT->SystemUser;
+        my $user_cdr = $current_user->UserObj->Preferences("CustomDateRanges" );
+        if($user_cdr ) {
+            _CustomDateRangesSpecOrigin( $user_cdr, 'user', $cdr_origin );
+            $config = MergeCustomDateRangesSpecs( $config, $user_cdr );
+        }
     }
+
     return ( $config, $cdr_origin );
 }
 
+sub CustomDateRangesFromOtherUsers {
+
+    my $current_user = $HTML::Mason::Commands::session{'CurrentUser'} || RT->SystemUser;
+
+    my $attributes = RT::Attributes->new( RT->SystemUser );
+    $attributes->Limit( FIELD => 'Name', VALUE => 'Pref-CustomDateRanges' );
+    $attributes->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $current_user->Id );
+    $attributes->OrderBy( FIELD => 'Creator' );
+    my @from_other_users = map { $_->Content } @{$attributes->ItemsArrayRef};
+
+    return MergeCustomDateRangesSpecs( @from_other_users );
+}
+
 # stores the origin of the custom date ranges specs, for error reporting
 # $spec is a CDR config (from CustomDateRanges, CustomDateRangesUI or a user preferences)
 # $origin is a string that gets recorded
 # $cdr_origin is a hash to which the origin gets added
+# annotating the original $spec would be nicer, but a spec can be a simple string, as in 
+# 'Resolution Time' => 'Resolved - Created', 
+# which doesn't allow for the origin to be recorded
 sub _CustomDateRangesSpecOrigin {
     my( $spec, $origin, $cdr_origin ) = @_;
     return {} if ! CustomDateRangesSpecHasContent( $spec );
@@ -2582,6 +2623,7 @@ sub BuildCustomDateRangesUI {
             my $current_name = $current_names[$id];
             my $spec         = $current->{'RT::Ticket'}{$current_name};
             my $name         = $args->{"$id-name"};
+            next if ! $name;
 
             if ( $config && $config->{'RT::Ticket'}{$name} ) {
                 my $defined_in = $origin->{'RT::Ticket'}->{$name};
@@ -2599,7 +2641,7 @@ sub BuildCustomDateRangesUI {
 
             my $updated;
             for my $field (qw/from from_fallback to to_fallback/) {
-                next if ( $spec->{$field} // '' ) eq $args->{"$id-$field"};
+                next if ( $spec->{$field} // '' ) eq ( $args->{"$id-$field"} // '' );
                 if ((   $args->{"$id-$field"}
                         && RT::Ticket->_ParseCustomDateRangeSpec( $name, join ' - ', 'now', $args->{"$id-$field"} )
                     )
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 6fea220fb..40ec49984 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2647,15 +2647,10 @@ sub CustomDateRanges {
 
     my %ranges;
 
-    if ( my $config = RT->Config->Get('CustomDateRanges') ) {
+    if ( my( $config, $origin) = RT->Config->AllCustomDateRanges( 2 ) ) {
         %ranges = %{ $config->{$type} } if $config->{$type};
     }
 
-    if ( my $configui =  RT->Config->Get('CustomDateRangesUI') ) {
-        for my $name ( keys %{ $configui->{$type} || {} } ) {
-            $ranges{$name} ||= $configui->{$type}{$name};
-        }
-    }
     return %ranges;
 }
 
diff --git a/share/html/Elements/CustomDateRangesSpecs b/share/html/Elements/CustomDateRangesSpecs
index 6f810588c..8ab9053bc 100644
--- a/share/html/Elements/CustomDateRangesSpecs
+++ b/share/html/Elements/CustomDateRangesSpecs
@@ -75,7 +75,7 @@
   </div>
 % }
 % else {
-  <p><&|/l&>No custom date ranges in <% $Location %></&></p>
+  <p><&|/l&>No custom date ranges in</&> <% $Location %></p>
 % }
 </&>
 
diff --git a/share/html/Search/Elements/BuildFormatString b/share/html/Search/Elements/BuildFormatString
index de60f98a9..0d4d1775c 100644
--- a/share/html/Search/Elements/BuildFormatString
+++ b/share/html/Search/Elements/BuildFormatString
@@ -52,7 +52,7 @@ $Format => undef
 
 $Face => undef
 $Size => undef
-$Link => undef
+$Link => ''
 $Title => undef
 
 $AddCol => undef
diff --git a/share/html/Widgets/Form/CustomDateRange b/share/html/Widgets/Form/CustomDateRange
index 5b3a7ce18..3075b1143 100644
--- a/share/html/Widgets/Form/CustomDateRange
+++ b/share/html/Widgets/Form/CustomDateRange
@@ -68,12 +68,7 @@ if( $UserPreferences ) {
     my $current_user_id = $session{'CurrentUser'}->Id;
 
     # get the content of all Pref-CustomDateRanges attributes for other users
-    my $attributes = RT::Attributes->new( RT->SystemUser );
-    $attributes->Limit( FIELD => 'Name', VALUE => 'Pref-CustomDateRanges' );
-    $attributes->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $current_user_id );
-    $attributes->OrderBy( FIELD => 'Creator' );
-    my @from_other_users = map { $_->Content } @{$attributes->ItemsArrayRef};
-    $from_other_users = RT::Config::MergeCustomDateRangesSpecs( @from_other_users );
+    $from_other_users = RT::Config->CustomDateRangesFromOtherUsers( );
 
     $from_user = $session{'CurrentUser'}->UserObj->Preferences("CustomDateRanges" );
 }
@@ -90,3 +85,25 @@ $CurrentValue => '',
 $UserPreferences => 0,
 
 </%ARGS>
+
+<%METHOD Process>
+<%ARGS>
+$Name
+
+$Arguments    => {},
+
+$Default      => 0,
+$DefaultValue => '',
+</%ARGS>
+<%INIT>
+my $value = $Arguments->{ $Name };
+$value = '' unless defined $value;
+
+if ( $value eq '' ) {
+    return $DefaultValue unless $Default;
+    return undef;
+}
+return $value;
+</%INIT>
+</%METHOD>
+

commit 85a3435142e3b0216f32f6309e0a1767dc85e432
Author: michel <michel at bestpractical.com>
Date:   Tue Mar 17 21:50:40 2020 +0100

    Remove [Custom Date Ranges] menu entry.

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index a63425252..8cb477aac 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -552,9 +552,6 @@ sub BuildMainNav {
             title => loc('Edit Search'), path => "/Search/Build.html" . ( ($has_query) ? $args : '' ) );
         $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' && $current_user->HasRight( Object=> RT->System, Right => 'SuperUser');
         if ($has_query) {
             $current_search_menu->child( results => title => loc('Show Results'), path => "/Search/Results.html$args" );
         }

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


More information about the rt-commit mailing list