[Rt-commit] rt branch, 4.4.1-releng, created. rt-4.4.0-226-g9ab09be

Shawn Moore shawn at bestpractical.com
Wed May 18 15:28:54 EDT 2016


The branch, 4.4.1-releng has been created
        at  9ab09be6696f937313fb78ec0c2d280b0644cd4c (commit)

- Log -----------------------------------------------------------------
commit e428c4e37ce4cf64cf53129f221ae14b45e15ce0
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 28 13:15:15 2015 -0400

    Only attempt to synchronize "All recipients" box if it exists
    
    setCheckbox takes a single element, not a jQuery collection; while it
    aborted early if passed null, it did not do so if passed an empty
    collection.  This resulted in console errors after every request if
    there were no recipients across all scrips.

diff --git a/share/html/Ticket/Update.html b/share/html/Ticket/Update.html
index f0b8130..001a4eb 100644
--- a/share/html/Ticket/Update.html
+++ b/share/html/Ticket/Update.html
@@ -226,7 +226,9 @@ jQuery( function() {
                txn_send_field.change( syncCheckboxes );
                txn_send_field.click( function () { setCheckbox(this) } );
                jQuery("#recipients input[name=TxnSendMailToAll]").click( function() { setCheckbox(this, 'TxnSendMailTo'); } );
-               setCheckbox(txn_send_field);
+               if (txn_send_field.length > 0) {
+                   setCheckbox(txn_send_field[0]);
+               }
            }
        );
        jQuery('#previewscrips div.titlebox-content').load( '<% RT->Config->Get('WebPath')%>/Helpers/PreviewScrips',
@@ -236,7 +238,9 @@ jQuery( function() {
                txn_send_field.change( syncCheckboxes );
                txn_send_field.click( function () { setCheckbox(this) } );
                jQuery("#previewscrips input[name=TxnSendMailToAll]").click( function() { setCheckbox(this, 'TxnSendMailTo'); } );
-               setCheckbox(txn_send_field);
+               if (txn_send_field.length > 0) {
+                   setCheckbox(txn_send_field[0]);
+               }
            }
        );
    };

commit 20a9c093c8d69e575695c54f7050f18b96eaa2fe
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Apr 28 13:17:43 2015 -0400

    Switch to making POST requests, not GET requests
    
    If passed a string, the .load() function makes a GET request; if passed
    an object, it makes a POST request.  Since GET requests are limited in
    query-string length by the webserver, use a POST rquest to ensure that
    if the helper request is not dropped if the textarea contains a large
    amount of text.

diff --git a/share/html/Ticket/Update.html b/share/html/Ticket/Update.html
index 001a4eb..2a24670 100644
--- a/share/html/Ticket/Update.html
+++ b/share/html/Ticket/Update.html
@@ -220,7 +220,7 @@ jQuery( function() {
            jQuery("input[name=TxnSendMailTo]").filter( function() { return this.value == target.value; } ).prop("checked",jQuery(target).prop('checked'));
        };
        jQuery('#recipients div.titlebox-content').load( '<% RT->Config->Get('WebPath')%>/Helpers/ShowSimplifiedRecipients',
-           jQuery('form[name=TicketUpdate]').serialize(),
+           jQuery('form[name=TicketUpdate]').serializeArray(),
            function() {
                var txn_send_field = jQuery("#recipients input[name=TxnSendMailTo]");
                txn_send_field.change( syncCheckboxes );
@@ -232,7 +232,7 @@ jQuery( function() {
            }
        );
        jQuery('#previewscrips div.titlebox-content').load( '<% RT->Config->Get('WebPath')%>/Helpers/PreviewScrips',
-           jQuery('form[name=TicketUpdate]').serialize(),
+           jQuery('form[name=TicketUpdate]').serializeArray(),
            function() {
                var txn_send_field = jQuery("#previewscrips input[name=TxnSendMailTo]");
                txn_send_field.change( syncCheckboxes );

commit dd89c593aa4cc5f819c4ce13b9d014fd03d8c47a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 04:11:54 2016 +0000

    Add a parameter to hide the titlebox in ShowAttachments

diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index f32ac9c..bfa03d7 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -49,7 +49,8 @@
 <&| /Widgets/TitleBox, title => loc('Attachments'), 
         title_class=> 'inverse',  
         class => 'ticket-info-attachments',
-        color => "#336699" &>
+        color => "#336699",
+        hide_chrome => $HideTitleBox &>
 
 % foreach my $key (sort { lc($a) cmp lc($b) } keys %documents) {
 
@@ -95,5 +96,6 @@ while ( my $attach = $Attachments->Next() ) {
 $Ticket => undef
 $Attachments => undef
 $DisplayPath => $session{'CurrentUser'}->Privileged ? 'Ticket' : 'SelfService'
+$HideTitleBox => 0
 </%ARGS>
 
diff --git a/share/html/Widgets/TitleBox b/share/html/Widgets/TitleBox
index 822170c..7c65082 100644
--- a/share/html/Widgets/TitleBox
+++ b/share/html/Widgets/TitleBox
@@ -46,11 +46,16 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <div class="<% $class %>">
+% if ($hide_chrome) {
+  <% $content | n %>
+% } else {
   <& TitleBoxStart, %ARGS &><% $content | n %><& TitleBoxEnd &>
+% }
 </div>
 <%ARGS>
 $class => ''
 $hide_empty => 0
+$hide_chrome => 0
 </%ARGS>
 <%INIT>
 my $content = $m->content;

commit c4aed50cc67615ece9c0865afeb6687b9f77e10f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 04:29:15 2016 +0000

    Use the existing template and stylings of attachment display for reuse
    
        The previous display was a bit harder to parse.
    
    Fixes: I#31709

diff --git a/share/html/Ticket/Elements/AddAttachments b/share/html/Ticket/Elements/AddAttachments
index 3a63c59..34798e2 100644
--- a/share/html/Ticket/Elements/AddAttachments
+++ b/share/html/Ticket/Elements/AddAttachments
@@ -163,25 +163,16 @@ jQuery( function() {
         </div>
     </td>
 </tr>
-% if (@quoted_attachments) {
+% if ($TicketObj && $TicketObj->id) {
 <tr>
   <td class="label" valign="top"><&|/l&>Include attachments</&>:</td>
   <td id="reuse-attachments">
-%     for my $attach (@quoted_attachments) {
-    <label>
-      <input type="checkbox" class="checkbox" name="AttachExisting" value="<% $attach->Id %>" \
-             <% (grep { $attach->Id == $_ } @AttachExisting) ? 'checked' : '' %> />
-      <% $attach->Filename %>
-      <% loc("[_1] ([_2]) by [_3]", $attach->CreatedAsString, $attach->FriendlyContentLength, $m->scomp('/Elements/ShowUser', User => $attach->CreatorObj)) |n %>
-    </label>
-<%perl>
-my $url = RT->System->ExternalStorageURLFor($attach) || sprintf '%s/Ticket/Attachment/%d/%d/%s',
-            RT->Config->Get('WebPath'), $attach->TransactionObj->Id, $attach->Id,
-            $m->interp->apply_escapes($attach->Filename, 'u');
-</%perl>
-      (<a href="<% $url %>" target="_blank"><&|/l&>View</&></a>)
-      <br />
-%     }
+    <& /Ticket/Elements/ShowAttachments,
+      Ticket       => $TicketObj,
+      Selectable   => 1,
+      HideTitleBox => 1,
+      Checked      => \@AttachExisting,
+    &>
   </td>
 </tr>
 % }
@@ -197,11 +188,4 @@ my $attachments;
 if ( exists $session{'Attachments'}{ $Token } && keys %{ $session{'Attachments'}{ $Token } } ) {
     $attachments = $session{'Attachments'}{ $Token };
 }
-
-my @quoted_attachments;
-if ($TicketObj and $TicketObj->id) {
-    @quoted_attachments = sort { lc($a->Filename) cmp lc($b->Filename) }
-                          grep { defined $_->Filename and length $_->Filename }
-                            @{$TicketObj->Attachments->ItemsArrayRef};
-}
 </%INIT>
diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index bfa03d7..2a49c91 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -55,10 +55,17 @@
 % foreach my $key (sort { lc($a) cmp lc($b) } keys %documents) {
 
 <%$key%><br />
-<ul>
+<ul <% $Selectable ? 'class="selectable"' : '' |n %> >
 % foreach my $rev (@{$documents{$key}}) {
 % if ($rev->ContentLength) {
 <li><font size="-2">
+
+% if ($Selectable) {
+    <label>
+    <input type="checkbox" class="checkbox" name="AttachExisting" value="<% $rev->Id %>" \
+             <% $is_checked{$rev->Id} ? 'checked' : '' %> />
+% }
+
 % if (my $url = RT->System->ExternalStorageURLFor($rev)) {
 <a href="<%$url%>">
 % } else {
@@ -67,6 +74,11 @@
 % my $desc = loc("[_1] ([_2]) by [_3]", $rev->CreatedAsString, $rev->FriendlyContentLength, $m->scomp('/Elements/ShowUser', User => $rev->CreatorObj));
 <% $desc |n%>
 </a>
+
+% if ($Selectable) {
+    </label>
+% }
+
 </font></li>
 % }
 % }
@@ -91,11 +103,14 @@ while ( my $attach = $Attachments->Next() ) {
    unshift( @{ $documents{ $attach->Filename } }, $attach );
 }
 
+my %is_checked = map { $_ => 1 } @Checked;
 </%INIT>
 <%ARGS>
 $Ticket => undef
 $Attachments => undef
 $DisplayPath => $session{'CurrentUser'}->Privileged ? 'Ticket' : 'SelfService'
 $HideTitleBox => 0
+$Selectable => 0
+ at Checked => ()
 </%ARGS>
 
diff --git a/share/static/css/base/forms.css b/share/static/css/base/forms.css
index 9f57331..a03be9b 100644
--- a/share/static/css/base/forms.css
+++ b/share/static/css/base/forms.css
@@ -251,6 +251,19 @@ form div.submit div.buttons div.next {
     display: none;
 }
 
+ul.selectable {
+    list-style-type: none;
+}
+
+ul.selectable input[type=checkbox],
+ul.selectable a {
+    vertical-align: bottom;
+}
+
+#reuse-attachments {
+    padding-top: 0.25em;
+}
+
 /* query builder */
 
 #formatbuttons {

commit 9de4ca7675f8398d7ef78992d1817f91b8d050b1
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 04:33:32 2016 +0000

    Replace explicit <font> tag with CSS

diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index 2a49c91..d9432b7 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -58,7 +58,7 @@
 <ul <% $Selectable ? 'class="selectable"' : '' |n %> >
 % foreach my $rev (@{$documents{$key}}) {
 % if ($rev->ContentLength) {
-<li><font size="-2">
+<li>
 
 % if ($Selectable) {
     <label>
@@ -79,7 +79,7 @@
     </label>
 % }
 
-</font></li>
+</li>
 % }
 % }
 </ul>
diff --git a/share/static/css/base/forms.css b/share/static/css/base/forms.css
index a03be9b..2584ee0 100644
--- a/share/static/css/base/forms.css
+++ b/share/static/css/base/forms.css
@@ -264,6 +264,10 @@ ul.selectable a {
     padding-top: 0.25em;
 }
 
+.ticket-info-attachments ul li {
+    font-size: .8em;
+}
+
 /* query builder */
 
 #formatbuttons {

commit b2159722beb062b138f9d348e8d343ba6e9f06c0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:35:13 2016 +0000

    Fix variable name in ShowQueue
    
        Label is what we display to the user; value is what we use internally

diff --git a/share/html/Ticket/Elements/ShowQueue b/share/html/Ticket/Elements/ShowQueue
index 1e4b444..5b14cb5 100644
--- a/share/html/Ticket/Elements/ShowQueue
+++ b/share/html/Ticket/Elements/ShowQueue
@@ -46,22 +46,22 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 % if ( $query ) {
-<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% $query |u,n %>"><% $value %></a>
+<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% $query |u,n %>"><% $label %></a>
 % } else {
-<% $value %>
+<% $label %>
 % }
 <%ARGS>
 $Ticket => undef
 $QueueObj
 </%ARGS>
 <%INIT>
-my $value = $QueueObj->Name;
+my $label = $QueueObj->Name;
 my $query;
 
 if ( $Ticket and $Ticket->CurrentUserHasRight('SeeQueue') ) {
     # Grab the queue name anyway if the current user can
     # see the queue based on his role for this ticket
-    $value = $QueueObj->__Value('Name');
+    $label = $QueueObj->__Value('Name');
     if ( $session{CurrentUser}->Privileged ) {
         my @statuses = $QueueObj->ActiveStatusArray();
         s{(['\\])}{\\$1}g for @statuses;
@@ -69,6 +69,6 @@ if ( $Ticket and $Ticket->CurrentUserHasRight('SeeQueue') ) {
     }
 }
 
-$value = '#'. $QueueObj->id
-    unless defined $value && length $value;
+$label = '#'. $QueueObj->id
+    unless defined $label && length $label;
 </%INIT>

commit 0890b14f461254929431eb33f1acbbd682bf6b17
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:36:46 2016 +0000

    Use Status = '__Active__' for ShowQueue
    
    Fixes: I#31696

diff --git a/share/html/Ticket/Elements/ShowQueue b/share/html/Ticket/Elements/ShowQueue
index 5b14cb5..baa1250 100644
--- a/share/html/Ticket/Elements/ShowQueue
+++ b/share/html/Ticket/Elements/ShowQueue
@@ -63,9 +63,7 @@ if ( $Ticket and $Ticket->CurrentUserHasRight('SeeQueue') ) {
     # see the queue based on his role for this ticket
     $label = $QueueObj->__Value('Name');
     if ( $session{CurrentUser}->Privileged ) {
-        my @statuses = $QueueObj->ActiveStatusArray();
-        s{(['\\])}{\\$1}g for @statuses;
-        $query = "Queue = " . $QueueObj->id . " AND ( ". join(" OR ", map {"Status = '$_'"} @statuses) . " )";
+        $query = "Queue = " . $QueueObj->id . " AND Status = '__Active__'";
     }
 }
 

commit df1699d9c4ee304c7a9d240c6bbc403437245c0e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:40:49 2016 +0000

    Use queue name rather than queue id in ShowQueue search link
    
        This is the same escaping code we've long since used in
        the QueueList. Using the queue by ID in a search is very
        surprising and not at all self-documenting.

diff --git a/share/html/Ticket/Elements/ShowQueue b/share/html/Ticket/Elements/ShowQueue
index baa1250..f15acfe 100644
--- a/share/html/Ticket/Elements/ShowQueue
+++ b/share/html/Ticket/Elements/ShowQueue
@@ -63,7 +63,9 @@ if ( $Ticket and $Ticket->CurrentUserHasRight('SeeQueue') ) {
     # see the queue based on his role for this ticket
     $label = $QueueObj->__Value('Name');
     if ( $session{CurrentUser}->Privileged ) {
-        $query = "Queue = " . $QueueObj->id . " AND Status = '__Active__'";
+        my $queue_name_parameter = $label;
+        $queue_name_parameter =~ s/(['\\])/\\$1/g; #'
+        $query = "Queue = '$queue_name_parameter' AND Status = '__Active__'";
     }
 }
 

commit e9dcc3e9f10bc7ff057c20aa7d5f8205a9229a8c
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:47:20 2016 +0000

    Use Status = '__Active__' for queuelist links
    
    Fixes: I#31695

diff --git a/share/html/Elements/QueueSummaryByLifecycle b/share/html/Elements/QueueSummaryByLifecycle
index 04953db..4bc1d24 100644
--- a/share/html/Elements/QueueSummaryByLifecycle
+++ b/share/html/Elements/QueueSummaryByLifecycle
@@ -70,7 +70,7 @@ for my $queue (@queues) {
 <tr class="<% $i%2 ? 'oddline' : 'evenline'%>" >
 
 <td>
-    <a href="<% $link_all->($queue, \@cur_statuses) %>" title="<% $queue->{Description} %>"><% $queue->{Name} %></a>
+    <a href="<% $link_all->($queue) %>" title="<% $queue->{Description} %>"><% $queue->{Name} %></a>
 </td>
 
 %   for my $status (@cur_statuses) {
@@ -93,10 +93,8 @@ my $build_search_link = sub {
 };
 
 my $link_all = sub {
-    my ($queue, $all_statuses) = @_;
-    my @escaped = @{$all_statuses};
-    s{(['\\])}{\\$1}g for @escaped;
-    return $build_search_link->($queue->{Name}, "(".join(" OR ", map "Status = '$_'", @escaped).")");
+    my ($queue) = @_;
+    return $build_search_link->($queue->{Name}, "Status = '__Active__'");
 };
 
 my $link_status = sub {
diff --git a/share/html/Elements/QueueSummaryByStatus b/share/html/Elements/QueueSummaryByStatus
index f9ec66c..bbb4c76 100644
--- a/share/html/Elements/QueueSummaryByStatus
+++ b/share/html/Elements/QueueSummaryByStatus
@@ -59,12 +59,11 @@ my $i = 0;
 for my $queue (@queues) {
     $i++;
     my $lifecycle = $lifecycle{ lc $queue->{'Lifecycle'} };
-    my @queue_statuses = grep { $lifecycle->IsValid($_) } @statuses;
 </%PERL>
 <tr class="<% $i%2 ? 'oddline' : 'evenline'%>" >
 
 <td>
-    <a href="<% $link_all->($queue, \@queue_statuses) %>" title="<% $queue->{Description} %>"><% $queue->{Name} %></a>
+    <a href="<% $link_all->($queue) %>" title="<% $queue->{Description} %>"><% $queue->{Name} %></a>
 </td>
 
 <%perl>
@@ -92,8 +91,8 @@ my $build_search_link = sub {
 };
 
 my $link_all = sub {
-    my ($queue, $all_statuses) = @_;
-    return $build_search_link->($queue->{Name}, "(".join(" OR ", map "Status = '$_'", @$all_statuses).")");
+    my ($queue) = @_;
+    return $build_search_link->($queue->{Name}, "Status = '__Active__'");
 };
 
 my $link_status = sub {

commit 560fe2f83056e330e1b78dc9c74ce531e8208d78
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:51:14 2016 +0000

    This syntax highlighting hint is no longer relevant
    
        This belongs on the line with the escaping regular expression,
        which was moved in d48afa23. It went unnoticed presumably because
        emacs colors leaning toothpicks more correctly than vim.

diff --git a/share/html/Elements/QueueSummaryByLifecycle b/share/html/Elements/QueueSummaryByLifecycle
index 4bc1d24..420ccd6 100644
--- a/share/html/Elements/QueueSummaryByLifecycle
+++ b/share/html/Elements/QueueSummaryByLifecycle
@@ -150,7 +150,7 @@ my @escaped = @statuses;
 s{(['\\])}{\\$1}g for @escaped;
 my $query =
     "(".
-    join(" OR ", map {"Status = '$_'"} @escaped) #'
+    join(" OR ", map {"Status = '$_'"} @escaped)
     .") AND (".
     join(' OR ', map "Queue = ".$_->{id}, @queues)
     .")";

commit e9119915e8b2321115404037463a8a8437ec241d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:54:02 2016 +0000

    Use Status = '__Active__' for queuelist report query
    
        Passing statuses _was_ supported but is used by neither core nor RTIR
        (which ships its own QueueSummary template)
    
        Along the way, ByLifecycle received a bugfix, d48afa23, which
        ByStatus did not. This commit obsoletes that bugfix, restoring some
        parity between these two templates.

diff --git a/share/html/Elements/QueueSummaryByLifecycle b/share/html/Elements/QueueSummaryByLifecycle
index 420ccd6..a768412 100644
--- a/share/html/Elements/QueueSummaryByLifecycle
+++ b/share/html/Elements/QueueSummaryByLifecycle
@@ -132,12 +132,11 @@ for my $queue (@queues) {
     $lifecycle{ lc $cycle->Name } = $cycle;
 }
 
-unless (@statuses) {
-    my %seen;
-    foreach my $set ( 'initial', 'active' ) {
-        foreach my $lifecycle ( map $lifecycle{$_}, sort keys %lifecycle ) {
-            push @statuses, grep !$seen{ lc $_ }++, $lifecycle->Valid($set);
-        }
+my @statuses;
+my %seen;
+foreach my $set ( 'initial', 'active' ) {
+    foreach my $lifecycle ( map $lifecycle{$_}, sort keys %lifecycle ) {
+        push @statuses, grep !$seen{ lc $_ }++, $lifecycle->Valid($set);
     }
 }
 
@@ -146,12 +145,8 @@ my $statuses = {};
 
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( RT->SystemUser );
-my @escaped = @statuses;
-s{(['\\])}{\\$1}g for @escaped;
 my $query =
-    "(".
-    join(" OR ", map {"Status = '$_'"} @escaped)
-    .") AND (".
+    "(Status = '__Active__') AND (".
     join(' OR ', map "Queue = ".$_->{id}, @queues)
     .")";
 $query = 'id < 0' unless @queues;
@@ -165,5 +160,4 @@ while ( my $entry = $report->Next ) {
 </%INIT>
 <%ARGS>
 $queue_filter => undef
- at statuses => ()
 </%ARGS>
diff --git a/share/html/Elements/QueueSummaryByStatus b/share/html/Elements/QueueSummaryByStatus
index bbb4c76..812ba4b 100644
--- a/share/html/Elements/QueueSummaryByStatus
+++ b/share/html/Elements/QueueSummaryByStatus
@@ -129,12 +129,11 @@ for my $queue (@queues) {
     $lifecycle{ lc $cycle->Name } = $cycle;
 }
 
-unless (@statuses) {
-    my %seen;
-    foreach my $set ( 'initial', 'active' ) {
-        foreach my $lifecycle ( map $lifecycle{$_}, sort keys %lifecycle ) {
-            push @statuses, grep !$seen{ lc $_ }++, $lifecycle->Valid($set);
-        }
+my @statuses;
+my %seen;
+foreach my $set ( 'initial', 'active' ) {
+    foreach my $lifecycle ( map $lifecycle{$_}, sort keys %lifecycle ) {
+        push @statuses, grep !$seen{ lc $_ }++, $lifecycle->Valid($set);
     }
 }
 
@@ -144,9 +143,7 @@ my $statuses = {};
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( RT->SystemUser );
 my $query =
-    "(".
-    join(" OR ", map {s{(['\\])}{\\$1}g; "Status = '$_'"} @statuses) #'
-    .") AND (".
+    "(Status = '__Active__') AND (".
     join(' OR ', map "Queue = ".$_->{id}, @queues)
     .")";
 $query = 'id < 0' unless @queues;
@@ -160,5 +157,4 @@ while ( my $entry = $report->Next ) {
 </%INIT>
 <%ARGS>
 $queue_filter => undef
- at statuses => ()
 </%ARGS>

commit 2513da78368d172fdf3a32ac84dd01c780f6e40e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 06:15:18 2016 +0000

    Use Status = '__Active__' for ExtendedStatus dependency search link

diff --git a/share/html/Elements/RT__Ticket/ColumnMap b/share/html/Elements/RT__Ticket/ColumnMap
index d695ba3..33e32ed 100644
--- a/share/html/Elements/RT__Ticket/ColumnMap
+++ b/share/html/Elements/RT__Ticket/ColumnMap
@@ -132,9 +132,7 @@ $COLUMN_MAP = {
                     return \'<em>', loc('(pending approval)'), \'</em>';
                 }
                 else {
-                    my $Query = "DependedOnBy = " . $Ticket->id;
-                    $Query .= " AND (" . join(" OR ", map { "Status = '$_'" } RT::Queue->ActiveStatusArray) . ")";
-
+                    my $Query = "DependedOnBy = " . $Ticket->id . " AND Status = '__Active__'";
                     my $SearchURL = RT->Config->Get('WebPath') . '/Search/Results.html?' . $m->comp('/Elements/QueryString', Query => $Query);
 
                     if ($count == 1) {

commit a1aa7e35a8a7aac66943cebac392a62100216fc6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 06:20:42 2016 +0000

    Use Status = '__Active__' for Assets->Ticket search

diff --git a/share/html/Elements/RT__Asset/ColumnMap b/share/html/Elements/RT__Asset/ColumnMap
index ecf6993..618501d 100644
--- a/share/html/Elements/RT__Asset/ColumnMap
+++ b/share/html/Elements/RT__Asset/ColumnMap
@@ -97,8 +97,7 @@ my $COLUMN_MAP = {
         title     => 'Active tickets', # loc
         value     => sub {
             my $Asset = shift;
-            my $Query = "RefersTo = 'asset:" . $Asset->id . "'";
-            $Query .= " AND (" . join(" OR ", map { "Status = '$_'" } RT::Queue->ActiveStatusArray) . ")";
+            my $Query = "RefersTo = 'asset:" . $Asset->id . "' AND Status = '__Active__'";
             my $SearchURL = RT->Config->Get('WebPath') . '/Search/Results.html?' . $m->comp('/Elements/QueryString', Query => $Query);
             return \'[ <a href="',$SearchURL,\'">Active</a> ]';
         }
@@ -107,8 +106,7 @@ my $COLUMN_MAP = {
         title     => 'Inactive tickets', # loc
         value     => sub {
             my $Asset = shift;
-            my $Query = "RefersTo = 'asset:" . $Asset->id . "'";
-            $Query .= " AND (" . join(" OR ", map { "Status = '$_'" } RT::Queue->InactiveStatusArray) . ")";
+            my $Query = "RefersTo = 'asset:" . $Asset->id . "' AND Status = '__Inactive__'";
             my $SearchURL = RT->Config->Get('WebPath') . '/Search/Results.html?' . $m->comp('/Elements/QueryString', Query => $Query);
             return \'[ <a href="',$SearchURL,\'">Inactive</a> ]';
         }

commit 2d20155bfdec978f3b2a0fd335b55eae9275ecf5
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 24 18:26:27 2016 +0000

    Use Status = '__Active__' for simple search status:active

diff --git a/lib/RT/Search/Simple.pm b/lib/RT/Search/Simple.pm
index ac7b226..6fad6be 100644
--- a/lib/RT/Search/Simple.pm
+++ b/lib/RT/Search/Simple.pm
@@ -256,9 +256,9 @@ sub HandleContent   { return content   => "Content LIKE '$_[1]'"; }
 sub HandleId        { $_[1] =~ s/^#//; return id => "Id = $_[1]"; }
 sub HandleStatus    {
     if ($_[1] =~ /^active$/i and !$_[2]) {
-        return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray();
+        return status => "Status = '__Active__'";
     } elsif ($_[1] =~ /^inactive$/i and !$_[2]) {
-        return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->InactiveStatusArray();
+        return status => "Status = '__Inactive__'";
     } elsif ($_[1] =~ /^any$/i and !$_[2]) {
         return 'status';
     } else {
diff --git a/t/web/simple_search.t b/t/web/simple_search.t
index 710efb1..bdfaacb 100644
--- a/t/web/simple_search.t
+++ b/t/web/simple_search.t
@@ -61,10 +61,10 @@ ok $two_words_queue && $two_words_queue->id, 'loaded or created a queue';
     is $parser->QueryToSQL('owner:root at localhost'), "( Owner.EmailAddress = 'root\@localhost' ) AND $active", "Email address as owner";
 
     is $parser->QueryToSQL("resolved me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' )", "correct parsing";
-    is $parser->QueryToSQL("resolved active me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' OR Status = 'new' OR Status = 'open' OR Status = 'stalled' )", "correct parsing";
-    is $parser->QueryToSQL("status:active"), $active, "Explicit active search";
+    is $parser->QueryToSQL("resolved active me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' OR Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("status:active"), "( Status = '__Active__' )", "Explicit active search";
     is $parser->QueryToSQL("status:'active'"), "( Status = 'active' )", "Quoting active makes it the actual word";
-    is $parser->QueryToSQL("inactive me"), "( Owner.id = '__CurrentUser__' ) AND $inactive", "correct parsing";
+    is $parser->QueryToSQL("inactive me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = '__Inactive__' )", "correct parsing";
 
     is $parser->QueryToSQL("cf.Foo:bar"), "( 'CF.{Foo}' LIKE 'bar' ) AND $active", "correct parsing of CFs";
     is $parser->QueryToSQL(q{cf."don't foo?":'bar n\\' baz'}), qq/( 'CF.{don\\'t foo?}' LIKE 'bar n\\' baz' ) AND $active/, "correct parsing of CFs with quotes";

commit 81b07c7dad26320b22f5ed85e98247f2fc7890a3
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 06:26:22 2016 +0000

    Use Status = '__Active__' for OnlySearchActiveTicketsInSimpleSearch

diff --git a/lib/RT/Search/Simple.pm b/lib/RT/Search/Simple.pm
index 6fad6be..ac44252 100644
--- a/lib/RT/Search/Simple.pm
+++ b/lib/RT/Search/Simple.pm
@@ -188,7 +188,7 @@ sub Finalize {
     # limits ourselves, and we're not limited by id
     if (not $limits->{status} and not $limits->{id}
         and RT::Config->Get('OnlySearchActiveTicketsInSimpleSearch', $self->TicketsObj->CurrentUser)) {
-        $limits->{status} = [map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray()];
+        $limits->{status} = ["Status = '__Active__'"];
     }
 
     # Respect the "only search these queues" limit if we didn't
diff --git a/t/ticket/simple_search.t b/t/ticket/simple_search.t
index 02bf733..bdd51b4 100644
--- a/t/ticket/simple_search.t
+++ b/t/ticket/simple_search.t
@@ -22,20 +22,18 @@ ok( $id, $msg );
 
 use_ok("RT::Search::Simple");
 
-my $active_statuses = join( " OR ", map "Status = '$_'", RT::Queue->ActiveStatusArray());
-
 my $tickets = RT::Tickets->new(RT->SystemUser);
 my $quick = RT::Search::Simple->new(Argument => "",
                                  TicketsObj => $tickets);
 my @tests = (
     "General new open root"     => "( Owner = 'root' ) AND ( Queue = 'General' ) AND ( Status = 'new' OR Status = 'open' )", 
-    "General"              => "( Queue = 'General' ) AND ( $active_statuses )",
+    "General"              => "( Queue = 'General' ) AND ( Status = '__Active__' )",
     "General any"          => "( Queue = 'General' )",
-    "fulltext:jesse"       => "( Content LIKE 'jesse' ) AND ( $active_statuses )",
-    $queue                 => "( Queue = '$queue' ) AND ( $active_statuses )",
-    "root $queue"          => "( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( $active_statuses )",
-    "notauser $queue"      => "( Subject LIKE 'notauser' ) AND ( Queue = '$queue' ) AND ( $active_statuses )",
-    "notauser $queue root" => "( Subject LIKE 'notauser' ) AND ( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( $active_statuses )");
+    "fulltext:jesse"       => "( Content LIKE 'jesse' ) AND ( Status = '__Active__' )",
+    $queue                 => "( Queue = '$queue' ) AND ( Status = '__Active__' )",
+    "root $queue"          => "( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( Status = '__Active__' )",
+    "notauser $queue"      => "( Subject LIKE 'notauser' ) AND ( Queue = '$queue' ) AND ( Status = '__Active__' )",
+    "notauser $queue root" => "( Subject LIKE 'notauser' ) AND ( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( Status = '__Active__' )");
 
 while (my ($from, $to) = splice @tests, 0, 2) {
     is($quick->QueryToSQL($from), $to, "<$from> -> <$to>");
diff --git a/t/web/simple_search.t b/t/web/simple_search.t
index bdfaacb..41ba6a4 100644
--- a/t/web/simple_search.t
+++ b/t/web/simple_search.t
@@ -18,47 +18,45 @@ ok $two_words_queue && $two_words_queue->id, 'loaded or created a queue';
 
 {
     my $tickets = RT::Tickets->new( RT->SystemUser );
-    my $active = "( ".join( " OR ", map "Status = '$_'", RT::Queue->ActiveStatusArray())." )";
-    my $inactive = "( ".join( " OR ", map "Status = '$_'", RT::Queue->InactiveStatusArray())." )";
 
     require RT::Search::Simple;
     my $parser = RT::Search::Simple->new(
         TicketsObj => $tickets,
         Argument   => '',
     );
-    is $parser->QueryToSQL("foo"), "( Subject LIKE 'foo' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("1 foo"), "( Subject LIKE 'foo' AND Subject LIKE '1' ) AND $active", "correct parsing";
+    is $parser->QueryToSQL("foo"), "( Subject LIKE 'foo' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("1 foo"), "( Subject LIKE 'foo' AND Subject LIKE '1' ) AND ( Status = '__Active__' )", "correct parsing";
     is $parser->QueryToSQL("1"), "( Id = 1 )", "correct parsing";
     is $parser->QueryToSQL("#1"), "( Id = 1 )", "correct parsing";
-    is $parser->QueryToSQL("'1'"), "( Subject LIKE '1' ) AND $active", "correct parsing";
+    is $parser->QueryToSQL("'1'"), "( Subject LIKE '1' ) AND ( Status = '__Active__' )", "correct parsing";
 
     is $parser->QueryToSQL("foo bar"),
-        "( Subject LIKE 'foo' AND Subject LIKE 'bar' ) AND $active",
+        "( Subject LIKE 'foo' AND Subject LIKE 'bar' ) AND ( Status = '__Active__' )",
         "correct parsing";
     is $parser->QueryToSQL("'foo bar'"),
-        "( Subject LIKE 'foo bar' ) AND $active",
+        "( Subject LIKE 'foo bar' ) AND ( Status = '__Active__' )",
         "correct parsing";
 
     is $parser->QueryToSQL("'foo \\' bar'"),
-        "( Subject LIKE 'foo \\' bar' ) AND $active",
+        "( Subject LIKE 'foo \\' bar' ) AND ( Status = '__Active__' )",
         "correct parsing";
     is $parser->QueryToSQL('"foo \' bar"'),
-        "( Subject LIKE 'foo \\' bar' ) AND $active",
+        "( Subject LIKE 'foo \\' bar' ) AND ( Status = '__Active__' )",
         "correct parsing";
     is $parser->QueryToSQL('"\f\o\o"'),
-        "( Subject LIKE '\\\\f\\\\o\\\\o' ) AND $active",
+        "( Subject LIKE '\\\\f\\\\o\\\\o' ) AND ( Status = '__Active__' )",
         "correct parsing";
 
-    is $parser->QueryToSQL("General"), "( Queue = 'General' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("'Two Words'"), "( Subject LIKE 'Two Words' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("queue:'Two Words'"), "( Queue = 'Two Words' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("subject:'Two Words'"), "$active AND ( Subject LIKE 'Two Words' )", "correct parsing";
+    is $parser->QueryToSQL("General"), "( Queue = 'General' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("'Two Words'"), "( Subject LIKE 'Two Words' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("queue:'Two Words'"), "( Queue = 'Two Words' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("subject:'Two Words'"), "( Status = '__Active__' ) AND ( Subject LIKE 'Two Words' )", "correct parsing";
 
-    is $parser->QueryToSQL("me"), "( Owner.id = '__CurrentUser__' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("'me'"), "( Subject LIKE 'me' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("owner:me"), "( Owner.id = '__CurrentUser__' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL("owner:'me'"), "( Owner = 'me' ) AND $active", "correct parsing";
-    is $parser->QueryToSQL('owner:root at localhost'), "( Owner.EmailAddress = 'root\@localhost' ) AND $active", "Email address as owner";
+    is $parser->QueryToSQL("me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("'me'"), "( Subject LIKE 'me' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("owner:me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL("owner:'me'"), "( Owner = 'me' ) AND ( Status = '__Active__' )", "correct parsing";
+    is $parser->QueryToSQL('owner:root at localhost'), "( Owner.EmailAddress = 'root\@localhost' ) AND ( Status = '__Active__' )", "Email address as owner";
 
     is $parser->QueryToSQL("resolved me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' )", "correct parsing";
     is $parser->QueryToSQL("resolved active me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = 'resolved' OR Status = '__Active__' )", "correct parsing";
@@ -66,8 +64,8 @@ ok $two_words_queue && $two_words_queue->id, 'loaded or created a queue';
     is $parser->QueryToSQL("status:'active'"), "( Status = 'active' )", "Quoting active makes it the actual word";
     is $parser->QueryToSQL("inactive me"), "( Owner.id = '__CurrentUser__' ) AND ( Status = '__Inactive__' )", "correct parsing";
 
-    is $parser->QueryToSQL("cf.Foo:bar"), "( 'CF.{Foo}' LIKE 'bar' ) AND $active", "correct parsing of CFs";
-    is $parser->QueryToSQL(q{cf."don't foo?":'bar n\\' baz'}), qq/( 'CF.{don\\'t foo?}' LIKE 'bar n\\' baz' ) AND $active/, "correct parsing of CFs with quotes";
+    is $parser->QueryToSQL("cf.Foo:bar"), "( 'CF.{Foo}' LIKE 'bar' ) AND ( Status = '__Active__' )", "correct parsing of CFs";
+    is $parser->QueryToSQL(q{cf."don't foo?":'bar n\\' baz'}), qq/( 'CF.{don\\'t foo?}' LIKE 'bar n\\' baz' ) AND ( Status = '__Active__' )/, "correct parsing of CFs with quotes";
 }
 
 my $ticket_found_1 = RT::Ticket->new($RT::SystemUser);

commit 348be17804bd621fa204ab8cc1587b2cd8ff3e9b
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Thu Mar 24 18:14:28 2016 +0000

    re-add return statement to Assets RoleLimit
    
    Fixes: I#31546

diff --git a/lib/RT/SearchBuilder/Role/Roles.pm b/lib/RT/SearchBuilder/Role/Roles.pm
index 4bf938a..8c56cfb 100644
--- a/lib/RT/SearchBuilder/Role/Roles.pm
+++ b/lib/RT/SearchBuilder/Role/Roles.pm
@@ -394,6 +394,7 @@ sub RoleLimit {
     if ($args{BUNDLE} and not @{$args{BUNDLE}}) {
         @{$args{BUNDLE}} = ($groups, $group_members, $users);
     }
+    return ($groups, $group_members, $users);
 }
 
 1;

commit 4e79b39d9e80e2325eb13c24e589badda96301da
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Thu Mar 24 23:42:16 2016 +0000

    add helper for uploading attachments to SelfService and change JS to use WebHomePath
    
    Fixes: I#31845

diff --git a/share/html/SelfService/Helpers/Upload/Add b/share/html/SelfService/Helpers/Upload/Add
new file mode 100644
index 0000000..53c3dab
--- /dev/null
+++ b/share/html/SelfService/Helpers/Upload/Add
@@ -0,0 +1,58 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 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 }}}
+<%args>
+$Token => ''
+</%args>
+
+<%init>
+
+ProcessAttachments( Token => $Token, ARGSRef => \%ARGS );
+$r->content_type('application/json; charset=utf-8');
+$m->out( JSON({status => 'success'}) );
+$m->abort;
+</%init>
diff --git a/share/html/Ticket/Elements/AddAttachments b/share/html/Ticket/Elements/AddAttachments
index 3a63c59..32217c1 100644
--- a/share/html/Ticket/Elements/AddAttachments
+++ b/share/html/Ticket/Elements/AddAttachments
@@ -66,7 +66,7 @@
 Dropzone.autoDiscover = false;
 jQuery( function() {
     var attachDropzone = new Dropzone('#attach-dropzone', {
-        url: '<% RT->Config->Get('WebPath') %>/Helpers/Upload/Add?Token=' + jQuery('#attach-dropzone').closest('form').find('input[name=Token]').val(),
+        url: RT.Config.WebHomePath + '/Helpers/Upload/Add?Token=' + jQuery('#attach-dropzone').closest('form').find('input[name=Token]').val(),
         paramName: "Attach",
         dictDefaultMessage: <% loc("Drop files here or click to attach") |n,j %>,
         maxFilesize: 10000,

commit 0ead601ea9e5649412586e20adf9c958bd42cf3b
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue Mar 29 10:30:36 2016 -0400

    Pass CurrentUser to HandleAction methods
    
    CurrentUser is often needed for mail actions, so pass
    it as a parameter to HandleAction methods. Also makes
    the code consistent with the Mail plugins documentation
    which lists CurrentUser as a passed parameter.

diff --git a/lib/RT/Interface/Email.pm b/lib/RT/Interface/Email.pm
index b8da7ff..7ad978f 100644
--- a/lib/RT/Interface/Email.pm
+++ b/lib/RT/Interface/Email.pm
@@ -248,6 +248,7 @@ sub Gateway {
             Action      => $action,
             Subject     => $Subject,
             Message     => $Message,
+            CurrentUser => $CurrentUser,
             Ticket      => $Ticket,
             TicketId    => $args{ticket},
             Queue       => $SystemQueueObj,

commit 8ee2c779526defe69f1e5b9ce34fb8bc98d7c72a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon Apr 11 20:40:18 2016 +0000

    Fix regression with Bulk Update check/clear all checkboxes
    
        input.name is empty, causing modern jQuery to throw a syntax error
    
    Fixes: I#31667

diff --git a/share/static/js/util.js b/share/static/js/util.js
index 45b3948..93db1b8 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -99,7 +99,7 @@ function setCheckbox(input, name, val) {
         name = input.name || input.attr('name');
         is_set_event = true;
     }
-    else {
+    else if (input.name) {
         var allfield = jQuery('input[name=' + input.name + ']');
         allfield.prop('checked', val);
     }

commit 63944d748204abfb1e4b69663481cd098d067a00
Merge: c50ba85 20a9c09
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Apr 21 20:14:46 2016 +0000

    Merge branch '4.4/preview-scrips-post' into 4.4-trunk


commit 508763e4285560dbf24f0c3a1d0cd3de1876d1b2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Apr 14 11:57:55 2016 -0400

    Add callback to right-column bottom of Ticket/Update

diff --git a/share/html/Ticket/Update.html b/share/html/Ticket/Update.html
index 2e07692..d4e5ed4 100644
--- a/share/html/Ticket/Update.html
+++ b/share/html/Ticket/Update.html
@@ -123,6 +123,8 @@
 
   </table>
   </&>
+
+% $m->callback( %ARGS, CallbackName => 'RightColumnBottom', Ticket => $TicketObj );
 </div>
 
 <div id="ticket-update-message">

commit b723ba07669ff03f32d885cb065fabc10cf3e9df
Merge: 508763e 348be17
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 12:25:46 2016 -0400

    Merge branch '4.4/fix-assets-people-search' into 4.4-trunk


commit 4134a5e93fcb0ef3e1b0bac32718d1d1e378ccb8
Merge: b723ba0 0ead601
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 12:32:30 2016 -0400

    Merge branch '4.4/handle-action-pass-currentuser' into 4.4-trunk


commit 81cf4beaedb4432af3c91dffed11537ecbfb8629
Merge: 4134a5e 4e79b39
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 12:35:17 2016 -0400

    Merge branch '4.4/fix-selfservice-attachments' into 4.4-trunk


commit 4b8e2b5111bdaedb2108883ad95f4752bb4b568c
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 17:01:42 2016 +0000

    Fix _CoreAccessible default for Queue SLADisabled
    
    Fixes: I#31822

diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 3f074b9..7565b1d 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -1013,7 +1013,7 @@ sub _CoreAccessible {
         LastUpdated => 
         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
         SLADisabled => 
-        {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
+        {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '1'},
         Disabled => 
         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
 

commit f1f76c728fc56d8aea45c0ad3dec0f1da461551a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 17:13:25 2016 +0000

    Consistently use $addr->address to avoid overloading error
    
        Using $addr directly exposes a bug with older versions of Email::Address
        since that module started using fallback string overloading only in
        version 1.899, while the minimum version we require is 1.897.
    
        The rest of the code, even when it was initially added as
        098c568fea06a7138cc8ef8e6e377ce27fbbc72f uses $addr->address, so this
        improves consistency while also fixing the bug.
    
    Fixes: I#31712

diff --git a/share/html/Helpers/PreviewScrips b/share/html/Helpers/PreviewScrips
index b44e9fc..0aa65b9 100644
--- a/share/html/Helpers/PreviewScrips
+++ b/share/html/Helpers/PreviewScrips
@@ -112,7 +112,7 @@ $submitted{$_} = 1 for split /,/, $ARGS{TxnRecipients};
 %                 $recips{$addr->address}++;
                   <b><%loc($type)%></b>:
 %                 my $show_checkbox = 1;
-%                 if ( grep {$_ eq $addr} @{$action->{NoSquelch}{$type}} ) {
+%                 if ( grep {$_ eq $addr->address} @{$action->{NoSquelch}{$type}} ) {
 %                     $show_checkbox = 0;
 %                 }
 

commit 3b0f4ba0a3ce86f9b665975096af1f2ae46ef687
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 19:06:59 2016 +0000

    Use CSS, rather than excluding HTML elements, for "hide unset fields"
    
        Which also allows administrators to customize the display of unset fields
        as desired, rather than simply "present or absent".
    
        This also adds a HideUnsetFields parameter to the ticket display page
        which can be used to override the user preference.
    
        This is the first step towards toggling hide/show unset fields with
        JavaScript.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index f01337c..25b3367 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -1425,6 +1425,8 @@ our %WHITELISTED_COMPONENT_ARGS = (
     '/Ticket/Update.html' => ['QuoteTransaction', 'Action', 'DefaultStatus'],
     # Action->Extract Article on a ticket's menu
     '/Articles/Article/ExtractIntoClass.html' => ['Ticket'],
+    # Only affects display
+    '/Ticket/Display.html' => ['HideUnsetFields'],
 );
 
 # Components which are blacklisted from automatic, argument-based whitelisting.
diff --git a/share/html/Elements/ShowLinks b/share/html/Elements/ShowLinks
index e7ab61d..afe55e1 100644
--- a/share/html/Elements/ShowLinks
+++ b/share/html/Elements/ShowLinks
@@ -47,8 +47,7 @@
 %# END BPS TAGGED BLOCK }}}
 <table>
 % for my $type (@display) {
-% if ( !RT->Config->Get('HideUnsetFieldsOnDisplay', $session{CurrentUser}) || $clone{$type} || $Object->$type->Count ) {
-  <tr>
+  <tr class="<%$type%><% $clone{$type} || $Object->$type->Count ? q{} : q{ unset-field}%>">
     <td class="labeltop">
       <& ShowRelationLabel, Object => $Object, Label => $labels{$type}.':', Relation => $type &>
 %     if ($clone{$type}) {
@@ -59,7 +58,7 @@
       <& ShowLinksOfType, Object => $Object, Type => $type, Recurse => ($type eq 'Members') &>
     </td>
   </tr>
-% } }
+% }
 
 % # Allow people to add more rows to the table
 % $m->callback( %ARGS );
diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index 5620e39..92d6fe9 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -57,7 +57,7 @@
 <& Elements/ShowDependencyStatus, Ticket => $TicketObj &>
 
 % $m->callback( %ARGS, Ticket => $TicketObj, Transactions => $transactions, Attachments => $attachments, CallbackName => 'BeforeShowSummary' );
-<div class="summary">
+<div class="summary unset-fields-container<% $HideUnsetFields ? ' unset-fields-hidden' : '' %>">
 <&| /Widgets/TitleBox, title => loc('Ticket metadata') &>
 <& /Ticket/Elements/ShowSummary,  Ticket => $TicketObj, Attachments => $attachments &>
 </&>
@@ -102,6 +102,7 @@
 $id => undef
 $TicketObj => undef
 $ShowHeaders => 0
+$HideUnsetFields => RT->Config->Get('HideUnsetFieldsOnDisplay', $session{CurrentUser})
 $ForceShowHistory => 0
 </%ARGS>
 
diff --git a/share/html/Ticket/Elements/ShowDates b/share/html/Ticket/Elements/ShowDates
index c476c79..f6a24ea 100644
--- a/share/html/Ticket/Elements/ShowDates
+++ b/share/html/Ticket/Elements/ShowDates
@@ -51,22 +51,17 @@
     <td class="value"><% $Ticket->CreatedObj->AsString %></td>
   </tr>
 % $m->callback( %ARGS, CallbackName => 'AfterCreated', TicketObj => $Ticket );
-% if ( !$hide_unset_fields || $Ticket->StartsObj->Unix ) {
-  <tr class="date starts">
+  <tr class="date starts<% $Ticket->StartsObj->Unix ? q{} : q{ unset-field}%>">
     <td class="label"><&|/l&>Starts</&>:</td>\
     <td class="value"><% $Ticket->StartsObj->AsString %></td>
   </tr>
 % $m->callback( %ARGS, CallbackName => 'AfterStarts', TicketObj => $Ticket );
-% }
-% if ( !$hide_unset_fields || $Ticket->StartedObj->Unix ) {
-  <tr class="date started">
+  <tr class="date started<% $Ticket->StartedObj->Unix ? q{} : q{ unset-field}%>">
     <td class="label"><&|/l&>Started</&>:</td>\
     <td class="value"><% $Ticket->StartedObj->AsString %></td>
   </tr>
 % $m->callback( %ARGS, CallbackName => 'AfterStarted', TicketObj => $Ticket );
-% }
-% if ( !$hide_unset_fields || $Ticket->ToldObj->Unix || $Ticket->CurrentUserHasRight('ModifyTicket') ) {
-  <tr class="date told">
+  <tr class="date told<% $Ticket->ToldObj->Unix || $Ticket->CurrentUserHasRight('ModifyTicket') ? q{} : q{ unset-field}%>">
     <td class="label">
 % if ( $Ticket->CurrentUserHasRight('ModifyTicket' ) ) {
 <a href="<% RT->Config->Get('WebPath') %>/Ticket/Display.html?id=<% $Ticket->id %>&Action=SetTold"><&|/l&>Last Contact</&></a>:
@@ -76,28 +71,23 @@
 </td><td class="value"><% $Ticket->ToldObj->AsString %></td>
   </tr>
 % $m->callback( %ARGS, CallbackName => 'AfterTold', TicketObj => $Ticket );
-% }
 
-% if ( !$hide_unset_fields || $Ticket->DueObj->Unix ) {
-  <tr class="date due">
+  <tr class="date due<% $Ticket->DueObj->Unix ? q{} : q{ unset-field}%>">
     <td class="label"><&|/l&>Due</&>:</td>\
 % my $due = $Ticket->DueObj;
-% if ( !$hide_unset_fields || $due && $due->IsSet && $due->Diff < 0 && $Ticket->QueueObj->IsActiveStatus($Ticket->Status) ) {
+% if ( $due && $due->IsSet && $due->Diff < 0 && $Ticket->QueueObj->IsActiveStatus($Ticket->Status) ) {
     <td class="value"><span class="overdue"><% $due->AsString  %></span></td>
 % } else {
     <td class="value"><% $due->AsString  %></td>
 % }
   </tr>
 % $m->callback( %ARGS, CallbackName => 'AfterDue', TicketObj => $Ticket );
-% }
 
-% if ( !$hide_unset_fields || $Ticket->ResolvedObj->Unix ) {
-  <tr class="date resolved">
+  <tr class="date resolved<% $Ticket->ResolvedObj->Unix ? q{} : q{ unset-field}%>">
     <td class="label"><&|/l&>Closed</&>:</td>\
     <td class="value"><% $Ticket->ResolvedObj->AsString  %></td>
   </tr>
 % $m->callback( %ARGS, CallbackName => 'AfterResolved', TicketObj => $Ticket );
-% }
 
   <tr class="date updated">
     <td class="label"><&|/l&>Updated</&>:</td>\
@@ -117,8 +107,6 @@ $Ticket => undef
 $UpdatedLink => 1
 </%ARGS>
 <%INIT>
-my $hide_unset_fields = RT->Config->Get('HideUnsetFieldsOnDisplay', $session{CurrentUser});
-
 if ($UpdatedLink and $Ticket) {
     my $txns = $Ticket->Transactions;
     $txns->OrderByCols(
diff --git a/share/html/Ticket/Elements/ShowPeople b/share/html/Ticket/Elements/ShowPeople
index bb50e8f..f31e907 100644
--- a/share/html/Ticket/Elements/ShowPeople
+++ b/share/html/Ticket/Elements/ShowPeople
@@ -46,7 +46,7 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <table>
-  <tr>
+  <tr class="owner">
     <td class="label"><&|/l&>Owner</&>:</td>
 % my $owner = $Ticket->OwnerObj;
     <td class="value"><& /Elements/ShowUser, User => $owner, Ticket => $Ticket &>
@@ -71,22 +71,18 @@
   </tr>
 % }
 
-  <tr>
+  <tr class="requestors">
     <td class="labeltop"><&|/l&>Requestors</&>:</td>
     <td class="value"><& ShowGroupMembers, Group => $Ticket->Requestors, Ticket => $Ticket &></td>
   </tr>
-% if ( !$hide_unset_fields || $Ticket->Cc->MembersObj->Count ) {
-  <tr>
+  <tr class="cc<% $Ticket->Cc->MembersObj->Count ? q{} : q{ unset-field}%>">
     <td class="labeltop"><&|/l&>Cc</&>:</td>
     <td class="value"><& ShowGroupMembers, Group => $Ticket->Cc, Ticket => $Ticket &></td>
   </tr>
-% }
-% if ( !$hide_unset_fields || $Ticket->AdminCc->MembersObj->Count ) {
-  <tr>
+  <tr class="admincc<% $Ticket->AdminCc->MembersObj->Count ? q{} : q{ unset-field}%>">
     <td class="labeltop"><&|/l&>AdminCc</&>:</td>
     <td class="value"><& ShowGroupMembers, Group => $Ticket->AdminCc, Ticket => $Ticket &></td>
   </tr>
-% }
 
 % my $multi_roles = $Ticket->QueueObj->CustomRoles;
 % $multi_roles->LimitToMultipleValue;
@@ -99,9 +95,6 @@
 
   <& /Ticket/Elements/ShowCustomFields, Ticket => $Ticket, Grouping => 'People', Table => 0 &>
 </table>
-<%INIT>
-my $hide_unset_fields = RT->Config->Get('HideUnsetFieldsOnDisplay', $session{CurrentUser});
-</%INIT>
 <%ARGS>
 $Ticket => undef
 </%ARGS>
diff --git a/share/static/css/base/ticket.css b/share/static/css/base/ticket.css
index cfe3768..0fec787 100644
--- a/share/static/css/base/ticket.css
+++ b/share/static/css/base/ticket.css
@@ -117,6 +117,9 @@ div.requestor-ticket-links {
     margin-bottom: 0;
 }
 
+.unset-fields-hidden .unset-field {
+    display: none !important;
+}
 
 /* textareas get to be bigger when we're in a table */
 tr.edit-custom-field.cftype-Text textarea,
diff --git a/t/web/ticket_display_unset_fields.t b/t/web/ticket_display_unset_fields.t
index 8556c26..09a7701 100644
--- a/t/web/ticket_display_unset_fields.t
+++ b/t/web/ticket_display_unset_fields.t
@@ -3,13 +3,13 @@ use warnings;
 
 use RT::Test tests => undef, config => 'Set( $HideUnsetFieldsOnDisplay, 1 );';
 
-my @link_labels = (
-    'Depends on',
-    'Depended on by',
-    'Parents',
-    'Children',
-    'Refers to',
-    'Referred to by',
+my @link_classes = qw(
+    DependsOn
+    DependedOnBy
+    MemberOf
+    Members
+    RefersTo
+    ReferredToBy
 );
 
 my $foo = RT::Test->create_ticket(
@@ -40,23 +40,23 @@ diag "test with root";
     $m->login;
     $m->goto_ticket( $foo->id );
 
-    for my $label (qw/Starts Started Closed Cc AdminCc/) {
-        $m->content_lacks( "$label:", "lacks $label as value is unset" );
+    my $dom = $m->dom;
+
+    for my $class (qw/starts started due resolved cc admincc/) {
+        is $dom->find(qq{tr.$class.unset-field})->size, 1, "found unset $class";
     }
 
-    # there is one Due: in reminder
-    $m->content_unlike( qr/Due:.*Due:/s, "lacks Due as value is unset" );
+    is $dom->find(qq{tr.told:not(.unset-field)})->size, 1, "has Told as root can modify it";
 
-    $m->content_contains( "Last Contact", "has Told as root can set it" );
-    for my $label (@link_labels) {
-        $m->content_contains( "$label:", "has $label as root can create" );
+    for my $class (@link_classes) {
+        is $dom->find(qq{tr.$class:not(.unset-field)})->size, 1, "has $class as root can create";
     }
 
     $m->goto_ticket( $bar->id );
-    for my $label (qw/Starts Started Closed Cc AdminCc/) {
-        $m->content_contains( "$label:", "has $label as value is set" );
+    $dom = $m->dom;
+    for my $class (qw/starts started due resolved cc admincc/) {
+        is $dom->find(qq{tr.$class:not(.unset-field)})->size, 1, "has $class as value is set";
     }
-    $m->content_like( qr/Due:.*Due:/s, "has Due as value is set" );
 }
 
 diag "test without ModifyTicket right";
@@ -66,13 +66,15 @@ diag "test without ModifyTicket right";
     RT::Test->set_rights( Principal => $user, Right => ['ShowTicket'] );
     $m->login( 'foo', 'password', logout => 1 );
     $m->goto_ticket( $foo->id );
-    $m->content_lacks( "Last Contact", "lacks Told as it is unset" );
-    for my $label ( @link_labels ) {
-        $m->content_lacks( "$label:", "lacks $label as it is unset" );
+    my $dom = $m->dom;
+    is $dom->find(qq{tr.told.unset-field})->size, 1, "lacks Told as it is unset and user has no modify right";
+    for my $class ( @link_classes ) {
+        is $dom->find(qq{tr.$class.unset-field})->size, 1, "lacks $class as it is unset and user has no modify right";
     }
 
     $m->goto_ticket( $bar->id );
-    $m->content_contains( "Depends on:", "has Depends on as it is set" );
+    $dom = $m->dom;
+    is $dom->find(qq{tr.DependsOn:not(.unset-field)})->size, 1, "has Depends on as it is set";
 }
 
 undef $m;

commit d7015116fa685b19c895bf30396b45b6d33b6ce8
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 19:08:26 2016 +0000

    Allow user to toggle "hide unset fields" with a click
    
    Fixes: I#31523

diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index 92d6fe9..bc709df 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -57,8 +57,19 @@
 <& Elements/ShowDependencyStatus, Ticket => $TicketObj &>
 
 % $m->callback( %ARGS, Ticket => $TicketObj, Transactions => $transactions, Attachments => $attachments, CallbackName => 'BeforeShowSummary' );
+
+<%PERL>
+my $show_all  = $m->interp->apply_escapes( loc("Show unset fields"), 'j' );
+my $show_html = $m->interp->apply_escapes( loc("Show unset fields"), 'h' );
+my $hide_all  = $m->interp->apply_escapes( loc("Hide unset fields"), 'j' );
+my $hide_html = $m->interp->apply_escapes( loc("Hide unset fields"), 'h' );
+my $initial_label = $HideUnsetFields ? $show_html : $hide_html;
+
+my $titleright = qq{<a href="#" onclick="return toggle_hide_unset(this, $show_all, $hide_all)">$initial_label</a>};
+</%PERL>
+
 <div class="summary unset-fields-container<% $HideUnsetFields ? ' unset-fields-hidden' : '' %>">
-<&| /Widgets/TitleBox, title => loc('Ticket metadata') &>
+<&| /Widgets/TitleBox, title => loc('Ticket metadata'), titleright_raw => $titleright &>
 <& /Ticket/Elements/ShowSummary,  Ticket => $TicketObj, Attachments => $attachments &>
 </&>
 </div>
diff --git a/share/static/js/util.js b/share/static/js/util.js
index 45b3948..3471cd0 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -565,3 +565,17 @@ function scrollToJQueryObject(obj) {
     }
 }
 
+function toggle_hide_unset(e, showmsg, hidemsg) {
+    var link      = jQuery(e);
+    var container = link.closest(".unset-fields-container");
+    container.toggleClass('unset-fields-hidden');
+
+    if (container.hasClass('unset-fields-hidden')) {
+        link.text(showmsg);
+    }
+    else {
+        link.text(hidemsg);
+    }
+
+    return false;
+}

commit 0c111ccfb10a40d64d2fd3ead726bd04cb075741
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 19:10:38 2016 +0000

    Allow hide/show unset fields link to function without JS

diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index bc709df..1ae3ef5 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -65,7 +65,10 @@ my $hide_all  = $m->interp->apply_escapes( loc("Hide unset fields"), 'j' );
 my $hide_html = $m->interp->apply_escapes( loc("Hide unset fields"), 'h' );
 my $initial_label = $HideUnsetFields ? $show_html : $hide_html;
 
-my $titleright = qq{<a href="#" onclick="return toggle_hide_unset(this, $show_all, $hide_all)">$initial_label</a>};
+my $url = "?HideUnsetFields=" . ($HideUnsetFields ? 0 : 1) . ";id=$id";
+my $url_html = $m->interp->apply_escapes($url, 'h');
+
+my $titleright = qq{<a href="$url_html" onclick="return toggle_hide_unset(this, $show_all, $hide_all)">$initial_label</a>};
 </%PERL>
 
 <div class="summary unset-fields-container<% $HideUnsetFields ? ' unset-fields-hidden' : '' %>">

commit 2f9d771b186a04c203813bfa2a1a940b79f7b4c7
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 19:15:10 2016 +0000

    Use data-* attributes rather than JavaScript function parameters
    
        Markup is preferable over code!

diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index 1ae3ef5..2ee8382 100644
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -59,16 +59,14 @@
 % $m->callback( %ARGS, Ticket => $TicketObj, Transactions => $transactions, Attachments => $attachments, CallbackName => 'BeforeShowSummary' );
 
 <%PERL>
-my $show_all  = $m->interp->apply_escapes( loc("Show unset fields"), 'j' );
-my $show_html = $m->interp->apply_escapes( loc("Show unset fields"), 'h' );
-my $hide_all  = $m->interp->apply_escapes( loc("Hide unset fields"), 'j' );
-my $hide_html = $m->interp->apply_escapes( loc("Hide unset fields"), 'h' );
-my $initial_label = $HideUnsetFields ? $show_html : $hide_html;
+my $show_label    = $m->interp->apply_escapes( loc("Show unset fields"), 'h' );
+my $hide_label    = $m->interp->apply_escapes( loc("Hide unset fields"), 'h' );
+my $initial_label = $HideUnsetFields ? $show_label : $hide_label;
 
 my $url = "?HideUnsetFields=" . ($HideUnsetFields ? 0 : 1) . ";id=$id";
 my $url_html = $m->interp->apply_escapes($url, 'h');
 
-my $titleright = qq{<a href="$url_html" onclick="return toggle_hide_unset(this, $show_all, $hide_all)">$initial_label</a>};
+my $titleright = qq{<a href="$url_html" data-show-label="$show_label" data-hide-label="$hide_label" onclick="return toggle_hide_unset(this)">$initial_label</a>};
 </%PERL>
 
 <div class="summary unset-fields-container<% $HideUnsetFields ? ' unset-fields-hidden' : '' %>">
diff --git a/share/static/js/util.js b/share/static/js/util.js
index 3471cd0..433580c 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -565,16 +565,16 @@ function scrollToJQueryObject(obj) {
     }
 }
 
-function toggle_hide_unset(e, showmsg, hidemsg) {
+function toggle_hide_unset(e) {
     var link      = jQuery(e);
     var container = link.closest(".unset-fields-container");
     container.toggleClass('unset-fields-hidden');
 
     if (container.hasClass('unset-fields-hidden')) {
-        link.text(showmsg);
+        link.text(link.data('show-label'));
     }
     else {
-        link.text(hidemsg);
+        link.text(link.data('hide-label'));
     }
 
     return false;

commit ff384323ad57a796806688223da9fc745e1aec2d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 2 19:49:19 2016 +0000

    Move create-related-ticket-in-queue form out of the table
    
        Browsers render this poorly when there are no existant links.
    
    Fixes: I#31871

diff --git a/share/html/Elements/ShowLinks b/share/html/Elements/ShowLinks
index e7ab61d..8558bd9 100644
--- a/share/html/Elements/ShowLinks
+++ b/share/html/Elements/ShowLinks
@@ -65,27 +65,24 @@
 % $m->callback( %ARGS );
   <& /Elements/ShowCustomFields, Object => $Object, Grouping => 'Links', Table => 0 &>
 
+</table>
+
 % if ($Object->isa('RT::Ticket')) {
-  <tr>
-    <td colspan=2>
-      <form action="<% RT->Config->Get('WebPath') ."/Helpers/SpawnLinkedTicket" %>" name="SpawnLinkedTicket">
-        <input type="hidden" name="CloneTicket" value="<% $Object->id %>">
-        <input type="submit" value="<&|/l&>Create</&>" name="SpawnLinkedTicket">
-        <select name="LinkType">
-          <option value="DependsOn-new"><% loc('Depends on') %></option>
-          <option value="new-DependsOn"><% loc('Depended on by') %></option>
-          <option value="MemberOf-new"><% loc('Parents') %></option>
-          <option value="new-MemberOf"><% loc('Children') %></option>
-          <option value="RefersTo-new"><% loc('Refers to') %></option>
-          <option value="new-RefersTo"><% loc('Referred to by') %></option>
-        </select>
-        <&|/l&>Ticket in</&>
-        <& /Elements/SelectQueue, ShowNullOption => 0, Name => 'CloneQueue' &>
-      </form>
-    </td>
-  </tr>
+  <form action="<% RT->Config->Get('WebPath') ."/Helpers/SpawnLinkedTicket" %>" name="SpawnLinkedTicket">
+    <input type="hidden" name="CloneTicket" value="<% $Object->id %>">
+    <input type="submit" value="<&|/l&>Create</&>" name="SpawnLinkedTicket">
+    <select name="LinkType">
+      <option value="DependsOn-new"><% loc('Depends on') %></option>
+      <option value="new-DependsOn"><% loc('Depended on by') %></option>
+      <option value="MemberOf-new"><% loc('Parents') %></option>
+      <option value="new-MemberOf"><% loc('Children') %></option>
+      <option value="RefersTo-new"><% loc('Refers to') %></option>
+      <option value="new-RefersTo"><% loc('Referred to by') %></option>
+    </select>
+    <&|/l&>Ticket in</&>
+    <& /Elements/SelectQueue, ShowNullOption => 0, Name => 'CloneQueue' &>
+  </form>
 % }
-</table>
 <%INIT>
 my @display = qw(DependsOn DependedOnBy MemberOf Members RefersTo ReferredToBy);
 $m->callback( %ARGS, CallbackName => 'ChangeDisplay', display => \@display );

commit 244bf30a1e7dbd440b09612ed322d4bdac8b4e0f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 2 19:56:45 2016 +0000

    Hide unset custom fields according to the user preference
    
        This obsoletes RT::Extension::CustomField::HideEmptyValues
    
    Fixes: I#31524

diff --git a/share/html/Elements/ShowCustomFields b/share/html/Elements/ShowCustomFields
index 1f15b69..07b828e 100644
--- a/share/html/Elements/ShowCustomFields
+++ b/share/html/Elements/ShowCustomFields
@@ -54,7 +54,7 @@
 % my $Values = $Object->CustomFieldValues( $CustomField->Id );
 % my $count = $Values->Count;
 % next if $HideEmpty and not $count;
-  <tr id="CF-<%$CustomField->id%>-ShowRow">
+  <tr class="custom-field custom-field-<% $CustomField->id%><% $count ? q{} : q{ unset-field}%>" id="CF-<%$CustomField->id%>-ShowRow">
     <td class="label"><% $CustomField->Name %>:</td>
     <td class="value<% $count ? '' : ' no-value' %>">
 % unless ( $count ) {

commit 83080567fd4a29bebabd1467cf31d6b7cc537783
Author: rachelkelly <rachel at bestpractical.com>
Date:   Tue May 3 10:05:24 2016 -0700

    Add Pod::Select to dependencies for shredder
    
    Pod::Select was removed from Perl core (5.18 and higher), so in order to
    continue to use Pod::Select we explicitly include it.
    
    Fixes: I#31873

diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in
index b930e47..326686c 100644
--- a/sbin/rt-test-dependencies.in
+++ b/sbin/rt-test-dependencies.in
@@ -251,6 +251,7 @@ Net::CIDR
 Net::IP
 Plack 1.0002
 Plack::Handler::Starlet
+Pod::Select
 Regexp::Common
 Regexp::Common::net::CIDR
 Regexp::IPv6

commit 6204e5a7b928eedb77f43a1cc736d2498a343599
Merge: f1f76c7 8308056
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 4 14:07:38 2016 -0400

    Merge branch '4.4/pod-select-dep' into 4.4-trunk


commit e03aa326c9688397f6f0e3975dcfa744a15ebfaa
Author: rachelkelly <rachel at bestpractical.com>
Date:   Thu May 5 05:22:33 2016 +0000

    Swap order of transition display for two transitions
    
        Transitions "stalled" and "deleted" had the transition order
        between "resolved" and "rejected" swapped.
    
    Fixed: #I31562

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 8fc8982..e5a5d1c 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -3036,10 +3036,10 @@ Set(%Lifecycles,
             # from   => [ to list ],
             new      => [qw(    open stalled resolved rejected deleted)],
             open     => [qw(new      stalled resolved rejected deleted)],
-            stalled  => [qw(new open         rejected resolved deleted)],
+            stalled  => [qw(new open         resolved rejected deleted)],
             resolved => [qw(new open stalled          rejected deleted)],
             rejected => [qw(new open stalled resolved          deleted)],
-            deleted  => [qw(new open stalled rejected resolved        )],
+            deleted  => [qw(new open stalled resolved rejected        )],
         },
         rights => {
             '* -> deleted'  => 'DeleteTicket',

commit b0f432d6b418efda3a4c849ac9a316f3a7311364
Author: rachelkelly <rachel at bestpractical.com>
Date:   Thu May 5 07:05:25 2016 +0000

    Fix failing FastCGI tests by changing how timestamps are removed from quote header
    
        t/web/signatures.t, introduced in
        9df4d3387e22754f160a5aba91f2e25381309b45, overrode the
        RT::Transaction::QuoteHeader method to replace the quote header text "On
        $date at $time, $person wrote:" with "Someone wrote:" to simplify the
        test cases. This solution works in most development environments but
        fails in a client/server environment like FastCGI. By the time this
        method override happens, the server and client processes have already
        forked, so it's simply too late for the test file to redefine methods in
        the server. This led to the test failures because the server output the
        stock "On $date at $time, $person wrote:" instead of the
        simplified-for-tests "Someone wrote:" header.
    
        This commit fixes the test by removing the method override and instead
        replacing the "On $date at $time, $person wrote:" with a regex
        substitution on the output. In this way is not only agnostic to the test
        environment, it is also more insulated from future changes (such as
        RT::Transaction method QuoteHeader being refactored away).

diff --git a/t/web/signatures.t b/t/web/signatures.t
index becc1f9..919e1e6 100644
--- a/t/web/signatures.t
+++ b/t/web/signatures.t
@@ -4,12 +4,6 @@ use warnings;
 use RT::Test tests => undef;
 use HTML::Entities qw/decode_entities/;
 
-# Remove the timestamp from the quote header
-{
-    no warnings 'redefine';
-    *RT::Transaction::QuoteHeader = sub { "Someone wrote:" };
-}
-
 my ($baseurl, $m) = RT::Test->started_ok;
 ok( $m->login, 'logged in' );
 
@@ -57,6 +51,9 @@ sub template_is {
     $display =~ s/^$/./mg;
     $display =~ s/([ ]+)$/$1\$/mg;
 
+    # Remove the timestamp from the quote header
+    $display =~ s/On \w\w\w \w\w\w+ \d\d \d\d:\d\d:\d\d \d\d\d\d, \w+ wrote:/Someone wrote:/;
+
     is($display, $expected, "Content matches expected");
 
     my $trim = RT::Interface::Web::StripContent(

commit e7f416cbde094538744b51ba564c8872b07ecb5c
Merge: 391c861 b0f432d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 11 19:43:52 2016 +0000

    Merge branch '4.4/signature-spacing-test-fix' into 4.4-trunk


commit bf2e9c23af57538a9fe02f8d7176779d6b303bd7
Merge: e7f416c ff38432
Author: rachelkelly <rachel at bestpractical.com>
Date:   Thu May 5 06:35:42 2016 +0000

    Merge branch '4.4/links-table-layout' into 4.4-trunk


commit 6329df1ac01288a3dc949418c87bb0f18a864e74
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Oct 24 23:24:36 2012 +0800

    remove the unnecessary "\" because browsers ignore the first newline
    
    see also #21152

diff --git a/share/html/Elements/MessageBox b/share/html/Elements/MessageBox
index df858b1..9e98f8b 100644
--- a/share/html/Elements/MessageBox
+++ b/share/html/Elements/MessageBox
@@ -45,7 +45,7 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<textarea autocomplete="off" class="messagebox <% $Type eq 'text/html' ? 'richtext' : '' %>" <% $width_attr %>="<% $Width %>" rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">\
+<textarea autocomplete="off" class="messagebox <% $Type eq 'text/html' ? 'richtext' : '' %>" <% $width_attr %>="<% $Width %>" rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">
 % $m->comp('/Articles/Elements/IncludeArticle', %ARGS) if $IncludeArticle;
 % $m->callback( %ARGS, SignatureRef => \$signature, DefaultRef => \$Default, MessageRef => $message );
 % if (RT->Config->Get("SignatureAboveQuote", $session{'CurrentUser'})) {

commit 9df4d3387e22754f160a5aba91f2e25381309b45
Author: Alex Vandiver <alex at chmrr.net>
Date:   Fri May 6 01:59:27 2016 -0700

    Tweak spaces around signature to provide useful default whitespace
    
    Most signatures don't have trailing newlines, additional newlines are
    necessary after the signature to give whitespace before showing the
    quote.  Additionally, having a blank line (or two) before the "-- \n"
    means there's an obvious place to click and start typing.  With
    signature-before-quote, this now gives (all examples use "." for a blank
    line):
        .
        .
         --
         Foo
         Bar
        .
         On Oct 18, you wrote:
         > ...
    
    This provides a blank line to click on and begin to type, as well as a
    blank line thereafter to separate the quote from the signature.
    
    With the standard signature-after setting, this now gives:
         On Oct 18, you wrote:
         > ...
        .
        .
        .
         --
         Foo
         Bar
    
    ...providing three blank lines in the middle.  This allows the user to
    click on the second one and start typing, but leave whitespace before
    and after their response.  Finally, if there's nothing to quote,
    regardless of the preference:
        .
        .
         --
         Foo
         Bar
    
    ..with a similar click-on-the-first-line use case as described above.
    All of the same behavior is preserved when HTML editing is enabled.

diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index 372deeb..774fbe3 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -400,12 +400,12 @@ sub Content {
                 . $self->QuoteHeader
                 . '<br /><blockquote class="gmail_quote" type="cite">'
                 . $content
-                . '</blockquote></div><br /><br />';
+                . '</blockquote></div>';
         } else {
             $content = $self->ApplyQuoteWrap(content => $content,
                                              cols    => $args{'Wrap'} );
 
-            $content = $self->QuoteHeader . "\n$content\n\n";
+            $content = $self->QuoteHeader . "\n$content";
         }
     }
 
diff --git a/share/html/Elements/MessageBox b/share/html/Elements/MessageBox
index 9e98f8b..78753ab 100644
--- a/share/html/Elements/MessageBox
+++ b/share/html/Elements/MessageBox
@@ -74,9 +74,10 @@ if ( $QuoteTransaction ) {
     $message = $transaction->Content( Quote => 1, Type  => $Type );
 }
 
-my $signature = '';
-if ( $IncludeSignature and my $text = $session{'CurrentUser'}->UserObj->Signature ) {
-    $signature = "-- \n". $text;
+my $signature = $session{'CurrentUser'}->UserObj->Signature // "";
+if ( $IncludeSignature and $signature =~ /\S/ ) {
+    $signature =~ s/\n*$//;
+
     if ($Type eq 'text/html') {
         $signature =~ s/&/&/g;
         $signature =~ s/</</g;
@@ -84,8 +85,20 @@ if ( $IncludeSignature and my $text = $session{'CurrentUser'}->UserObj->Signatur
         $signature =~ s/"/"/g;  # "//;
         $signature =~ s/'/'/g;   # '//;
         $signature =~ s{\n}{<br />}g;
-        $signature = "<p>$signature</p>";
+        $signature = "<br /><p>-- <br />$signature</p>";
+    } else {
+        $signature = "\n\n-- \n". $signature . "\n";
+    }
+
+    if ($message =~ /\S/) {
+        if (RT->Config->Get('SignatureAboveQuote', $session{CurrentUser})) {
+            $signature .= $Type eq 'text/html' ? "<br />" : "\n";
+        } else {
+            $signature = ($Type eq 'text/html' ? "" : "\n") . $signature;
+        }
     }
+} else {
+    $signature = '';
 }
 
 # wrap="something" seems to really break IE + richtext
diff --git a/t/web/search_bulk_update_links.t b/t/web/search_bulk_update_links.t
index d9b477e..c272345 100644
--- a/t/web/search_bulk_update_links.t
+++ b/t/web/search_bulk_update_links.t
@@ -79,11 +79,13 @@ $m->content_lacks( 'DeleteLink--', 'no delete link stuff' );
 $m->form_name('BulkUpdate');
 my @fields = qw/Owner AddRequestor DeleteRequestor AddCc DeleteCc AddAdminCc
 DeleteAdminCc Subject Priority Queue Status Starts_Date Told_Date Due_Date
-UpdateSubject UpdateContent/;
+UpdateSubject/;
 for my $field ( @fields ) {
     is( $m->value($field), '', "default $field is empty" );
 }
 
+like( $m->value('UpdateContent'), qr/^\s*$/, "default UpdateContent is effectively empty" );
+
 # test DependsOn, MemberOf and RefersTo
 $m->submit_form(
     form_name => 'BulkUpdate',
diff --git a/t/web/signatures.t b/t/web/signatures.t
new file mode 100644
index 0000000..becc1f9
--- /dev/null
+++ b/t/web/signatures.t
@@ -0,0 +1,162 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+use HTML::Entities qw/decode_entities/;
+
+# Remove the timestamp from the quote header
+{
+    no warnings 'redefine';
+    *RT::Transaction::QuoteHeader = sub { "Someone wrote:" };
+}
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok( $m->login, 'logged in' );
+
+my $root = RT::Test->load_or_create_user( Name => 'root' );
+my ($ok) = $root->SetSignature("Signature one\nSignature two\n");
+ok($ok, "Set signature");
+
+my $t = RT::Test->create_ticket(
+    Queue   => 'General',
+    Subject => 'Signature quoting',
+    Content => "First\nSecond\nThird\n",
+);
+
+my $initial = $t->Transactions->First->id;
+
+sub template_is {
+    my (%args) = (
+        HTML        => 0,
+        Quote       => 0,
+        BeforeQuote => 0,
+    );
+    my $expected = pop(@_);
+    $args{$_}++ for @_;
+
+    my $prefs = $root->Preferences($RT::System);
+    $prefs->{SignatureAboveQuote} = $args{BeforeQuote};
+    $prefs->{MessageBoxRichText}  = $args{HTML};
+    ($ok) = $root->SetPreferences($RT::System, $prefs);
+    ok($ok, "Preferences updated");
+
+    my $url = "/Ticket/Update.html?id=" . $t->id;
+    $url .= "&QuoteTransaction=$initial" if $args{Quote};
+    $m->get_ok($url);
+
+    $m->form_name('TicketUpdate');
+    my $value = $m->value("UpdateContent");
+
+    # Work around a bug in Mechanize, wherein it thinks newlines
+    # following textareas are significant -- browsers do not.
+    $value =~ s/^\n//;
+
+    # For ease of interpretation, replace blank lines with dots, and
+    # put a $ after trailing whitespace.
+    my $display = $value;
+    $display =~ s/^$/./mg;
+    $display =~ s/([ ]+)$/$1\$/mg;
+
+    is($display, $expected, "Content matches expected");
+
+    my $trim = RT::Interface::Web::StripContent(
+        CurrentUser    => RT::CurrentUser->new($root),
+        Content        => $value,
+        ContentType    => $args{HTML} ? "text/html" : "text/plain",
+        StripSignature => 1,
+    );
+    if ($args{Quote}) {
+        ok($trim, "Counts as not empty");
+    } else {
+        is($trim, '', "Counts as empty");
+    }
+}
+
+
+### Text
+
+subtest "Non-HTML, no reply" => sub {
+    template_is(<<'EOT') };
+.
+.
+-- $
+Signature one
+Signature two
+EOT
+
+
+subtest "Non-HTML, no reply, before quote (which is irrelevant)" => sub {
+    template_is(qw/BeforeQuote/, <<'EOT') };
+.
+.
+-- $
+Signature one
+Signature two
+EOT
+
+subtest "Non-HTML, reply" => sub {
+    template_is(qw/Quote/, <<'EOT') };
+Someone wrote:
+> First
+> Second
+> Third
+.
+.
+.
+-- $
+Signature one
+Signature two
+EOT
+
+subtest "Non-HTML, reply, before quote" => sub {
+    template_is(qw/Quote BeforeQuote/, <<'EOT') };
+.
+.
+-- $
+Signature one
+Signature two
+.
+Someone wrote:
+> First
+> Second
+> Third
+EOT
+
+
+
+### HTML
+
+my $quote = '<div class="gmail_quote">Someone wrote:<br />'
+    .'<blockquote class="gmail_quote" type="cite">'
+    .'<pre style="white-space: pre-wrap; font-family: monospace;">'
+    ."First\nSecond\nThird\n"
+    .'</pre></blockquote></div>';
+
+subtest "HTML, no reply" => sub {
+    template_is(
+        qw/HTML/,
+        '<br /><p>-- <br />Signature one<br />Signature two</p>',
+    ) };
+
+subtest "HTML, no reply, before quote (which is irrelevant)" => sub {
+    template_is(
+        qw/HTML BeforeQuote/,
+        '<br /><p>-- <br />Signature one<br />Signature two</p>',
+    ) };
+
+subtest "HTML, reply" => sub {
+    template_is(
+        qw/HTML Quote/,
+        $quote.'<br /><p>-- <br />Signature one<br />Signature two</p>',
+    ) };
+
+subtest "HTML, reply, before quote" => sub {
+    template_is(
+        qw/HTML Quote BeforeQuote/,
+        '<br /><p>-- <br />Signature one<br />Signature two</p>'
+            . "<br />" . $quote,
+    ) };
+
+
+undef $m;
+done_testing;

commit 391c86194e3bcb07e682878cd163e0b9b7cd69cf
Merge: 8108f9f 9df4d33
Author: rachelkelly <rachel at bestpractical.com>
Date:   Thu May 5 08:53:53 2016 +0000

    Merge branch '4.4/signature-spacing' into 4.4-trunk


commit 100b9c755a056b282e88a615d112664bc6f68e24
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu May 5 20:06:58 2016 +0000

    Change ticket timer from ticking seconds to measuring duration
    
    There are several problems with incrementing a counter every second.  It
    boils down to the fact that setInterval is certainly not a guarantee,
    it's more of a best effort. In particular, on mobile devices, browsers
    will not waste precious battery and CPU cycles to run timers for tabs in
    the background. So when you come back to your ticket timer after minutes
    or hours of work, the browser will not have been calling setInterval,
    causing you to lose all that time worked.
    
    Rather than incrementing a seconds counter every second, we instead
    track when the last unpause happened, and take its difference from the
    current time. If the user pauses and unpauses repeatedly, we also track
    "committed seconds". Any time we need the duration (for display in the
    UI or for submitting as time worked) we sum the two.  The result is that
    when the browser resumes calling setInterval after being backgrounded
    away for hours, that duration will be accurate, since it's directly
    measuring time passed rather than relying on setInterval having been
    called regularly. setInterval is now used _only_ for updating the
    display, so it can be called as frequently or rarely as the browser
    decides.
    
    Fixes: I#31707

diff --git a/share/html/Helpers/TicketTimer b/share/html/Helpers/TicketTimer
index c0f58c2..5774cf1 100644
--- a/share/html/Helpers/TicketTimer
+++ b/share/html/Helpers/TicketTimer
@@ -65,7 +65,26 @@ my $submit_url = RT->Config->Get('WebPath') . '/Helpers/AddTimeWorked';
 <script type="text/javascript">
 jQuery( function() {
     var Interval;
-    var Seconds = 0;
+
+    // LastUnpause tracks when the current timer started. Then we render
+    // (current time - LastUnpause). This is more reliable than a timer
+    // that ticks up every second. For example, if JavaScript is temporarily
+    // paused (such as being on a background browser tab on a mobile device),
+    // the seconds ticker doesn't run. When the timer is paused, LastUnpaused
+    // will be a false value.
+    var LastUnpause = (new Date).getTime() / 1000;
+
+    // How many seconds has passed since the current timer started?
+    var CurrentSeconds = function () {
+        if (!LastUnpause) return 0;
+        return Math.floor(((new Date).getTime() / 1000) - LastUnpause);
+    };
+
+    // CommittedSeconds tracks how long we've "committed" time, which is
+    // different from when the timer was initially launched, due to unpausing.
+    // Every time we pause, we add (current time - LastUnpause) to
+    // CommittedSeconds.
+    var CommittedSeconds = 0;
 
     var readout = jQuery('.readout');
     var playpause = jQuery('.playpause');
@@ -90,8 +109,7 @@ jQuery( function() {
     };
 
     var tick = function () {
-        Seconds++;
-        renderReadout(Seconds);
+        renderReadout(CommittedSeconds + CurrentSeconds());
     };
 
     jQuery('.playpause').click(function () {
@@ -99,6 +117,8 @@ jQuery( function() {
             // pause
             clearInterval(Interval);
             Interval = false;
+            CommittedSeconds += CurrentSeconds();
+            LastUnpause = false;
             playpause_img.attr('src', <% $unpause_img |n,j %>);
             playpause_img.attr('alt', unpause_alt);
             playpause_img.attr('title', unpause_alt);
@@ -106,6 +126,7 @@ jQuery( function() {
         else {
             // unpause
             Interval = setInterval(tick, 1000);
+            LastUnpause = new Date().getTime() / 1000;
             playpause_img.attr('src', <% $pause_img |n,j %>);
             playpause_img.attr('alt', pause_alt);
             playpause_img.attr('title', pause_alt);
@@ -116,16 +137,17 @@ jQuery( function() {
     jQuery('.submit-time').click(function () {
         clearInterval(Interval);
         jQuery('.control-line a').hide();
+        CommittedSeconds += CurrentSeconds();
 
         var payload = {
             id: <% $Ticket->id %>,
-            seconds: Seconds
+            seconds: CommittedSeconds
         };
 
         readout.text('<% loc("Submitting") %>');
 
         var renderSubmitError = function (reason) {
-            renderReadout(Seconds);
+            renderReadout(CommittedSeconds);
             jQuery('.ticket-timer').addClass('error');
 
             // give the browser a chance to redraw the readout
@@ -161,8 +183,8 @@ jQuery( function() {
         return false;
     });
 
-    renderReadout(Seconds);
-    Interval = setInterval(tick, 1000);
+    tick();
+    Interval = setInterval(tick, 500);
 });
 </script>
 

commit ef2beb5f8dc097d0280de9137468fc878417fb05
Author: Rachel Kelly <rachel at bestpractical.com>
Date:   Tue May 3 22:30:34 2016 +0000

    Add signature-above-quote option
    
    Fixes: I#31877

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 6852ff2..8fc8982 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2474,6 +2474,16 @@ minutes.  Note that this only effects entry, not display.
 
 Set($DefaultTimeUnitsToHours, 0);
 
+=item C<$SignatureAboveQuote>
+
+By default RT places the signature at the bottom of the quoted text in
+the message box for ticket replies.  Set this to 1 to place the signature
+above the quoted text.
+
+=cut
+
+Set($SignatureAboveQuote, 0);
+
 =item C<$TimeInICal>
 
 By default, events in the iCal feed on the ticket search page
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 2aad19c..bbc5694 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -321,6 +321,15 @@ our %META;
             Hints       => 'Only for entry, not display', #loc
         },
     },
+    SignatureAboveQuote => {
+        Section         => 'Ticket composition', #loc
+        Overridable     => 1,
+        SortOrder       => 10,
+        Widget          => '/Widgets/Form/Boolean',
+        WidgetArguments => {
+            Description => 'Place signature above quote', #loc
+        },
+    },
     SearchResultsRefreshInterval => {
         Section         => 'General',                       #loc
         Overridable     => 1,
diff --git a/share/html/Elements/MessageBox b/share/html/Elements/MessageBox
index b61f1e7..df858b1 100644
--- a/share/html/Elements/MessageBox
+++ b/share/html/Elements/MessageBox
@@ -48,7 +48,13 @@
 <textarea autocomplete="off" class="messagebox <% $Type eq 'text/html' ? 'richtext' : '' %>" <% $width_attr %>="<% $Width %>" rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">\
 % $m->comp('/Articles/Elements/IncludeArticle', %ARGS) if $IncludeArticle;
 % $m->callback( %ARGS, SignatureRef => \$signature, DefaultRef => \$Default, MessageRef => $message );
+% if (RT->Config->Get("SignatureAboveQuote", $session{'CurrentUser'})) {
+<% $Default || '' %><% $signature %><% $message %></textarea>
+% }
+% else {
 <% $Default || '' %><% $message %><% $signature %></textarea>
+% }
+
 % $m->callback( %ARGS, CallbackName => 'AfterTextArea' );
 
 % if (!$SuppressAttachmentWarning) {

commit 15c9399ff3722998fa31cd81b8eab16dee88a7fd
Merge: 6204e5a ef2beb5
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu May 5 17:02:10 2016 -0400

    Merge branch '4.4/sig-move' into 4.4-trunk


commit 865dfbf4d09a7c6193d07ed0bafb3cc932a3c208
Author: rachelkelly <rachel at bestpractical.com>
Date:   Fri May 6 05:00:06 2016 +0000

    Remove two erroneous semicolons throwing warnings

diff --git a/share/static/css/base/assets.css b/share/static/css/base/assets.css
index 3526fa7..c09a8a7 100644
--- a/share/static/css/base/assets.css
+++ b/share/static/css/base/assets.css
@@ -104,7 +104,7 @@ body#comp-Asset-Search .collection-as-table td {
 }
 
 .asset-metadata {
-    padding-top: 2em; /* nav overflows this :( */;
+    padding-top: 2em; /* nav overflows this :( */
 }
 
 @media (max-width: 800px) {
@@ -118,7 +118,7 @@ body#comp-Asset-Search .collection-as-table td {
 @media (max-width: 600px) {
     .asset-metadata {
         padding-top: 6em;
-        /* nav overflows this: < */;
+        /* nav overflows this: < */
     }
 
     .asset-metadata>div {

commit 1a25f83b94ccd8fdf1c1be34c6b6da2ec2e0ca5f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 04:41:53 2016 +0000

    Filter out empty attachment names in SQL rather than Perl
    
        Cloning is necessary when ShowHistory is set to immediate,
        as the Attachments object is shared across this template and
        the transaction log; without ->Clone, all messages are filtered
        away.

diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index d9432b7..b6a9d85 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -95,11 +95,20 @@
 # then we need to find one
 $Attachments ||= $Ticket->Attachments;
 
-# XXX PERF: why doesn't this Limit on Filename to avoid fetching *all* the
-# attachments?
+# Avoid applying limits to this collection that may be used elsewhere
+# (e.g. transaction display)
+$Attachments = $Attachments->Clone;
+
+# Remember, each message in a transaction is an attachment; we only
+# want named attachments (real files)
+$Attachments->Limit(
+    FIELD    => 'Filename',
+    OPERATOR => '!=',
+    VALUE    => '',
+);
+
 my %documents;
 while ( my $attach = $Attachments->Next() ) {
-    next unless defined $attach->Filename && length $attach->Filename;
    unshift( @{ $documents{ $attach->Filename } }, $attach );
 }
 

commit 8b35ca835122e4d5335cf0d8ff31896b759eaa78
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 04:57:44 2016 +0000

    Add a way to display just the last N attachments

diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index b6a9d85..3ff586e 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -108,7 +108,20 @@ $Attachments->Limit(
 );
 
 my %documents;
+
+# if we're limiting the number of attachments, show only the most recent
+# if we're not limiting the number, order doesn't much matter
+if (defined($Count)) {
+    $Attachments->OrderByCols(
+        { FIELD => 'Created', ORDER => 'DESC' },
+        { FIELD => 'id', ORDER => 'DESC' },
+    );
+}
+
 while ( my $attach = $Attachments->Next() ) {
+   if (defined($Count) && --$Count < 0) {
+       last;
+   }
    unshift( @{ $documents{ $attach->Filename } }, $attach );
 }
 
@@ -120,6 +133,7 @@ $Attachments => undef
 $DisplayPath => $session{'CurrentUser'}->Privileged ? 'Ticket' : 'SelfService'
 $HideTitleBox => 0
 $Selectable => 0
+$Count => undef
 @Checked => ()
 </%ARGS>
 

commit 651cb49bf06abd129fb0922d41dad9120110f666
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:08:24 2016 +0000

    Display only latest 5 attachments, with an AJAX "Show all" link
    
    Fixes: I#31710

diff --git a/share/html/Helpers/TicketAttachments b/share/html/Helpers/TicketAttachments
new file mode 100644
index 0000000..2292a45
--- /dev/null
+++ b/share/html/Helpers/TicketAttachments
@@ -0,0 +1,63 @@
+%# 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 }}}
+<%ARGS>
+$id
+$Selectable => undef
+$Count => undef
+</%ARGS>
+<%INIT>
+my $TicketObj = RT::Ticket->new($session{'CurrentUser'});
+$TicketObj->Load($id);
+</%INIT>
+<& /Ticket/Elements/ShowAttachments,
+  Ticket       => $TicketObj,
+  HideTitleBox => 1,
+  Selectable   => $Selectable,
+  Count        => $Count,
+&>
+% $m->abort();
diff --git a/share/html/Ticket/Elements/AddAttachments b/share/html/Ticket/Elements/AddAttachments
index 34798e2..fb4ec23 100644
--- a/share/html/Ticket/Elements/AddAttachments
+++ b/share/html/Ticket/Elements/AddAttachments
@@ -172,6 +172,7 @@ jQuery( function() {
       Selectable   => 1,
       HideTitleBox => 1,
       Checked      => \@AttachExisting,
+      Count        => 5,
     &>
   </td>
 </tr>
diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index 3ff586e..f878381 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -52,6 +52,8 @@
         color => "#336699",
         hide_chrome => $HideTitleBox &>
 
+<div class="attachment-list">
+
 % foreach my $key (sort { lc($a) cmp lc($b) } keys %documents) {
 
 <%$key%><br />
@@ -85,6 +87,21 @@
 </ul>
 
 % }
+
+% if ($show_more) {
+<span class="show-more-link">
+% my %params = %ARGS;
+% delete $params{Ticket};
+% delete $params{Attachments};
+% delete $params{Count};
+
+% my $query = $m->comp('/Elements/QueryString', %params, id => $Ticket->id );
+% my $url   = RT->Config->Get('WebPath')."/Helpers/TicketAttachments?$query";
+<a href="#" onclick="jQuery(this).parent().text(<% loc('Loading...') | n,j%>).closest('.attachment-list').load(<% $url |n,j %>); return false;" ><% loc('Show all attachments') %></a>
+</span>
+% }
+
+</div>
 </&>
 
 % }
@@ -107,6 +124,7 @@ $Attachments->Limit(
     VALUE    => '',
 );
 
+my $show_more = 0;
 my %documents;
 
 # if we're limiting the number of attachments, show only the most recent
@@ -119,7 +137,9 @@ if (defined($Count)) {
 }
 
 while ( my $attach = $Attachments->Next() ) {
+   # display "show more" only when there will be more attachments
    if (defined($Count) && --$Count < 0) {
+       $show_more = 1;
        last;
    }
    unshift( @{ $documents{ $attach->Filename } }, $attach );
diff --git a/share/html/Ticket/Elements/ShowSummary b/share/html/Ticket/Elements/ShowSummary
index f891631..4183dab 100644
--- a/share/html/Ticket/Elements/ShowSummary
+++ b/share/html/Ticket/Elements/ShowSummary
@@ -64,7 +64,7 @@
         class => 'ticket-info-people',
     &><& /Ticket/Elements/ShowPeople, Ticket => $Ticket &></&>
 % $m->callback( %ARGS, CallbackName => 'AfterPeople' );
-    <& /Ticket/Elements/ShowAttachments, Ticket => $Ticket, Attachments => $Attachments &>
+    <& /Ticket/Elements/ShowAttachments, Ticket => $Ticket, Attachments => $Attachments, Count => 5 &>
 % $m->callback( %ARGS, CallbackName => 'AfterAttachments' );
     <& /Ticket/Elements/ShowRequestor, Ticket => $Ticket &>
 % $m->callback( %ARGS, CallbackName => 'LeftColumn' );

commit c453a8a0d43cb2c61069bcf4df1639492c54125f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:25:32 2016 +0000

    Preserve selected attachments when loading the rest in via ajax

diff --git a/share/html/Helpers/TicketAttachments b/share/html/Helpers/TicketAttachments
index 2292a45..c4de7e9 100644
--- a/share/html/Helpers/TicketAttachments
+++ b/share/html/Helpers/TicketAttachments
@@ -49,6 +49,7 @@
 $id
 $Selectable => undef
 $Count => undef
+ at AttachExisting => ()
 </%ARGS>
 <%INIT>
 my $TicketObj = RT::Ticket->new($session{'CurrentUser'});
@@ -59,5 +60,6 @@ $TicketObj->Load($id);
   HideTitleBox => 1,
   Selectable   => $Selectable,
   Count        => $Count,
+  Checked      => \@AttachExisting,
 &>
 % $m->abort();
diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index f878381..5c8022f 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -94,10 +94,24 @@
 % delete $params{Ticket};
 % delete $params{Attachments};
 % delete $params{Count};
-
 % my $query = $m->comp('/Elements/QueryString', %params, id => $Ticket->id );
 % my $url   = RT->Config->Get('WebPath')."/Helpers/TicketAttachments?$query";
-<a href="#" onclick="jQuery(this).parent().text(<% loc('Loading...') | n,j%>).closest('.attachment-list').load(<% $url |n,j %>); return false;" ><% loc('Show all attachments') %></a>
+
+<script type="text/javascript">
+    function showAllAttachments(node) {
+        var container = node.closest('.attachment-list');
+        var params = node.closest('form').find('input[name=AttachExisting]').serialize();
+
+        node.parent().text(<% loc('Loading...') | n,j%>);
+
+        var url = <% $url |n,j %>;
+        if (params) url += '&' + params;
+        container.load(url);
+    }
+</script>
+
+<a href="#" onclick="showAllAttachments(jQuery(this)); return false;" ><% loc('Show all attachments') %></a>
+
 </span>
 % }
 

commit ec1f85b090b741227ae21bd9e692be0800a699af
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 05:28:37 2016 +0000

    Always pull out attachments in newest-first order
    
        Without this change, the order of attachments with the
        same name would reverse based on whether you provided a $Count
        or not.

diff --git a/share/html/Ticket/Elements/ShowAttachments b/share/html/Ticket/Elements/ShowAttachments
index 5c8022f..ac663a9 100644
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@ -141,14 +141,11 @@ $Attachments->Limit(
 my $show_more = 0;
 my %documents;
 
-# if we're limiting the number of attachments, show only the most recent
-# if we're not limiting the number, order doesn't much matter
-if (defined($Count)) {
-    $Attachments->OrderByCols(
-        { FIELD => 'Created', ORDER => 'DESC' },
-        { FIELD => 'id', ORDER => 'DESC' },
-    );
-}
+# show newest first
+$Attachments->OrderByCols(
+    { FIELD => 'Created', ORDER => 'DESC' },
+    { FIELD => 'id',      ORDER => 'DESC' },
+);
 
 while ( my $attach = $Attachments->Next() ) {
    # display "show more" only when there will be more attachments
@@ -156,7 +153,7 @@ while ( my $attach = $Attachments->Next() ) {
        $show_more = 1;
        last;
    }
-   unshift( @{ $documents{ $attach->Filename } }, $attach );
+   push @{ $documents{ $attach->Filename } }, $attach;
 }
 
 my %is_checked = map { $_ => 1 } @Checked;

commit 884669be5e4a984d2e60aad9744f44fe60a1caaf
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 6 11:26:57 2016 -0400

    Only show "include attachments" if we have attachments

diff --git a/share/html/Ticket/Elements/AddAttachments b/share/html/Ticket/Elements/AddAttachments
index fb4ec23..f61bb6a 100644
--- a/share/html/Ticket/Elements/AddAttachments
+++ b/share/html/Ticket/Elements/AddAttachments
@@ -163,7 +163,7 @@ jQuery( function() {
         </div>
     </td>
 </tr>
-% if ($TicketObj && $TicketObj->id) {
+% if ($HasExisting) {
 <tr>
   <td class="label" valign="top"><&|/l&>Include attachments</&>:</td>
   <td id="reuse-attachments">
@@ -189,4 +189,16 @@ my $attachments;
 if ( exists $session{'Attachments'}{ $Token } && keys %{ $session{'Attachments'}{ $Token } } ) {
     $attachments = $session{'Attachments'}{ $Token };
 }
+
+my $HasExisting = 0;
+
+if ($TicketObj && $TicketObj->id) {
+    my $Existing = $TicketObj->Attachments;
+    $Existing->Limit(
+        FIELD    => 'Filename',
+        OPERATOR => '!=',
+        VALUE    => '',
+    );
+    $HasExisting = 1 if $Existing->Count;
+}
 </%INIT>

commit 7d66e4ff0f4df28fe87fe5dc2519103e2900b560
Merge: 15c9399 884669b
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Fri May 6 17:17:17 2016 +0000

    Merge branch '4.4/attachments-list' into 4.4-trunk


commit dca5e8bd2aa2cae4e90978f56b36cd526f93e57f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon Dec 28 02:12:21 2015 +0000

    Add a new SetInitialCustomField right
    
        This lets you set custom field values on records (tickets, assets,
        articles) that you're creating. But it does not permit modifying the
        custom field values of existing records.
    
    Fixes: I#14974

diff --git a/lib/RT/Article.pm b/lib/RT/Article.pm
index e8ea75c..6eb9640 100644
--- a/lib/RT/Article.pm
+++ b/lib/RT/Article.pm
@@ -147,7 +147,8 @@ sub Create {
                     : (Value => $value)
                 ),
                 Field             => $cf,
-                RecordTransaction => 0
+                RecordTransaction => 0,
+                ForCreation       => 1,
             );
 
             unless ($cfid) {
diff --git a/lib/RT/Asset.pm b/lib/RT/Asset.pm
index c9efb08..b2e75bb 100644
--- a/lib/RT/Asset.pm
+++ b/lib/RT/Asset.pm
@@ -293,7 +293,8 @@ sub Create {
                     ? %$value
                     : (Value => $value)),
                 Field             => $cf,
-                RecordTransaction => 0
+                RecordTransaction => 0,
+                ForCreation       => 1,
             );
             unless ($cfid) {
                 RT->DatabaseHandle->Rollback();
diff --git a/lib/RT/Catalog.pm b/lib/RT/Catalog.pm
index 1ed8585..c6b457c 100644
--- a/lib/RT/Catalog.pm
+++ b/lib/RT/Catalog.pm
@@ -83,8 +83,9 @@ __PACKAGE__->AddRight( General => ShowAsset    => 'See assets' ); #loc
 __PACKAGE__->AddRight( Staff   => CreateAsset  => 'Create assets' ); #loc
 __PACKAGE__->AddRight( Staff   => ModifyAsset  => 'Modify assets' ); #loc
 
-__PACKAGE__->AddRight( General => SeeCustomField      => 'View custom field values' ); # loc
-__PACKAGE__->AddRight( Staff   => ModifyCustomField   => 'Modify custom field values' ); # loc
+__PACKAGE__->AddRight( General => SeeCustomField        => 'View custom field values' ); # loc
+__PACKAGE__->AddRight( Staff   => ModifyCustomField     => 'Modify custom field values' ); # loc
+__PACKAGE__->AddRight( Staff   => SetInitialCustomField => 'Add custom field values only at object creation time'); # loc
 
 RT::ACE->RegisterCacheHandler(sub {
     my %args = (
diff --git a/lib/RT/Class.pm b/lib/RT/Class.pm
index 99faf19..71315ab 100644
--- a/lib/RT/Class.pm
+++ b/lib/RT/Class.pm
@@ -84,19 +84,20 @@ sub Load {
     }
 }
 
-__PACKAGE__->AddRight( Staff   => SeeClass            => 'See that this class exists'); # loc
-__PACKAGE__->AddRight( Staff   => CreateArticle       => 'Create articles in this class'); # loc
-__PACKAGE__->AddRight( General => ShowArticle         => 'See articles in this class'); # loc
-__PACKAGE__->AddRight( Staff   => ShowArticleHistory  => 'See changes to articles in this class'); # loc
-__PACKAGE__->AddRight( General => SeeCustomField      => 'View custom field values' ); # loc
-__PACKAGE__->AddRight( Staff   => ModifyArticle       => 'Modify articles in this class'); # loc
-__PACKAGE__->AddRight( Staff   => ModifyArticleTopics => 'Modify topics for articles in this class'); # loc
-__PACKAGE__->AddRight( Staff   => ModifyCustomField   => 'Modify custom field values' ); # loc
-__PACKAGE__->AddRight( Admin   => AdminClass          => 'Modify metadata and custom fields for this class'); # loc
-__PACKAGE__->AddRight( Admin   => AdminTopics         => 'Modify topic hierarchy associated with this class'); # loc
-__PACKAGE__->AddRight( Admin   => ShowACL             => 'Display Access Control List'); # loc
-__PACKAGE__->AddRight( Admin   => ModifyACL           => 'Create, modify and delete Access Control List entries'); # loc
-__PACKAGE__->AddRight( Staff   => DisableArticle      => 'Disable articles in this class'); # loc
+__PACKAGE__->AddRight( Staff   => SeeClass              => 'See that this class exists'); # loc
+__PACKAGE__->AddRight( Staff   => CreateArticle         => 'Create articles in this class'); # loc
+__PACKAGE__->AddRight( General => ShowArticle           => 'See articles in this class'); # loc
+__PACKAGE__->AddRight( Staff   => ShowArticleHistory    => 'See changes to articles in this class'); # loc
+__PACKAGE__->AddRight( General => SeeCustomField        => 'View custom field values' ); # loc
+__PACKAGE__->AddRight( Staff   => ModifyArticle         => 'Modify articles in this class'); # loc
+__PACKAGE__->AddRight( Staff   => ModifyArticleTopics   => 'Modify topics for articles in this class'); # loc
+__PACKAGE__->AddRight( Staff   => ModifyCustomField     => 'Modify custom field values' ); # loc
+__PACKAGE__->AddRight( Staff   => SetInitialCustomField => 'Add custom field values only at object creation time'); # loc
+__PACKAGE__->AddRight( Admin   => AdminClass            => 'Modify metadata and custom fields for this class'); # loc
+__PACKAGE__->AddRight( Admin   => AdminTopics           => 'Modify topic hierarchy associated with this class'); # loc
+__PACKAGE__->AddRight( Admin   => ShowACL               => 'Display Access Control List'); # loc
+__PACKAGE__->AddRight( Admin   => ModifyACL             => 'Create, modify and delete Access Control List entries'); # loc
+__PACKAGE__->AddRight( Staff   => DisableArticle        => 'Disable articles in this class'); # loc
 
 # {{{ Create
 
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index a6fb1d7..ed8503c 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -214,6 +214,7 @@ __PACKAGE__->AddRight( General => SeeCustomField         => 'View custom fields'
 __PACKAGE__->AddRight( Admin   => AdminCustomField       => 'Create, modify and delete custom fields'); # loc
 __PACKAGE__->AddRight( Admin   => AdminCustomFieldValues => 'Create, modify and delete custom fields values'); # loc
 __PACKAGE__->AddRight( Staff   => ModifyCustomField      => 'Add, modify and delete custom field values for objects'); # loc
+__PACKAGE__->AddRight( Staff   => SetInitialCustomField  => 'Add custom field values only at object creation time'); # loc
 
 =head1 NAME
 
@@ -1639,11 +1640,15 @@ sub AddValueForObject {
         Content      => undef,
         LargeContent => undef,
         ContentType  => undef,
+        ForCreation  => 0,
         @_
     );
     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
 
-    unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
+    unless (
+        $self->CurrentUserHasRight('ModifyCustomField') ||
+        ($args{ForCreation} && $self->CurrentUserHasRight('SetInitialCustomField'))
+    ) {
         return ( 0, $self->loc('Permission Denied') );
     }
 
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 7565b1d..b3940b3 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -100,16 +100,17 @@ use RT::CustomRoles;
 use RT::ACL;
 use RT::Interface::Email;
 
-__PACKAGE__->AddRight( General => SeeQueue            => 'View queue' ); # loc
-__PACKAGE__->AddRight( Admin   => AdminQueue          => 'Create, modify and delete queue' ); # loc
-__PACKAGE__->AddRight( Admin   => ShowACL             => 'Display Access Control List' ); # loc
-__PACKAGE__->AddRight( Admin   => ModifyACL           => 'Create, modify and delete Access Control List entries' ); # loc
-__PACKAGE__->AddRight( Admin   => ModifyQueueWatchers => 'Modify queue watchers' ); # loc
-__PACKAGE__->AddRight( General => SeeCustomField      => 'View custom field values' ); # loc
-__PACKAGE__->AddRight( Staff   => ModifyCustomField   => 'Modify custom field values' ); # loc
-__PACKAGE__->AddRight( Admin   => AssignCustomFields  => 'Assign and remove queue custom fields' ); # loc
-__PACKAGE__->AddRight( Admin   => ModifyTemplate      => 'Modify Scrip templates' ); # loc
-__PACKAGE__->AddRight( Admin   => ShowTemplate        => 'View Scrip templates' ); # loc
+__PACKAGE__->AddRight( General => SeeQueue              => 'View queue' ); # loc
+__PACKAGE__->AddRight( Admin   => AdminQueue            => 'Create, modify and delete queue' ); # loc
+__PACKAGE__->AddRight( Admin   => ShowACL               => 'Display Access Control List' ); # loc
+__PACKAGE__->AddRight( Admin   => ModifyACL             => 'Create, modify and delete Access Control List entries' ); # loc
+__PACKAGE__->AddRight( Admin   => ModifyQueueWatchers   => 'Modify queue watchers' ); # loc
+__PACKAGE__->AddRight( General => SeeCustomField        => 'View custom field values' ); # loc
+__PACKAGE__->AddRight( Staff   => ModifyCustomField     => 'Modify custom field values' ); # loc
+__PACKAGE__->AddRight( Staff   => SetInitialCustomField => 'Add custom field values only at object creation time'); # loc
+__PACKAGE__->AddRight( Admin   => AssignCustomFields    => 'Assign and remove queue custom fields' ); # loc
+__PACKAGE__->AddRight( Admin   => ModifyTemplate        => 'Modify Scrip templates' ); # loc
+__PACKAGE__->AddRight( Admin   => ShowTemplate          => 'View Scrip templates' ); # loc
 
 __PACKAGE__->AddRight( Admin   => ModifyScrips        => 'Modify Scrips' ); # loc
 __PACKAGE__->AddRight( Admin   => ShowScrips          => 'View Scrips' ); # loc
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 2d96103..7f04111 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -1937,6 +1937,7 @@ sub _AddCustomFieldValue {
         LargeContent      => undef,
         ContentType       => undef,
         RecordTransaction => 1,
+        ForCreation       => 0,
         @_
     );
 
@@ -2015,6 +2016,7 @@ sub _AddCustomFieldValue {
             Content      => $args{'Value'},
             LargeContent => $args{'LargeContent'},
             ContentType  => $args{'ContentType'},
+            ForCreation  => $args{'ForCreation'},
         );
 
         unless ( $new_value_id ) {
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index bd17619..61f0ee0 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -502,6 +502,7 @@ sub Create {
                 ),
                 Field             => $cfid,
                 RecordTransaction => 0,
+                ForCreation       => 1,
             );
             push @non_fatal_errors, $msg unless $status;
         }
diff --git a/share/html/Articles/Article/Edit.html b/share/html/Articles/Article/Edit.html
index f2905e6..36d21e9 100644
--- a/share/html/Articles/Article/Edit.html
+++ b/share/html/Articles/Article/Edit.html
@@ -67,7 +67,8 @@
                               CFContent => \%CFContent,
                               ClassObj => $ClassObj,
                               %ARGS,
-                              id =>$id,
+                              id => $id,
+                              ForCreation => ($id eq 'new'),
                               &>
 </&>
 
diff --git a/share/html/Asset/Create.html b/share/html/Asset/Create.html
index 1d258ce..d059ec9 100644
--- a/share/html/Asset/Create.html
+++ b/share/html/Asset/Create.html
@@ -94,7 +94,9 @@
         Object => $asset,
         TitleBoxARGS => { title_class => "inverse" },
         KeepValue => 1,
-        CustomFieldGenerator => sub { $catalog->AssetCustomFields } &>
+        CustomFieldGenerator => sub { $catalog->AssetCustomFields },
+        ForCreation => 1,
+  &>
 </div>
 
   <& /Elements/Submit, Label => loc('Create asset') &>
diff --git a/share/html/Elements/EditCustomFields b/share/html/Elements/EditCustomFields
index 79174de..4423b0a 100644
--- a/share/html/Elements/EditCustomFields
+++ b/share/html/Elements/EditCustomFields
@@ -51,7 +51,8 @@
 <<% $WRAP %> class="edit-custom-fields">
 % }
 % while ( my $CustomField = $CustomFields->Next ) {
-% next unless $CustomField->CurrentUserHasRight('ModifyCustomField');
+% next unless $CustomField->CurrentUserHasRight('ModifyCustomField')
+%          || ($ForCreation && $CustomField->CurrentUserHasRight('SetInitialCustomField'));
 % my $Type = $CustomField->Type || 'Unknown';
 
   <<% $FIELD %> class="edit-custom-field cftype-<% $Type %>">
@@ -116,4 +117,5 @@ $Grouping     => undef
 $AsTable => 1
 $InTable => 0
 $ShowHints => 1
+$ForCreation => 0
 </%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 8357af2..e396857 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -121,6 +121,7 @@
           CustomFields => $QueueObj->TicketCustomFields,
           Grouping => 'Basics',
           InTable => 1,
+          ForCreation => 1,
       &>
       <& /Ticket/Elements/EditTransactionCustomFields, %ARGS, QueueObj => $QueueObj, InTable => 1 &>
     </table>
@@ -134,6 +135,7 @@
     %ARGS,
     Object => $ticket,
     CustomFieldGenerator => sub { $QueueObj->TicketCustomFields },
+    ForCreation => 1,
 &>
 
 </div>
@@ -210,6 +212,7 @@
     CustomFields => $QueueObj->TicketCustomFields,
     Grouping => 'People',
     InTable => 1,
+    ForCreation => 1,
 &>
 
 <tr>
@@ -304,6 +307,7 @@
     CustomFields => $QueueObj->TicketCustomFields,
     Grouping => 'Dates',
     InTable => 1,
+    ForCreation => 1,
 &>
 </table>
 </&>

commit 182d10ac59f83b7939e6c77949bf7b6d80f9aae8
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Mar 3 23:19:07 2016 +0000

    Hide empty "edit custom fields" panels
    
        The titlebox widget hides its chrome when its content contains only
        whitespace characters. This commit suppresses all output from
        EditCustomFields when there are no editable custom fields. This causes
        the titlebox to render nothing, rather than a panel with
        chrome but without content.
    
        Beware that the BeforeCustomFields and AfterCustomFields callbacks
        _may_ generate output even when there are no editable custom fields.
        This commit was carefully written to handle such cases by continuing to
        invoke those callbacks unconditionally.
    
        As best I can tell, we have always had this bug surviving through
        several refactorings. The only conditions under which we successfully
        hid "edit custom field" panels is for users who had no rights to _see_
        any custom fields, since that triggers the following long-standing
        short-circuit return:
    
            # don't print anything if there is no custom fields
            return unless $CustomFields->First;
    
        If you can _see_ any custom fields, they are included in the
        $CustomFields collection, causing us to skip right past that
        short-circuit return. Then, we begin rendering with a <div> or <table>
        container tag. We then iterate over $CustomFields, skipping any custom
        fields you have no modify permissions for. Finally we close the
        container tag. So in the failure case, even though there was no
        _visible_ content, there was still some non-whitespace content (the HTML
        of the container tag) being produced, which was enough to cause the
        titlebox to render its chrome.

diff --git a/share/html/Elements/EditCustomFields b/share/html/Elements/EditCustomFields
index 4423b0a..f09fe6c 100644
--- a/share/html/Elements/EditCustomFields
+++ b/share/html/Elements/EditCustomFields
@@ -47,12 +47,18 @@
 %# END BPS TAGGED BLOCK }}}
 % $m->callback( CallbackName => 'BeforeCustomFields', Object => $Object,
 %               Grouping => $Grouping, ARGSRef => \%ARGS, CustomFields => $CustomFields);
+
+%# only show the wrapper if there are editable custom fields, so we can
+%# suppress the empty titlebox. we do this in such a way that we still call the
+%# BeforeCustomFields and AfterCustomFields callbacks (rather than returning
+%# from the INIT block) to maintain compatibility with old behavior
+
+% if (@CustomFields) {
+
 % if ( $WRAP ) {
 <<% $WRAP %> class="edit-custom-fields">
 % }
-% while ( my $CustomField = $CustomFields->Next ) {
-% next unless $CustomField->CurrentUserHasRight('ModifyCustomField')
-%          || ($ForCreation && $CustomField->CurrentUserHasRight('SetInitialCustomField'));
+% for my $CustomField (@CustomFields) {
 % my $Type = $CustomField->Type || 'Unknown';
 
   <<% $FIELD %> class="edit-custom-field cftype-<% $Type %>">
@@ -86,6 +92,9 @@
 % if ( $WRAP ) {
 </<% $WRAP %>>
 % }
+
+% }
+
 % $m->callback( CallbackName => 'AfterCustomFields', Object => $Object,
 %               Grouping => $Grouping, ARGSRef => \%ARGS );
 <%INIT>
@@ -95,9 +104,14 @@ $CustomFields->LimitToGrouping( $Object => $Grouping ) if defined $Grouping;
 
 $m->callback( %ARGS, CallbackName => 'MassageCustomFields', CustomFields => $CustomFields );
 
-# don't print anything if there is no custom fields
-return unless $CustomFields->First;
 $CustomFields->GotoFirstItem;
+my @CustomFields;
+while ( my $CustomField = $CustomFields->Next ) {
+    next unless $CustomField->CurrentUserHasRight('ModifyCustomField')
+             || ($ForCreation && $CustomField->CurrentUserHasRight('SetInitialCustomField'));
+
+    push @CustomFields, $CustomField;
+}
 
 $AsTable ||= $InTable;
 my $FIELD = $AsTable ? 'tr' : 'div';

commit 13016f4b0cfcde0a531385a39d442a1f901f7598
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Apr 20 18:51:26 2016 +0000

    Add a $CF->CurrentUserCanSee method and switch to it
    
        This is to prepare for allowing users to see custom fields for which they
        do not have SeeCustomField, but for those who should still have access
        thanks to SetInitialCustomField

diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index ed8503c..21db16f 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -611,7 +611,7 @@ sub Values {
     my $cf_values = $class->new( $self->CurrentUser );
     $cf_values->SetCustomFieldObject( $self );
     # if the user has no rights, return an empty object
-    if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
+    if ( $self->id && $self->CurrentUserCanSee ) {
         $cf_values->LimitToCustomField( $self->Id );
     } else {
         $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
@@ -1050,7 +1050,7 @@ sub _Value {
     return undef unless $self->id;
 
     # we need to do the rights check
-    unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
+    unless ( $self->CurrentUserCanSee ) {
         $RT::Logger->debug(
             "Permission denied. User #". $self->CurrentUser->id
             ." has no SeeCustomField right on CF #". $self->id
@@ -1870,7 +1870,7 @@ sub ValuesForObject {
     my $object = shift;
 
     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
-    unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
+    unless ($self->id and $self->CurrentUserCanSee) {
         # Return an empty object if they have no rights to see
         $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
         return ($values);
@@ -1882,6 +1882,16 @@ sub ValuesForObject {
     return ($values);
 }
 
+=head2 CurrentUserCanSee
+
+If the user has SeeCustomField they can see this custom field and its details.
++
+=cut
+
+sub CurrentUserCanSee {
+    my $self = shift;
+    return $self->CurrentUserHasRight('SeeCustomField');
+}
 
 =head2 RegisterLookupType LOOKUPTYPE FRIENDLYNAME
 
@@ -1971,7 +1981,7 @@ sub _URLTemplate {
         }
         return ( 1, $self->loc('Updated') );
     } else {
-        unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
+        unless ( $self->id && $self->CurrentUserCanSee ) {
             return (undef);
         }
 
@@ -1993,7 +2003,7 @@ sub SetBasedOn {
     $cf->Load( ref $value ? $value->id : $value );
 
     return (0, "Permission Denied")
-        unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
+        unless $cf->id && $cf->CurrentUserCanSee;
 
     # XXX: Remove this restriction once we support lists and cascaded selects
     if ( $self->RenderType =~ /List/ ) {
diff --git a/lib/RT/CustomFields.pm b/lib/RT/CustomFields.pm
index d421ead..6aa6d41 100644
--- a/lib/RT/CustomFields.pm
+++ b/lib/RT/CustomFields.pm
@@ -403,7 +403,8 @@ sub AddRecord {
     my ($record) = @_;
 
     $record->SetContextObject( $self->ContextObject );
-    return unless $record->CurrentUserHasRight('SeeCustomField');
+    return unless $record->CurrentUserCanSee;
+
     return $self->SUPER::AddRecord( $record );
 }
 
diff --git a/lib/RT/ObjectCustomFieldValue.pm b/lib/RT/ObjectCustomFieldValue.pm
index 7235ed1..ba781f5 100644
--- a/lib/RT/ObjectCustomFieldValue.pm
+++ b/lib/RT/ObjectCustomFieldValue.pm
@@ -221,7 +221,7 @@ my $re_ip_serialized = qr/$re_ip_sunit(?:\.$re_ip_sunit){3}/;
 sub Content {
     my $self = shift;
 
-    return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField');
+    return undef unless $self->CustomFieldObj->CurrentUserCanSee;
 
     my $content = $self->_Value('Content');
     if (   $self->CustomFieldObj->Type eq 'IPAddress'
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index 372deeb..cd15dd3 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -1423,7 +1423,7 @@ sub CurrentUserCanSee {
         my $cf = RT::CustomField->new( $self->CurrentUser );
         $cf->SetContextObject( $self->Object );
         $cf->Load( $cf_id );
-        return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
+        return 0 unless $cf->CurrentUserCanSee;
     }
 
     # Transactions that might have changed the ->Object's visibility to

commit ff67b7533073659d6aea6bb3de97a94ec4301212
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Apr 20 19:57:29 2016 +0000

    Support passing the CF directly to CustomFieldValueIsEmpty
    
        No reason to reload it if we already have the object

diff --git a/lib/RT/Action/CreateTickets.pm b/lib/RT/Action/CreateTickets.pm
index dd32da7..75b38b1 100644
--- a/lib/RT/Action/CreateTickets.pm
+++ b/lib/RT/Action/CreateTickets.pm
@@ -1140,7 +1140,7 @@ sub UpdateCustomFields {
 
         foreach my $value (@values) {
             next if $ticket->CustomFieldValueIsEmpty(
-                Field => $cf,
+                Field => $CustomFieldObj,
                 Value => $value,
             );
             my ( $val, $msg ) = $ticket->AddCustomFieldValue(
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index f01337c..23dc84d 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -3211,7 +3211,7 @@ sub _ProcessObjectCustomFieldUpdates {
         if ( $arg eq 'AddValue' || $arg eq 'Value' ) {
             foreach my $value (@values) {
                 next if $args{'Object'}->CustomFieldValueIsEmpty(
-                    Field => $cf->id,
+                    Field => $cf,
                     Value => $value,
                 );
                 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 7f04111..930d044 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -2124,7 +2124,7 @@ sub AddCustomFieldDefaultValues {
         my $values = $cf->DefaultValues( Object => $on || RT->System );
         foreach my $value ( UNIVERSAL::isa( $values => 'ARRAY' ) ? @$values : $values ) {
             next if $self->CustomFieldValueIsEmpty(
-                Field => $cf->id,
+                Field => $cf,
                 Value => $value,
             );
 
@@ -2161,7 +2161,10 @@ sub CustomFieldValueIsEmpty {
     my $value = $args{Value};
     return 1 unless defined $value  && length $value;
 
-    my $cf = $self->LoadCustomFieldByIdentifier( $args{'Field'} );
+    my $cf = ref($args{'Field'})
+           ? $args{'Field'}
+           : $self->LoadCustomFieldByIdentifier( $args{'Field'} );
+
     if ($cf) {
         if ( $cf->Type =~ /^Date(?:Time)?$/ ) {
             my $DateObj = RT::Date->new( $self->CurrentUser );
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index 61f0ee0..07e5c57 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -490,7 +490,7 @@ sub Create {
             UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
         {
             next if $self->CustomFieldValueIsEmpty(
-                Field => $cfid,
+                Field => $cf,
                 Value => $value,
             );
 
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index cd15dd3..9fc3211 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -1539,7 +1539,7 @@ sub UpdateCustomFields {
           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
         {
             next if $self->CustomFieldValueIsEmpty(
-                Field => $cfid,
+                Field => $cf,
                 Value => $value,
             );
             $self->_AddCustomFieldValue(

commit f3cd5f6fd03de149f1c87bc16bc90c60c5b2fb6a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Apr 20 21:19:56 2016 +0000

    Avoid constantly reloading the OCFV CustomFieldObj
    
        This code was written as though OCFV kept its CF object in cache, but
        it doesn't.

diff --git a/lib/RT/ObjectCustomFieldValue.pm b/lib/RT/ObjectCustomFieldValue.pm
index ba781f5..fa6623c 100644
--- a/lib/RT/ObjectCustomFieldValue.pm
+++ b/lib/RT/ObjectCustomFieldValue.pm
@@ -221,11 +221,13 @@ my $re_ip_serialized = qr/$re_ip_sunit(?:\.$re_ip_sunit){3}/;
 sub Content {
     my $self = shift;
 
-    return undef unless $self->CustomFieldObj->CurrentUserCanSee;
+    my $cf = $self->CustomFieldObj;
+
+    return undef unless $cf->CurrentUserCanSee;
 
     my $content = $self->_Value('Content');
-    if (   $self->CustomFieldObj->Type eq 'IPAddress'
-        || $self->CustomFieldObj->Type eq 'IPAddressRange' )
+    if (   $cf->Type eq 'IPAddress'
+        || $cf->Type eq 'IPAddressRange' )
     {
 
         require Net::IP;
@@ -236,7 +238,7 @@ sub Content {
             $content = Net::IP::ip_compress_address($1, 6);
         }
 
-        return $content if $self->CustomFieldObj->Type eq 'IPAddress';
+        return $content if $cf->Type eq 'IPAddress';
 
         my $large_content = $self->__Value('LargeContent');
         if ( $large_content =~ /^\s*($re_ip_serialized)\s*$/o ) {

commit ce92fa285c404b7ad8ee65aa20133969a4631853
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Apr 20 20:04:02 2016 +0000

    Allow SetInitialCustomFieldValue without SeeCustomField
    
        This allows you to set up permissions such that users can set custom
        fields on initial ticket create but not see them on ticket display,
        nor edit them on ticket modify.
    
        We have to propagate "this is for creation so
        SetInitialCustomFieldValue is enough to see the CF" from the web
        interface down through to custom field rights checking.

diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 21db16f..e317810 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -1885,12 +1885,22 @@ sub ValuesForObject {
 =head2 CurrentUserCanSee
 
 If the user has SeeCustomField they can see this custom field and its details.
-+
+
+Otherwise, if the user has SetInitialCustomField and this is being used in a
+"create" context, then they can see this custom field and its details. This
+allows you to set up custom fields that are only visible on create pages and
+are then inaccessible.
+
 =cut
 
 sub CurrentUserCanSee {
     my $self = shift;
-    return $self->CurrentUserHasRight('SeeCustomField');
+    return 1 if $self->CurrentUserHasRight('SeeCustomField');
+
+    return 1 if $self->{include_set_initial}
+             && $self->CurrentUserHasRight('SetInitialCustomField');
+
+    return 0;
 }
 
 =head2 RegisterLookupType LOOKUPTYPE FRIENDLYNAME
diff --git a/lib/RT/CustomFields.pm b/lib/RT/CustomFields.pm
index 6aa6d41..7ef55da 100644
--- a/lib/RT/CustomFields.pm
+++ b/lib/RT/CustomFields.pm
@@ -403,6 +403,8 @@ sub AddRecord {
     my ($record) = @_;
 
     $record->SetContextObject( $self->ContextObject );
+    $record->{include_set_initial} = $self->{include_set_initial};
+
     return unless $record->CurrentUserCanSee;
 
     return $self->SUPER::AddRecord( $record );
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 23dc84d..17a73e6 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -3306,6 +3306,7 @@ sub ProcessObjectCustomFieldUpdatesForCreate {
         # we're only interested in new objects, so only look at $id == 0
         for my $cfid (keys %{ $custom_fields{$class}{0} || {} }) {
             my $cf = RT::CustomField->new( $session{'CurrentUser'} );
+            $cf->{include_set_initial} = 1;
             if ($context) {
                 my $system_cf = RT::CustomField->new( RT->SystemUser );
                 $system_cf->LoadById($cfid);
diff --git a/lib/RT/ObjectCustomFieldValue.pm b/lib/RT/ObjectCustomFieldValue.pm
index fa6623c..101f9e9 100644
--- a/lib/RT/ObjectCustomFieldValue.pm
+++ b/lib/RT/ObjectCustomFieldValue.pm
@@ -222,6 +222,7 @@ sub Content {
     my $self = shift;
 
     my $cf = $self->CustomFieldObj;
+    $cf->{include_set_initial} = $self->{include_set_initial};
 
     return undef unless $cf->CurrentUserCanSee;
 
diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 930d044..6d24385 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -1942,6 +1942,8 @@ sub _AddCustomFieldValue {
     );
 
     my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
+    $cf->{include_set_initial} = 1 if $args{'ForCreation'};
+
     unless ( $cf->Id ) {
         return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
     }
@@ -2024,6 +2026,7 @@ sub _AddCustomFieldValue {
         }
 
         my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
+        $new_value->{include_set_initial} = 1 if $args{'ForCreation'};
         $new_value->Load( $new_value_id );
 
         # now that adding the new value was successful, delete the old one
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index 07e5c57..3fa0557 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -484,6 +484,7 @@ sub Create {
         next unless $arg =~ /^CustomField-(\d+)$/i;
         my $cfid = $1;
         my $cf = $self->LoadCustomFieldByIdentifier($cfid);
+        $cf->{include_set_initial} = 1;
         next unless $cf->ObjectTypeFromLookupType($cf->__Value('LookupType'))->isa(ref $self);
 
         foreach my $value (
diff --git a/share/html/Elements/EditCustomFields b/share/html/Elements/EditCustomFields
index f09fe6c..fbfb5ed 100644
--- a/share/html/Elements/EditCustomFields
+++ b/share/html/Elements/EditCustomFields
@@ -99,6 +99,7 @@
 %               Grouping => $Grouping, ARGSRef => \%ARGS );
 <%INIT>
 $CustomFields ||= $Object->CustomFields;
+$CustomFields->{include_set_initial} = 1 if $ForCreation;
 
 $CustomFields->LimitToGrouping( $Object => $Grouping ) if defined $Grouping;
 
diff --git a/t/web/cf_set_initial.t b/t/web/cf_set_initial.t
new file mode 100644
index 0000000..2bf81d9
--- /dev/null
+++ b/t/web/cf_set_initial.t
@@ -0,0 +1,89 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+ok $m->login, 'logged in';
+
+my $cf = RT::CustomField->new( RT->SystemUser );
+my ($cfid, $msg) = $cf->Create(
+    Name => 'Test Set Initial CF',
+    Queue => '0',
+    Type => 'FreeformSingle',
+);
+
+my $tester = RT::Test->load_or_create_user( Name => 'tester', Password => '123456' );
+RT::Test->set_rights(
+    { Principal => $tester->PrincipalObj,
+      Right => [qw(SeeQueue ShowTicket CreateTicket)],
+    },
+);
+ok $m->login( $tester->Name, 123456, logout => 1), 'logged in';
+
+diag "check that we have no CF on the create"
+    ." ticket page when user has no SetInitialCustomField right";
+{
+    $m->submit_form(
+        form_name => "CreateTicketInQueue",
+        fields => { Queue => 'General' },
+    );
+    $m->content_lacks('Test Set Initial CF', 'has no CF input');
+
+    my $form = $m->form_name("TicketCreate");
+    my $edit_field = "Object-RT::Ticket--CustomField-$cfid-Value";
+    ok !$form->find_input( $edit_field ), 'no form field on the page';
+
+    $m->submit_form(
+        form_name => "TicketCreate",
+        fields => { Subject => 'test' },
+    );
+    $m->content_like(qr/Ticket \d+ created/, "a ticket is created succesfully");
+
+    $m->content_lacks('Test Set Initial CF', 'has no CF on the page');
+    $m->follow_link( text => 'Custom Fields');
+    $m->content_lacks('Test Set Initial CF', 'has no CF field');
+}
+
+RT::Test->set_rights(
+    { Principal => $tester->PrincipalObj,
+      Right => [qw(SeeQueue ShowTicket CreateTicket SetInitialCustomField)],
+    },
+);
+
+diag "check that we have the CF on the create"
+    ." ticket page when user has SetInitialCustomField but no SeeCustomField";
+{
+    $m->submit_form(
+        form_name => "CreateTicketInQueue",
+        fields => { Queue => 'General' },
+    );
+    $m->content_contains('Test Set Initial CF', 'has CF input');
+
+    my $form = $m->form_name("TicketCreate");
+    my $edit_field = "Object-RT::Ticket--CustomField-$cfid-Value";
+    ok $form->find_input( $edit_field ), 'has form field on the page';
+
+    $m->submit_form(
+        form_name => "TicketCreate",
+        fields => {
+            $edit_field => 'yatta',
+            Subject => 'test 2',
+        },
+    );
+    $m->content_like(qr/Ticket \d+ created/, "a ticket is created succesfully");
+    if (my ($id) = $m->content =~ /Ticket (\d+) created/) {
+        my $ticket = RT::Ticket->new(RT->SystemUser);
+        my ($ok, $msg) = $ticket->Load($id);
+        ok($ok, "loaded ticket $id");
+        is($ticket->Subject, 'test 2', 'subject is correct');
+        is($ticket->FirstCustomFieldValue('Test Set Initial CF'), 'yatta', 'CF was set correctly');
+    }
+
+    $m->content_lacks('Test Set Initial CF', 'has no CF on the page');
+    $m->follow_link( text => 'Custom Fields');
+    $m->content_lacks('Test Set Initial CF', 'has no CF edit field');
+}
+
+undef $m;
+done_testing;

commit bce6d3c76f0bc7e4b9306c7dc917aa5f53088338
Merge: 7d66e4f ce92fa2
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Fri May 6 17:23:17 2016 +0000

    Merge branch '4.4/initial-custom-field' into 4.4-trunk


commit 1d8949150a43e906f63ccac954273a4f16550608
Merge: bce6d3c 8ee2c77
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Fri May 6 22:27:38 2016 +0000

    Merge branch '4.4/bulkupdate-checkboxes' into 4.4-trunk


commit c2dfb321be84c3ead90b86a5a940172373076f99
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri Apr 22 16:42:41 2016 +0000

    Fix JS compile errors for translations with apostrophes
    
        This particularly affects Catalan since it translates "Error" as
        "S'ha produït un error". This breaks when we try to localize directly
        inside a JavaScript literal string like so:
    
            … + '<title><&|/l&>Error</&></title>' + …
    
        since Mason compiles this as JavaScript which has one too many quotes:
    
            … + '<title>S'ha produït un error</title>' + …
                ^        ^                           ^
    
        The solution is to switch this line to use our JavaScript-based
        localization engine which avoids quoting problems, since Mason
        is no longer directly involved:
    
            … + '<title>' + loc_key('error') + '</title>' + …
    
         This only affects single-quoted strings since <&|/l&> escapes
         double quotes (and other HTML meta characters).
    
         This commit fixes the similar "Remove" and "Check" i18n
         interpolations in the same component, though as of this commit
         no translations include apostrophes for either msgid.
    
    Fixes: I#31864

diff --git a/share/html/Elements/JavascriptConfig b/share/html/Elements/JavascriptConfig
index 2a135b3..53046b4 100644
--- a/share/html/Elements/JavascriptConfig
+++ b/share/html/Elements/JavascriptConfig
@@ -66,6 +66,9 @@ my $Catalog = {
     quote_in_filename => "Filenames with double quotes can not be uploaded.", #loc
     attachment_warning_regex => "\\battach", #loc
     shortcut_help_error => "Unable to open shortcut help. Reason:", #loc
+    error => "Error", #loc
+    check => "Check", #loc
+    remove => "Remove", #loc
 };
 $_ = loc($_) for values %$Catalog;
 
diff --git a/share/html/Ticket/Elements/AddAttachments b/share/html/Ticket/Elements/AddAttachments
index 32217c1..c040608 100644
--- a/share/html/Ticket/Elements/AddAttachments
+++ b/share/html/Ticket/Elements/AddAttachments
@@ -74,7 +74,7 @@ jQuery( function() {
             '<div class="dz-preview dz-file-preview">' +
             '    <div class="dz-remove-mark pointer-events" data-dz-remove>' +
             '        <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">' +
-            '            <title><&|/l&>Remove</&></title>' +
+            '            <title>' + loc_key('remove') + '</title>' +
             '            <defs></defs>' +
             '            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">' +
             '                <g style="fill:#d9534f" id="Check-+-Oval-2" sketch:type="MSLayerGroup" stroke="#747474" stroke-opacity="0.198794158" fill="#FFFFFF" fill-opacity="0.816519475">' +
@@ -95,7 +95,7 @@ jQuery( function() {
             '    <div class="dz-error-message"><span data-dz-errormessage></span></div>' +
             '    <div class="dz-success-mark">' +
             '        <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">' + 
-            '            <title><&|/l&>Check</&></title>' +
+            '            <title>' + loc_key('check') + '</title>' +
             '            <defs></defs>' +
             '            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">' +
             '                <path d="M23.5,31.8431458 L17.5852419,25.9283877 C16.0248253,24.3679711 13.4910294,24.366835 11.9289322,25.9289322 C10.3700136,27.4878508 10.3665912,30.0234455 11.9283877,31.5852419 L20.4147581,40.0716123' +
@@ -107,7 +107,7 @@ jQuery( function() {
             '    </div>' +
             '    <div class="dz-error-mark">' +
             '        <svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">' +
-            '            <title><&|/l&>Error</&></title>' +
+            '            <title>' + loc_key('error') + '</title>' +
             '            <defs></defs>' +
             '            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">' +
             '                <g id="Check-+-Oval-2" sketch:type="MSLayerGroup" stroke="#747474" stroke-opacity="0.198794158" fill="#FFFFFF" fill-opacity="0.816519475">' +

commit 3a55affb21d201af102dfe01d327e3a61a57c957
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 06:29:56 2016 +0000

    Use Status = '__Active__' for self service queries
    
        The status argument to /SelfService/Elements/MyRequests changed
        from an array of statuses to a single scalar status (because the
        core code only ever used the equivalent of __Active__ and
        __Inactive__)

diff --git a/share/html/SelfService/Closed.html b/share/html/SelfService/Closed.html
index fdce958..822733a 100644
--- a/share/html/SelfService/Closed.html
+++ b/share/html/SelfService/Closed.html
@@ -49,7 +49,7 @@
 
 <& /SelfService/Elements/MyRequests,
     %ARGS,
-    status          => [ RT::Queue->InactiveStatusArray ],
+    status          => '__Inactive__',
     title           => loc('My closed tickets'),
     BaseURL         => RT->Config->Get('WebPath') ."/SelfService/Closed.html?",
     Page            => $Page,
diff --git a/share/html/SelfService/Elements/MyRequests b/share/html/SelfService/Elements/MyRequests
index d0b04c3..8a988d0 100644
--- a/share/html/SelfService/Elements/MyRequests
+++ b/share/html/SelfService/Elements/MyRequests
@@ -62,16 +62,18 @@
 $title ||= loc("My [_1] tickets", $friendly_status);
 my $id = $session{'CurrentUser'}->id;
 my $Query = "( Watcher.id = $id )";
-if ( @status ) {
-    @status = map {s/(['\\])/\\$1/g; "Status = '$_'"} @status;
-    $Query .= " AND ( " . join(' OR ', @status ) . " )";
+
+if ($status) {
+    $status =~ s/(['\\])/\\$1/g;
+    $Query .= " AND Status = '$status'";
 }
+
 my $Format = RT->Config->Get('DefaultSelfServiceSearchResultFormat');
 </%INIT>
 <%ARGS>
 $title => undef
 $friendly_status => loc('open')
- at status => ()
+$status => undef
 $BaseURL => undef
 $Page => 1
 @Order => ('ASC')
diff --git a/share/html/SelfService/index.html b/share/html/SelfService/index.html
index c5aad9f..6f3251d 100644
--- a/share/html/SelfService/index.html
+++ b/share/html/SelfService/index.html
@@ -48,7 +48,7 @@
 <& /SelfService/Elements/Header, Title => loc('Open tickets') &>
 <& /SelfService/Elements/MyRequests,
     %ARGS,
-    status          => [ RT::Queue->ActiveStatusArray() ],
+    status  => '__Active__',
     title   => loc('My open tickets'),
     BaseURL => RT->Config->Get('WebPath') ."/SelfService/?",
     Page    => $Page, 

commit 088e5d48cd8aea4ce54bbe9bb1cb39de1e3e2d97
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 06:31:49 2016 +0000

    Use Status = '__Active__' for requestor ticket lists
    
        The condition's "name" is unused.

diff --git a/share/html/Ticket/Elements/ShowRequestorTicketsActive b/share/html/Ticket/Elements/ShowRequestorTicketsActive
index 3d945bc..99adba1 100644
--- a/share/html/Ticket/Elements/ShowRequestorTicketsActive
+++ b/share/html/Ticket/Elements/ShowRequestorTicketsActive
@@ -48,9 +48,7 @@
 <& ShowRequestorTickets, %ARGS, Description => loc('active'), conditions => $conditions, Rows => $Rows  &>
 <%INIT>
 unless ( @$conditions ) {
-    foreach (RT::Queue->ActiveStatusArray()) {
-        push @$conditions, { cond => "Status = '$_'", name => loc($_) };
-    }
+    push @$conditions, { cond => "Status = '__Active__'" };
 }
 </%INIT>
 <%ARGS>
diff --git a/share/html/Ticket/Elements/ShowRequestorTicketsInactive b/share/html/Ticket/Elements/ShowRequestorTicketsInactive
index eb08c4c..fa8e6df 100644
--- a/share/html/Ticket/Elements/ShowRequestorTicketsInactive
+++ b/share/html/Ticket/Elements/ShowRequestorTicketsInactive
@@ -48,9 +48,7 @@
 <& ShowRequestorTickets, %ARGS, Description => loc('inactive'), conditions => $conditions, Rows => $Rows &>
 <%INIT>
 unless ( @$conditions ) {
-    foreach (RT::Queue->InactiveStatusArray()) {
-        push @$conditions, { cond => "Status = '$_'", name => loc($_) };
-    }
+    push @$conditions, { cond => "Status = '__Inactive__'" };
 }
 </%INIT>
 <%ARGS>

commit c0587b32ff0b0449e9842e5161e5f482ae576da6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 17 06:35:24 2016 +0000

    Use Status = '__Active__' for user ticket portlets
    
        The condition's "name" is unused.

diff --git a/share/html/User/Elements/Portlets/ActiveTickets b/share/html/User/Elements/Portlets/ActiveTickets
index 8ed8c58..665ae7d 100644
--- a/share/html/User/Elements/Portlets/ActiveTickets
+++ b/share/html/User/Elements/Portlets/ActiveTickets
@@ -58,9 +58,7 @@
 &>
 <%INIT>
 unless ( @$conditions ) {
-    foreach (RT::Queue->ActiveStatusArray()) {
-        push @$conditions, { cond => "Status = '$_'", name => loc($_) };
-    }
+    push @$conditions, { cond => "Status = '__Active__'" };
 }
 </%INIT>
 <%ARGS>
diff --git a/share/html/User/Elements/Portlets/InactiveTickets b/share/html/User/Elements/Portlets/InactiveTickets
index a768427..7507fe0 100644
--- a/share/html/User/Elements/Portlets/InactiveTickets
+++ b/share/html/User/Elements/Portlets/InactiveTickets
@@ -58,9 +58,7 @@
 &>
 <%INIT>
 unless ( @$conditions ) {
-    foreach (RT::Queue->InactiveStatusArray()) {
-        push @$conditions, { cond => "Status = '$_'", name => loc($_) };
-    }
+    push @$conditions, { cond => "Status = '__Inactive__'" };
 }
 </%INIT>
 <%ARGS>

commit d5c9858b9905b1d5c5b6cb7e94d1ed083d876c47
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Feb 18 18:30:57 2016 -0500

    Use Status = '__Active__' for reminders
    
    Fixes: I#31178

diff --git a/share/html/Elements/ShowReminders b/share/html/Elements/ShowReminders
index 2101a26..460e1e7 100644
--- a/share/html/Elements/ShowReminders
+++ b/share/html/Elements/ShowReminders
@@ -90,7 +90,7 @@ if ( my $ticket= $targets->First ) {
 my $reminders = RT::Tickets->new($session{'CurrentUser'});
 my $tsql = 'Type = "reminder"' .
            ' AND ( Owner = "Nobody" OR Owner ="' . $session{'CurrentUser'}->id . '")' .
-           ' AND ( Status = "new" OR Status = "open" )';
+           ' AND Status = "__Active__"';
 
 $tsql .= ' AND ( Due < "now" OR Due IS NULL )' if $OnlyOverdue;
 

commit a331558042e57de46eadd2d0176be83e31749c20
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 24 18:38:08 2016 +0000

    Escape individual status names in QueueSummaryByStatus
    
        QueueSummaryByLifecycle has escaping here added by
        d48afa239779ec2e7325aa98cf43736019d5ffab, but that change looks to have
        overlooked QueueSummaryByStatus
    
        The other disparities from that commit have already been resolved by
        e9119915e8b2321115404037463a8a8437ec241d's switching to
        Status = '__Active__' for these templates

diff --git a/share/html/Elements/QueueSummaryByStatus b/share/html/Elements/QueueSummaryByStatus
index 812ba4b..c939d01 100644
--- a/share/html/Elements/QueueSummaryByStatus
+++ b/share/html/Elements/QueueSummaryByStatus
@@ -97,6 +97,7 @@ my $link_all = sub {
 
 my $link_status = sub {
     my ($queue, $status) = @_;
+    $status =~ s{(['\\])}{\\$1}g;
     return $build_search_link->($queue->{Name}, "Status = '$status'");
 };
 

commit 85a34ae57bfa36b7f72d1383097f7bcb5a1f220e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Feb 24 20:56:49 2016 +0000

    Quote lifecycle and status names for Status = '__Active__'

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 2e930cc..38c7609 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -2980,6 +2980,12 @@ sub _parser {
 
     state ( $active_status_node, $inactive_status_node );
 
+    my $escape_quotes = sub {
+        my $text = shift;
+        $text =~ s{(['\\])}{\\$1}g;
+        return $text;
+    };
+
     $tree->traverse(
         sub {
             my $node = shift;
@@ -3001,15 +3007,16 @@ sub _parser {
 
                     my $sql;
                     if ( keys %lifecycle == 1 ) {
-                        $sql = join ' OR ', map { qq{ Status = "$_" } } map { @$_ } values %lifecycle;
+                        $sql = join ' OR ', map { qq{ Status = '$_' } } map { $escape_quotes->($_) } map { @$_ } values %lifecycle;
                     }
                     else {
                         my @inactive_sql;
                         for my $name ( keys %lifecycle ) {
+                            my $escaped_name = $escape_quotes->($name);
                             my $inactive_sql =
-                                qq{Lifecycle = "$name"}
+                                qq{Lifecycle = '$escaped_name'}
                               . ' AND ('
-                              . join( ' OR ', map { qq{ Status = "$_" } } @{ $lifecycle{ $name } } ) . ')';
+                              . join( ' OR ', map { qq{ Status = '$_' } } map { $escape_quotes->($_) } @{ $lifecycle{ $name } } ) . ')';
                             push @inactive_sql, qq{($inactive_sql)};
                         }
                         $sql = join ' OR ', @inactive_sql;
@@ -3040,15 +3047,16 @@ sub _parser {
 
                     my $sql;
                     if ( keys %lifecycle == 1 ) {
-                        $sql = join ' OR ', map { qq{ Status = "$_" } } map { @$_ } values %lifecycle;
+                        $sql = join ' OR ', map { qq{ Status = '$_' } } map { $escape_quotes->($_) } map { @$_ } values %lifecycle;
                     }
                     else {
                         my @active_sql;
                         for my $name ( keys %lifecycle ) {
+                            my $escaped_name = $escape_quotes->($name);
                             my $active_sql =
-                                qq{Lifecycle = "$name"}
+                                qq{Lifecycle = '$escaped_name'}
                               . ' AND ('
-                              . join( ' OR ', map { qq{ Status = "$_" } } @{ $lifecycle{ $name } } ) . ')';
+                              . join( ' OR ', map { qq{ Status = '$_' } } map { $escape_quotes->($_) } @{ $lifecycle{ $name } } ) . ')';
                             push @active_sql, qq{($active_sql)};
                         }
                         $sql = join ' OR ', @active_sql;

commit 6ebcea11dc5629403c116fc090d5096c6c17d283
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 9 21:02:42 2016 +0000

    Use Status = '__Active__' for initialdata searches
    
        This fixes "newest unowned tickets" and "highest priority tickets I
        own" for new installs. Upgrades to existing RTs won't receive this
        change.

diff --git a/etc/initialdata b/etc/initialdata
index 078da41..1ed204c 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -866,7 +866,7 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
       { Format =>  q{'<a href="__WebPath__/Ticket/Display.html?id=__id__">__id__</a>/TITLE:#',}
                  . q{'<a href="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a>/TITLE:Subject',}
                  . q{Priority, QueueName, ExtendedStatus},
-        Query   => " Owner = '__CurrentUser__' AND ( Status = 'new' OR Status = 'open')",
+        Query   => " Owner = '__CurrentUser__' AND Status = '__Active__'",
         OrderBy => 'Priority',
         Order   => 'DESC'
       },
@@ -879,7 +879,7 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
                  . q{'<a href="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a>/TITLE:Subject',}
                  . q{QueueName, ExtendedStatus, CreatedRelative, }
                  . q{'<A HREF="__WebPath__/Ticket/Display.html?Action=Take&id=__id__">__loc(Take)__</a>/TITLE:NBSP'},
-        Query   => " Owner = 'Nobody' AND ( Status = 'new' OR Status = 'open')",
+        Query   => " Owner = 'Nobody' AND Status = '__Active__'",
         OrderBy => 'Created',
         Order   => 'DESC'
       },

commit c4baf26d75fcc7d01b518a9c53a223e87a4d661d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 9 21:28:52 2016 +0000

    Use single variable naming convention in timer JS

diff --git a/share/html/Helpers/TicketTimer b/share/html/Helpers/TicketTimer
index 5774cf1..2a8b0d6 100644
--- a/share/html/Helpers/TicketTimer
+++ b/share/html/Helpers/TicketTimer
@@ -52,13 +52,13 @@ $id
 my $Ticket = RT::Ticket->new($session{'CurrentUser'});
 $Ticket->Load( $id );
 
-my $unpause_img = RT->Config->Get('WebPath') . '/static/images/unpause.png';
-my $pause_img   = RT->Config->Get('WebPath') . '/static/images/pause.png';
+my $UnpauseImg = RT->Config->Get('WebPath') . '/static/images/unpause.png';
+my $PauseImg   = RT->Config->Get('WebPath') . '/static/images/pause.png';
 
-my $now = RT::Date->new($session{'CurrentUser'});
-$now->SetToNow;
+my $Now = RT::Date->new($session{'CurrentUser'});
+$Now->SetToNow;
 
-my $submit_url = RT->Config->Get('WebPath') . '/Helpers/AddTimeWorked';
+my $SubmitURL = RT->Config->Get('WebPath') . '/Helpers/AddTimeWorked';
 </%INIT>
 <& /Elements/Header, Title => loc('Timer for #[_1]: [_2]', $Ticket->Id, $Ticket->Subject), RichText => 0, ShowBar => 0, ShowTitle => 0 &>
 
@@ -86,30 +86,30 @@ jQuery( function() {
     // CommittedSeconds.
     var CommittedSeconds = 0;
 
-    var readout = jQuery('.readout');
-    var playpause = jQuery('.playpause');
-    var playpause_img = playpause.find('img');
+    var Readout = jQuery('.readout');
+    var PlayPause = jQuery('.playpause');
+    var PlayPauseImg = PlayPause.find('img');
 
-    var pause_alt = playpause_img.attr('alt');
-    var unpause_alt = playpause_img.data('toggle-alt');
+    var PauseAlt = PlayPauseImg.attr('alt');
+    var UnpauseAlt = PlayPauseImg.data('toggle-alt');
 
-    var toHHMMSS = function (total) {
-        var hours   = Math.floor(total / 3600);
-        var minutes = Math.floor((total - (hours * 3600)) / 60);
-        var seconds = total - (hours * 3600) - (minutes * 60);
+    var ToHHMMSS = function (Total) {
+        var Hours   = Math.floor(Total / 3600);
+        var Minutes = Math.floor((Total - (Hours * 3600)) / 60);
+        var Seconds = Total - (Hours * 3600) - (Minutes * 60);
 
-        if (minutes < 10) { minutes = "0" + minutes; }
-        if (seconds < 10) { seconds = "0" + seconds; }
+        if (Minutes < 10) { Minutes = "0" + Minutes; }
+        if (Seconds < 10) { Seconds = "0" + Seconds; }
 
-        return hours + ':' + minutes + ':' + seconds;
+        return Hours + ':' + Minutes + ':' + Seconds;
     };
 
-    var renderReadout = function (seconds) {
-        readout.text(toHHMMSS(seconds));
+    var RenderReadout = function (seconds) {
+        Readout.text(ToHHMMSS(seconds));
     };
 
-    var tick = function () {
-        renderReadout(CommittedSeconds + CurrentSeconds());
+    var Tick = function () {
+        RenderReadout(CommittedSeconds + CurrentSeconds());
     };
 
     jQuery('.playpause').click(function () {
@@ -119,17 +119,17 @@ jQuery( function() {
             Interval = false;
             CommittedSeconds += CurrentSeconds();
             LastUnpause = false;
-            playpause_img.attr('src', <% $unpause_img |n,j %>);
-            playpause_img.attr('alt', unpause_alt);
-            playpause_img.attr('title', unpause_alt);
+            PlayPauseImg.attr('src', <% $UnpauseImg |n,j %>);
+            PlayPauseImg.attr('alt', UnpauseAlt);
+            PlayPauseImg.attr('title', UnpauseAlt);
         }
         else {
             // unpause
-            Interval = setInterval(tick, 1000);
+            Interval = setInterval(Tick, 1000);
             LastUnpause = new Date().getTime() / 1000;
-            playpause_img.attr('src', <% $pause_img |n,j %>);
-            playpause_img.attr('alt', pause_alt);
-            playpause_img.attr('title', pause_alt);
+            PlayPauseImg.attr('src', <% $PauseImg |n,j %>);
+            PlayPauseImg.attr('alt', PauseAlt);
+            PlayPauseImg.attr('title', PauseAlt);
         }
         return false;
     });
@@ -139,39 +139,39 @@ jQuery( function() {
         jQuery('.control-line a').hide();
         CommittedSeconds += CurrentSeconds();
 
-        var payload = {
+        var Payload = {
             id: <% $Ticket->id %>,
             seconds: CommittedSeconds
         };
 
-        readout.text('<% loc("Submitting") %>');
+        Readout.text('<% loc("Submitting") %>');
 
-        var renderSubmitError = function (reason) {
-            renderReadout(CommittedSeconds);
+        var RenderSubmitError = function (Reason) {
+            RenderReadout(CommittedSeconds);
             jQuery('.ticket-timer').addClass('error');
 
             // give the browser a chance to redraw the readout
             setTimeout(function () {
-                alert('<% loc("Unable to submit time. Please add it to the ticket manually. Reason:") %>' + ' ' + reason);
+                alert('<% loc("Unable to submit time. Please add it to the ticket manually. Reason:") %>' + ' ' + Reason);
             }, 100);
         };
 
         jQuery.ajax({
-            url: <% $submit_url |n,j %>,
-            data: payload,
+            url: <% $SubmitURL |n,j %>,
+            data: Payload,
             timeout: 30000, /* 30 seconds */
-            success: function (response) {
-                if (response.ok) {
-                    readout.addClass('response');
-                    readout.text(response.msg);
+            success: function (Response) {
+                if (Response.ok) {
+                    Readout.addClass('response');
+                    Readout.text(Response.msg);
                     jQuery('.control-line .close-popup').show().removeClass('hidden');
                 }
                 else {
-                    renderSubmitError(response.msg);
+                    RenderSubmitError(Response.msg);
                 }
             },
             error: function (xhr, reason) {
-                renderSubmitError(reason);
+                RenderSubmitError(reason);
             }
         });
 
@@ -183,8 +183,8 @@ jQuery( function() {
         return false;
     });
 
-    tick();
-    Interval = setInterval(tick, 500);
+    Tick();
+    Interval = setInterval(Tick, 500);
 });
 </script>
 
@@ -200,13 +200,13 @@ jQuery( function() {
         <div class="readout"></div>
 
         <div class="control-line">
-            <a href="#" class="playpause"><img src="<% $pause_img %>" alt="<% loc('Pause Timer') %>" data-toggle-alt="<% loc('Resume Timer') %>" title="<% loc('Pause Timer') %>" /></a>
+            <a href="#" class="playpause"><img src="<% $PauseImg %>" alt="<% loc('Pause Timer') %>" data-toggle-alt="<% loc('Resume Timer') %>" title="<% loc('Pause Timer') %>" /></a>
             <a href="#" class="submit-time"><img src="<% RT->Config->Get('WebPath') %>/static/images/submit.png" alt="<% loc('Submit Timer') %>" title="<% loc('Submit Timer') %>" /></a>
             <a href="#" class="close-popup hidden"><img src="<% RT->Config->Get('WebPath') %>/static/images/close.png" alt="<% loc('Close Window') %>" title="<% loc('Close Window') %>" /></a>
         </div>
     </div>
 
-    <div class="extra"><&|/l, $now->AsString &>Started at [_1].</&></div>
+    <div class="extra"><&|/l, $Now->AsString &>Started at [_1].</&></div>
 
 % if ($Ticket->TimeEstimated) {
     <div class="extra"><&|/l&>Time estimated</&>: <& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeEstimated &></div>

commit 8108f9f9f124351e455cad8b836c7072208a681f
Merge: 1d89491 c4baf26
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Mon May 9 21:31:35 2016 +0000

    Merge branch '4.4/background-ticket-timer' into 4.4-trunk


commit 2544758ca70912b236d98777f0b3549beb7a166d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 9 22:44:05 2016 +0000

    Make ChartStyle a scalar
    
        I can see no reason why this is treated as an array reference
        in /Search/Chart.html; it's treated as a scalar everywhere else,
        including in the UI.

diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 9dc0d78..57cd0ec 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -49,7 +49,7 @@
 my $default_value = {
     Query => 'id > 0',
     GroupBy => ['Status'],
-    ChartStyle => ['bar+table+sql'],
+    ChartStyle => 'bar+table+sql',
     ChartFunction => ['COUNT'],
 };
     
@@ -174,8 +174,8 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
 </&>
 
 <&| /Widgets/TitleBox, title => loc('Picture'), class => "chart-picture" &>
-<input name="ChartStyle" type="hidden" value="<% $query{ChartStyle}[0] %>" />
-<label><% loc('Style') %>: <& Elements/SelectChartType, Default => $query{ChartStyle}[0] =~ /^(pie|bar|table)\b/ ? $1 : undef &></label>
+<input name="ChartStyle" type="hidden" value="<% $query{ChartStyle} %>" />
+<label><% loc('Style') %>: <& Elements/SelectChartType, Default => $query{ChartStyle} =~ /^(pie|bar|table)\b/ ? $1 : undef &></label>
 <span class="width">
 <label><% loc("Width") %>: <input type="text" name="Width" value="<% $query{'Width'} || q{} %>"> <% loc("px") %></label>
 </span>
@@ -184,10 +184,10 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
   <label><% loc("Height") %>: <input type="text" name="Height" value="<% $query{'Height'} || q{} %>"> <% loc("px") %></label>
 </span>
 <div class="include-table">
-    <input type="checkbox" name="ChartStyleIncludeTable" <% $query{ChartStyle}[0] =~ /\btable\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include data table') %>
+    <input type="checkbox" name="ChartStyleIncludeTable" <% $query{ChartStyle} =~ /\btable\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include data table') %>
 </div>
 <div class="include-sql">
-    <input type="checkbox" name="ChartStyleIncludeSQL" <% $query{ChartStyle}[0] =~ /\bsql\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include TicketSQL query') %>
+    <input type="checkbox" name="ChartStyleIncludeSQL" <% $query{ChartStyle} =~ /\bsql\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include TicketSQL query') %>
 </div>
 </&>
 <script type="text/javascript">

commit 6270f1334a9fdb5ba67a08ce9573f92d0f407c8b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue May 10 15:13:15 2016 -0400

    L<RT::Authen::ExternalAuth> shouldn't link to metacpan
    
    Now that RT::Authen::ExternalAuth is part of core, documentation
    hyperlinks (e.g. L<RT::Authen::ExternalAuth>) for modules in that
    namespace should be local, rather than pointing to metacpan. Any other
    L<RT::Authen::*> links should continue pointing to metacpan.
    
    This change fixes several hyperlinks within
    lib/RT/Authen/ExternalAuth.pm as well as in docs/authentication.pod.
    
    L<RT::LDAPImport> does not need a similar fix since its namespace was
    changed from L<RT::Extension::LDAPImport> as part of coring.
    
    Fixes: I#31957

diff --git a/lib/RT/Pod/HTML.pm b/lib/RT/Pod/HTML.pm
index d38a882..8cdcc7b 100644
--- a/lib/RT/Pod/HTML.pm
+++ b/lib/RT/Pod/HTML.pm
@@ -129,7 +129,7 @@ sub resolve_local_link {
         : '';
 
     my $local;
-    if ($name =~ /^RT(::(?!Extension::|Authen::)|$)/ or $self->batch->found($name)) {
+    if ($name =~ /^RT(::(?!Extension::|Authen::(?!ExternalAuth))|$)/ or $self->batch->found($name)) {
         $local = join "/",
                   map { $self->encode_entities($_) }
                 split /::/, $name;

commit 2da708cb86b3ebea47b172c718fa4ef939c18c10
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue May 10 20:53:53 2016 +0000

    Add test demonstrating not-fully-initialized saved charts
    
        Reported on issues ticket 31557.

diff --git a/t/web/saved_search_chart.t b/t/web/saved_search_chart.t
index 3737b51..e964a8f 100644
--- a/t/web/saved_search_chart.t
+++ b/t/web/saved_search_chart.t
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use RT::Test no_plan => 1;
+use RT::Test tests => undef;
 my ( $url, $m ) = RT::Test->started_ok;
 use RT::Attribute;
 
@@ -151,3 +151,43 @@ is(
 );
 
 page_chart_link_has($m, $saved_search_ids[1]);
+
+diag "saving a chart without changing its config shows up on dashboards (I#31557)";
+{
+    $m->get_ok( $url . "/Search/Chart.html?Query=" . 'id!=-1' );
+    $m->submit_form(
+        form_name => 'SaveSearch',
+        fields    => {
+            SavedSearchDescription => 'chart without updates',
+            SavedSearchOwner       => $owner,
+        },
+        button => 'SavedSearchSave',
+    );
+
+    $m->form_name('SaveSearch');
+    @saved_search_ids =
+        $m->current_form->find_input('SavedSearchLoad')->possible_values;
+    shift @saved_search_ids; # first value is blank
+    my $chart_without_updates_id = $saved_search_ids[2];
+    ok($chart_without_updates_id, 'got a saved chart id');
+
+    my ($privacy, $user_id, $search_id) = $chart_without_updates_id =~ /^(RT::User-(\d+))-SavedSearch-(\d+)$/;
+    my $user = RT::User->new(RT->SystemUser);
+    $user->Load($user_id);
+    is($user->Name, 'root', 'loaded user');
+    my $currentuser = RT::CurrentUser->new($user);
+
+    my $search = RT::SavedSearch->new($currentuser);
+    $search->Load($privacy, $search_id);
+    is($search->Name, 'chart without updates', 'loaded search');
+    is($search->GetParameter('ChartStyle'), 'bar+table+sql', 'chart correctly initialized with default ChartStyle');
+    is($search->GetParameter('Height'), undef, 'no height by default');
+    is($search->GetParameter('Width'), undef, 'no width by default');
+    is($search->GetParameter('Query'), 'id!=-1', 'chart correctly initialized with Query');
+    is($search->GetParameter('SearchType'), 'Chart', 'chart correctly initialized with SearchType');
+    is_deeply($search->GetParameter('GroupBy'), ['Status'], 'chart correctly initialized with default GroupBy');
+    is_deeply($search->GetParameter('ChartFunction'), ['COUNT'], 'chart correctly initialized with default ChartFunction');
+}
+
+undef $m;
+done_testing;

commit f75aa33c4f6be5724525931b40159595f162bc5a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 9 22:55:23 2016 +0000

    Fix charts sometimes not showing up on dashboards
    
        The problem was charts that had not been updated using the "Update
        Chart" button were incompletely written into Saved Charts: their
        default values (ChartStyle=bar+table+sql, etc) were not saved
        into the saved chart. This is ordinarily not a problem for saved
        charts because those defaults would be set up when you loaded a
        saved chart using the normal UI. However, dashboards don't have the
        same initialization order, so the $ChartStyle would be empty, causing
        no rendering to happen.
    
        This regression was exarcerbated by
        aedc560e55bea689e083214f33912abb59ac0617 which introduces a
        conditional on the uninitialized $ChartStyle. Previously,
        <img src="…/Search/Chart"> was included unconditionally, which
        worked because /Search/Chart itself provides a default $ChartStyle.
    
        This commit addresses the issue by unconditionally saving the
        default values (e.g. ChartStyle=bar+table+sql) into the Saved Chart
        record, fixing all codepaths including dashboards.
    
    Fixes: I#31557

diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 57cd0ec..284e4b3 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -63,7 +63,11 @@ my $saved_search = $m->comp( '/Widgets/SavedSearch:new',
     SearchFields => [@search_fields],
 );
 
-my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self => $saved_search );
+my @actions = $m->comp( '/Widgets/SavedSearch:process',
+    args     => \%ARGS,
+    defaults => $default_value,
+    self     => $saved_search,
+);
 
 my %query;
 
diff --git a/share/html/Widgets/SavedSearch b/share/html/Widgets/SavedSearch
index d029203..022ec2d 100644
--- a/share/html/Widgets/SavedSearch
+++ b/share/html/Widgets/SavedSearch
@@ -115,6 +115,9 @@ if ( $args->{SavedSearchSave} ) {
     }
     else {
         # new saved search
+
+        $SearchParams->{$_} //= $defaults->{$_} for @{$self->{SearchFields}};
+
         my $saved_search = RT::SavedSearch->new( $session{'CurrentUser'} );
         my ( $ok, $search_msg ) = $saved_search->Save(
             Privacy      => $args->{'SavedSearchOwner'},
@@ -151,6 +154,7 @@ return @actions;
 <%ARGS>
 $self
 $args
+$defaults => {}
 </%ARGS>
 
 </%method>

commit 11db86c3edb7cfcf0de9ae98133c70ffb61801ae
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 11 19:25:56 2016 +0000

    Avoid setting last updated and creating a txn for SLA upgrades
    
        This is just moving internal data around, so it's not worth
        screwing up reporting and adding noise to the history.
    
    Fixes: I#31924

diff --git a/etc/upgrade/upgrade-sla.in b/etc/upgrade/upgrade-sla.in
index 5e8635c..d88ed92 100644
--- a/etc/upgrade/upgrade-sla.in
+++ b/etc/upgrade/upgrade-sla.in
@@ -56,12 +56,25 @@ use lib "@RT_LIB_PATH@";
 use RT::Interface::CLI qw(Init);
 Init();
 
-my $tickets = RT::Tickets->new(RT->SystemUser);
-$tickets->FromSQL('CF.SLA IS NOT NULL AND SLA IS NULL');
-while ( my $ticket = $tickets->Next ) {
-    my ($ret, $msg) = $ticket->SetSLA($ticket->FirstCustomFieldValue('SLA'));
-    unless ( $ret ) {
-        RT->Logger->error("Failed to upgrade SLA for ticket #" . $ticket->id . ": $msg");
+{
+    local *RT::Ticket::_SetLastUpdated = sub {
+        return (1, "Migrating SLA from CF to core field silently");
+    };
+
+    my $tickets = RT::Tickets->new(RT->SystemUser);
+    $tickets->FromSQL('CF.SLA IS NOT NULL AND SLA IS NULL');
+    while ( my $ticket = $tickets->Next ) {
+        my $SLA = $ticket->FirstCustomFieldValue('SLA');
+
+        my ($ret, $msg) = $ticket->_Set(
+            Field => 'SLA',
+            Value => $SLA,
+            RecordTransaction => 0,
+        );
+
+        unless ( $ret ) {
+            RT->Logger->error("Failed to upgrade SLA for ticket #" . $ticket->id . ": $msg");
+        }
     }
 }
 

commit fbf5d07358478b7d0f5f49881feee1a6fc618134
Merge: bf2e9c2 c2dfb32
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 12 16:52:32 2016 -0400

    Merge branch '4.4/attachment-error-i18n' into 4.4-trunk


commit 707e1014b8699c40788032f1786f16be19fb9117
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 4 16:29:10 2016 -0400

    Update authentication documentation
    
    Update authentication docs to reflect ExternalAuth
    and LDAPImport becoming part of core RT.
    
    Fixes: I#31861

diff --git a/docs/authentication.pod b/docs/authentication.pod
index 26599cd..433d8b9 100644
--- a/docs/authentication.pod
+++ b/docs/authentication.pod
@@ -22,11 +22,16 @@ username and password into RT's login form, and in the other your web server
 (such as Apache) handles authentication, often seamlessly, and tells RT the
 user logged in.
 
-The second is supported by RT out of the box under the configuration option
-C<$WebRemoteUserAuth> and other related options.  The first is supported by an RT
-extension named L</RT::Authen::ExternalAuth>.  These two types may be used
-independently or together, and both can fallback to RT's internal
-authentication.
+Starting with RT 4.4, both of these options are supported by RT out of the
+box, activated using different configuration options. The first is supported
+by the L<RT::Authen::ExternalAuth> module. The second is activated using
+the configuration option C<$WebRemoteUserAuth> along with some related
+options. These two types may be used independently or together, and both
+can fallback to RT's internal authentication.
+
+If you are running a version of RT earlier than 4.4, you can install
+L<RT::Authen::ExternalAuth|https://metacpan.org/pod/RT::Authen::ExternalAuth>
+as an extension.
 
 No matter what type of external authentication you use, RT still maintains user
 records in its database that correspond to your external source.  This is
@@ -36,7 +41,7 @@ All that is necessary for integration with external authentication systems is a
 shared username or email address.  However, in RT you may want to leverage
 additional information from your external source.  Synchronization of users,
 user data, and groups is provided by an extension named
-L</RT::Extension::LDAPImport>.  It uses an external LDAP source, such an
+L<RT::LDAPImport>.  It uses an external LDAP source, such an
 OpenLDAP or Active Directory server, as the authoritative repository and keeps
 RT up to date accordingly.  This can be used in tandem with any of the external
 authentication options as it does not provide any authentication itself.
@@ -57,10 +62,10 @@ The flexibility of RT's C<$WebRemoteUserAuth> support means that it can be setup
 with almost any authentication system.
 
 In order to keep user data in sync, this type of external auth is almost always
-used in combination with one or both of L</RT::Authen::ExternalAuth> and
-L</RT::Extension::LDAPImport>.
+used in combination with one or both of L<RT::Authen::ExternalAuth> and
+L<RT::LDAPImport>.
 
-=head3 Apache configuration
+=head3 Apache Configuration
 
 When configuring Apache to protect RT, remember that the RT mail gateway
 uses the web interface to upload the incoming email messages.  You will
@@ -77,10 +82,12 @@ An example of using LDAP authentication and HTTP Basic auth:
             "ldap://ldap.example.com/dc=example,dc=com"
     </Location>
     <Location /REST/1.0/NoAuth/mail-gateway>
-        <IfVersion >= 2.4> # For Apache 2.4
+        # For Apache 2.4
+        <IfVersion >= 2.4>
             Require local
         </IfVersion>
-        <IfVersion < 2.4>  # For Apache 2.2
+        # For Apache 2.2
+        <IfVersion < 2.4>
             Order deny,allow
             Deny from all
             Allow from localhost
@@ -89,11 +96,11 @@ An example of using LDAP authentication and HTTP Basic auth:
     </Location>
 
 
-=head3 RT configuration options
+=head3 RT Configuration Options
 
-All of the following options control the behaviour of RT's built-in external
+All of the following options control the behavior of RT's built-in external
 authentication which relies on the web server.  They are documented in detail
-under the "Authorization and user configuration" section of C<etc/RT_Config.pm>
+under the "Authorization and user configuration" section of L<RT_Config>
 and you can read the documentation by running C<perldoc /opt/rt4/etc/RT_Config.pm>.
 
 The list below is meant to make you aware of what's available.  You should read
@@ -137,13 +144,18 @@ the C<Name> field.
 
 =head2 Via RT's login form, aka RT::Authen::ExternalAuth
 
-L<RT::Authen::ExternalAuth> is an RT extension which provides authentication
+L<RT::Authen::ExternalAuth> provides authentication
 B<using> RT's login form.  It can be configured to talk to an LDAP source (such
 as Active Directory), an external database, or an SSO cookie.
 
-The key difference between C<$WebRemoteUserAuth> and L<RT::Authen::ExternalAuth>
-is the use of the RT login form and what part of the system talks to your
-authentication source (your web server vs. RT itself).
+The key difference between C<$WebRemoteUserAuth> and
+L<RT::Authen::ExternalAuth> is the use of the RT login form and what
+part of the system talks to your authentication source (your web
+server vs. RT itself).
+
+As noted above, for versions of RT before 4.4, you can install
+L<RT::Authen::ExternalAuth|https://metacpan.org/pod/RT::Authen::ExternalAuth>
+as an extension.
 
 =head3 Info mode and Authentication mode
 
@@ -160,9 +172,13 @@ logs in.
 
 =head2 RT::Extension::LDAPImport
 
-L<RT::Extension::LDAPImport> provides no authentication, but is worth
-mentioning because it provides user data and group member synchronization from
-any LDAP source into RT.  It provides a similar but more complete sync solution
-than L<RT::Authen::ExternalAuth> (which only updates upon login and doesn't
-handle groups).  It may be used with either of RT's external authentication
-sources, or on it's own.
+L<RT::LDAPImport> provides no authentication, but
+is useful alongside authentication because it provides user data and group
+member synchronization from any LDAP source into RT.  It provides a similar
+but more complete sync solution than L<RT::Authen::ExternalAuth> (which
+only updates upon login and doesn't handle groups).  It may be used with
+either of RT's external authentication sources, or on it's own.
+
+Starting with RT 4.4, L<RT::LDAPImport> is part of RT. For
+earlier versions of RT, you can install L<RT::Extension::LDAPImport> as
+an extension.
diff --git a/lib/RT/Authen/ExternalAuth.pm b/lib/RT/Authen/ExternalAuth.pm
index 8081a8b..eff73bb 100644
--- a/lib/RT/Authen/ExternalAuth.pm
+++ b/lib/RT/Authen/ExternalAuth.pm
@@ -71,9 +71,11 @@ access it.
 
 =head1 CONFIGURATION
 
-L<RT::Authen::ExternalAuth> provides a lot of flexibility with many
+C<RT::Authen::ExternalAuth> provides a lot of flexibility with many
 configuration options.  The following describes these configuration options,
-and provides a complete example.
+and provides a complete example. As with all RT configuration, you set
+these values in C<RT_SiteConfig.pm> or for RT 4.4 or later in a custom
+configuration file in the directory C<RT_SiteConfig.d>.
 
 =over 4
 

commit 330ef55309c55f2cf7c89e348f5ed3b7696a6f45
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu May 12 23:27:36 2016 +0000

    Allow caller to specify whether test db should be dropped

diff --git a/lib/RT/Test.pm b/lib/RT/Test.pm
index aa9ec20..381c24b 100644
--- a/lib/RT/Test.pm
+++ b/lib/RT/Test.pm
@@ -616,14 +616,20 @@ sub _get_dbh {
 }
 
 sub __create_database {
+    my %args = (
+        # already dropped db in parallel tests, need to do so for other cases.
+        DropDatabase => $ENV{RT_TEST_PARALLEL} ? 0 : 1,
+
+        @_,
+    );
+
     # bootstrap with dba cred
     my $dbh = _get_dbh(
         RT::Handle->SystemDSN,
         $ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
     );
 
-    unless ( $ENV{RT_TEST_PARALLEL} ) {
-        # already dropped db in parallel tests, need to do so for other cases.
+    if ($args{DropDatabase}) {
         __drop_database( $dbh );
 
     }

commit 415264de9234ab08f0c66d29e4c1d076678b8a40
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Nov 10 18:00:53 2015 +0000

    Add language selector for dashboard subscriptions

diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm
index 1786c06..fae8c27 100644
--- a/lib/RT/Dashboard/Mailer.pm
+++ b/lib/RT/Dashboard/Mailer.pm
@@ -109,6 +109,7 @@ sub MailDashboards {
             my $recipients_groups = $recipients->{Groups};
 
             my @emails;
+            my %recipient_language;
 
             # add users' emails to email list
             for my $user_id (@{ $recipients_users || [] }) {
@@ -117,6 +118,7 @@ sub MailDashboards {
                 next unless $user->id;
 
                 push @emails, $user->EmailAddress;
+                $recipient_language{$user->EmailAddress} = $user->Lang;
             }
 
             # add emails for every group's members
@@ -128,12 +130,19 @@ sub MailDashboards {
                 my $users = $group->UserMembersObj;
                 while (my $user = $users->Next) {
                     push @emails, $user->EmailAddress;
+                    $recipient_language{$user->EmailAddress} = $user->Lang;
                 }
             }
 
             my $email_success = 0;
             for my $email (uniq @emails) {
                 eval {
+                    my $lang = $subscription->SubValue('Language')
+                            || $recipient_language{$email}
+                            || 'en';
+
+                    $currentuser->{'LangHandle'} = RT::I18N->get_handle($lang);
+
                     $self->SendDashboard(
                         %args,
                         CurrentUser  => $currentuser,
diff --git a/share/html/Dashboards/Render.html b/share/html/Dashboards/Render.html
index 0306077..37ee4fc 100644
--- a/share/html/Dashboards/Render.html
+++ b/share/html/Dashboards/Render.html
@@ -55,6 +55,15 @@
 <& /Elements/Tabs &>
 % }
 
+% # honor the chosen language for just the dashboard content
+% my $original_handle;
+% if ($SubscriptionObj->id) {
+%     if (my $lang = $SubscriptionObj->SubValue('Language')) {
+%         $original_handle = $session{'CurrentUser'}->{'LangHandle'};
+%         $session{'CurrentUser'}->{'LangHandle'} = RT::I18N->get_handle($lang);
+%     }
+% }
+
 % $m->callback(CallbackName => 'BeforeTable', Dashboard => $Dashboard, show_cb => $show_cb);
 
 <table class="dashboard" id="dashboard-<%$id%>">
@@ -91,6 +100,10 @@
 </html>
 % }
 
+% # restore the original language for anything else on the page
+% if ($original_handle) {
+%     $session{'CurrentUser'}->{'LangHandle'} = $original_handle;
+% }
 <%INIT>
 
 
diff --git a/share/html/Dashboards/Subscription.html b/share/html/Dashboards/Subscription.html
index cb45f1b..73f2557 100644
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@ -165,6 +165,15 @@
 (<%$timezone%>)
 </td></tr>
 <tr><td class="label">
+<&|/l&>Language</&>:
+</td><td class="value">
+<& /Elements/SelectLang,
+    Name => 'Language',
+    Default => $fields{'Language'},
+    ShowNullOption => 1,
+ &>
+</td></tr>
+<tr><td class="label">
 <&|/l&>Rows</&>:
 </td><td class="value">
 <select name="Rows">
@@ -228,6 +237,7 @@ my %fields = (
     Recipients  => { Users => [], Groups => [] },
     Fow         => 1,
     Counter     => 0,
+    Language    => '',
 );
 
 # update any fields with the values from the subscription object
@@ -360,6 +370,8 @@ $Hour        => undef
 $Dow         => undef
 $Dom         => undef
 $Rows        => undef
+$Recipient   => undef
+$Language    => undef
 
 $UserField => undef
 $UserOp => undef
diff --git a/t/mail/dashboards.t b/t/mail/dashboards.t
index 3d004ae..6f803ed 100644
--- a/t/mail/dashboards.t
+++ b/t/mail/dashboards.t
@@ -91,6 +91,8 @@ my ($dashboard_id, $subscription_id) = get_dash_sub_ids();
 sub produces_dashboard_mail_ok { # {{{
     my %args = @_;
     my $subject = delete $args{Subject};
+    my $body_like = delete $args{BodyLike};
+    my $body_unlike = delete $args{BodyUnlike};
 
     local $Test::Builder::Level = $Test::Builder::Level + 1;
 
@@ -107,8 +109,20 @@ sub produces_dashboard_mail_ok { # {{{
     is($mail->head->get('X-RT-Dashboard-Subscription-Id'), "$subscription_id\n");
 
     my $body = $mail->bodyhandle->as_string;
-    like($body, qr{My dashboards});
+    like($body, qr{My dashboards}) if !$body_like && !$body_unlike;
     like($body, qr{<a href="http://[^/]+/Dashboards/\d+/Testing!">Testing!</a>});
+
+    if ($body_like) {
+        for my $re (ref($body_like) eq 'ARRAY' ? @$body_like : $body_like) {
+            ok($body =~ $re, "body should match $re");
+        }
+    }
+
+    if ($body_unlike) {
+        for my $re (ref($body_unlike) eq 'ARRAY' ? @$body_unlike : $body_unlike) {
+            ok($body !~ $re, "body should not match $re");
+        }
+    }
 } # }}}
 
 sub produces_no_dashboard_mail_ok { # {{{
@@ -318,6 +332,7 @@ create_dashboard($baseurl, $m);
 create_subscription($baseurl, $m,
     Frequency => 'monthly',
     Hour => '06:00',
+    Language => 'fr',
 );
 
 ($dashboard_id, $subscription_id) = get_dash_sub_ids();
@@ -326,8 +341,10 @@ $good_time = 1291201200;        # dec 1
 $bad_time = $good_time - 86400; # day before (i.e. different month)
 
 produces_dashboard_mail_ok(
-    Time    => $good_time,
-    Subject =>  "[example.com] a Monthly b Testing! c\n",
+    Time       => $good_time,
+    Subject    => "[example.com] a Mensuel b Testing! c\n",
+    BodyLike   => qr/Mes tableaux de bord/,
+    BodyUnlike => qr/My dashboards/,
 );
 
 produces_no_dashboard_mail_ok(

commit 9e7da569747c0d635467460888645d83c56894cc
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 17:16:21 2016 +0000

    Add config option EmailDashboardLanguageOrder

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 35ac0a2..b683629 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -760,6 +760,33 @@ mailed dashboards.
 
 Set(@EmailDashboardRemove, ());
 
+=item C<@EmailDashboardLanguageOrder>
+
+A list that specifies which language to use for dashboard subscription email.
+There are several special keys:
+
+* _subscription: the language chosen on the dashboard subscription page
+* _recipient: the recipient's language, as chosen on their "About Me" page
+* _subscriber: the subscriber's language, as chosen on their "About Me" page
+
+The first key that produces a value is used for the email. Be aware that users
+may not actually have a language set on their "About Me" page, since RT falls
+back to the language their web browser specifies (and of course in a scheduled
+email dashboard, there is no web browser).
+
+You may also include a specific language as a fallback when there is no
+language specified otherwise. Using a specific language never fails to produce
+a value, so subsequent values in the list will never be considered.
+
+By default, RT examines the subscription, then the recipient, then subscriber,
+then finally falls back to English.
+
+See also L</@LexiconLanguages>.
+
+=cut
+
+Set(@EmailDashboardLanguageOrder, qw(_subscription _recipient _subscriber en));
+
 =back
 
 
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index ded8e81..427598f 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -795,6 +795,7 @@ our %META;
     },
     GnuPGOptions => { Type => 'HASH' },
     ReferrerWhitelist => { Type => 'ARRAY' },
+    EmailDashboardLanguageOrder  => { Type => 'ARRAY' },
     WebPath => {
         PostLoadCheck => sub {
             my $self  = shift;
diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm
index fae8c27..0b43a1e 100644
--- a/lib/RT/Dashboard/Mailer.pm
+++ b/lib/RT/Dashboard/Mailer.pm
@@ -94,6 +94,8 @@ sub MailDashboards {
         my $currentuser = RT::CurrentUser->new;
         $currentuser->LoadByName($user->Name);
 
+        my $subscriber_lang = $user->Lang;
+
         # look through this user's subscriptions, are any supposed to be generated
         # right now?
         for my $subscription ($user->Attributes->Named('Subscription')) {
@@ -137,9 +139,42 @@ sub MailDashboards {
             my $email_success = 0;
             for my $email (uniq @emails) {
                 eval {
-                    my $lang = $subscription->SubValue('Language')
-                            || $recipient_language{$email}
-                            || 'en';
+                    my $lang;
+                    for my $langkey (RT->Config->Get('EmailDashboardLanguageOrder')) {
+                        if ($langkey eq '_subscription') {
+                            if ($lang = $subscription->SubValue('Language')) {
+                                $RT::Logger->debug("Using subscription's specified language '$lang'");
+                                last;
+                            }
+                        }
+                        elsif ($langkey eq '_recipient') {
+                            if ($lang = $recipient_language{$email}) {
+                                $RT::Logger->debug("Using recipient's preferred language '$lang'");
+                                last;
+                            }
+                        }
+                        elsif ($langkey eq '_subscriber') {
+                            if ($lang = $subscriber_lang) {
+                                $RT::Logger->debug("Using subscriber's preferred language '$lang'");
+                                last;
+                            }
+                        }
+                        else { # specific language name
+                            $lang = $langkey;
+                            $RT::Logger->debug("Using EmailDashboardLanguageOrder fallback language '$lang'");
+                            last;
+                        }
+                    }
+
+                    # use English as the absolute fallback. Though the config
+                    # lets you specify a site-specific fallback, it also lets
+                    # you not specify a fallback, and we don't want to
+                    # accidentally reuse whatever language the previous
+                    # recipient happened to have
+                    if (!$lang) {
+                        $RT::Logger->debug("Using RT's fallback language 'en'. You may specify a different fallback language in your config with EmailDashboardLanguageOrder.");
+                        $lang = 'en';
+                    }
 
                     $currentuser->{'LangHandle'} = RT::I18N->get_handle($lang);
 
diff --git a/t/mail/dashboards.t b/t/mail/dashboards.t
index 6f803ed..fde81c6 100644
--- a/t/mail/dashboards.t
+++ b/t/mail/dashboards.t
@@ -325,6 +325,7 @@ RT::Test->clean_caught_mails;
 RT::Test->stop_server;
 
 RT->Config->Set('EmailDashboardRemove' => ());
+RT->Config->Set('EmailDashboardLanguageOrder' => qw(_subscription _recipient _subscriber fr));
 RT->Config->Set('DashboardAddress' => 'root');
 ($baseurl, $m) = RT::Test->started_ok;
 $m->login;
@@ -332,7 +333,47 @@ create_dashboard($baseurl, $m);
 create_subscription($baseurl, $m,
     Frequency => 'monthly',
     Hour => '06:00',
-    Language => 'fr',
+);
+
+($dashboard_id, $subscription_id) = get_dash_sub_ids();
+
+$good_time = 1291201200;        # dec 1
+
+produces_dashboard_mail_ok(
+    Time       => $good_time,
+    Subject    => "[example.com] a Mensuel b Testing! c\n",
+    BodyLike   => qr/Mes tableaux de bord/,
+    BodyUnlike => qr/My dashboards/,
+);
+
+
+
+ at mails = RT::Test->fetch_caught_mails;
+is(@mails, 0, "no mail leftover");
+
+$m->no_warnings_ok;
+RT::Test->stop_server;
+RT->Config->Set('DashboardSubject' => 'a %s b %s c');
+RT->Config->Set('DashboardAddress' => 'dashboard at example.com');
+RT->Config->Set('EmailDashboardRemove' => (qr/My dashboards/, "Testing!"));
+($baseurl, $m) = RT::Test->started_ok;
+
+delete_dashboard($dashboard_id);
+
+RT::Test->clean_caught_mails;
+
+RT::Test->stop_server;
+
+RT->Config->Set('EmailDashboardRemove' => ());
+RT->Config->Set('EmailDashboardLanguage' => 'ja');
+RT->Config->Set('DashboardAddress' => 'root');
+($baseurl, $m) = RT::Test->started_ok;
+$m->login;
+create_dashboard($baseurl, $m);
+create_subscription($baseurl, $m,
+    Frequency => 'monthly',
+    Hour => '06:00',
+    Language => 'fr', # overrides EmailDashboardLanguage
 );
 
 ($dashboard_id, $subscription_id) = get_dash_sub_ids();

commit 4a588f963ca6ff9035bc99f41f04168cddd98799
Author: rachelkelly <rachel at bestpractical.com>
Date:   Tue May 3 10:05:24 2016 -0700

    Add Pod::Select to dependencies for shredder
    
    Pod::Select was removed from Perl core (5.18 and higher), so in order to
    continue to use Pod::Select we explicitly include it.
    
    Fixes: I#31873

diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in
index b930e47..326686c 100644
--- a/sbin/rt-test-dependencies.in
+++ b/sbin/rt-test-dependencies.in
@@ -251,6 +251,7 @@ Net::CIDR
 Net::IP
 Plack 1.0002
 Plack::Handler::Starlet
+Pod::Select
 Regexp::Common
 Regexp::Common::net::CIDR
 Regexp::IPv6

commit 02b065978ec3a50b6f9f6af21cd79aa6ae6ffe2f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 5 16:06:31 2016 -0400

    Automatically enable ExternalAuth when ExternalAuth options enabled
    
    ExternalAuth uses three configuration options to configure the
    service and also requires a flag, ExternalAuth, to be set to
    enable it. The presence of the three configuration options is
    enough to confirm the user would like to use ExternalAuth, so
    automatically enable it when these are selected.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 2aad19c..65da8c5 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1000,16 +1000,6 @@ our %META;
         },
     },
 
-    ExternalAuth => {
-        PostLoadCheck => sub {
-            my $self = shift;
-            my $ExternalAuthEnabled = $self->Get('ExternalAuth');
-            if ( $ExternalAuthEnabled ) {
-                require RT::Authen::ExternalAuth;
-            }
-        }
-    },
-
     ExternalSettings => {
         Obfuscate => sub {
             # Ensure passwords are obfuscated on the System Configuration page
@@ -1026,6 +1016,7 @@ our %META;
         PostLoadCheck => sub {
             my $self = shift;
             my $settings = shift || {};
+            $self->EnableExternalAuth();
 
             my $remove = sub {
                 my ($service) = @_;
@@ -1075,6 +1066,8 @@ our %META;
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };
+            $self->EnableExternalAuth();
+
             if (not @values) {
                 $self->Set( 'ExternalAuthPriority', \@values );
                 return;
@@ -1093,6 +1086,8 @@ our %META;
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };
+            $self->EnableExternalAuth();
+
             if (not @values) {
                 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");
                 $self->Set( 'ExternalInfoPriority', \@values );
@@ -1716,6 +1711,16 @@ sub ObjectHasCustomFieldGrouping {
     return 0;
 }
 
+# Internal method to activate ExtneralAuth if any ExternalAuth config
+# options are set.
+sub EnableExternalAuth {
+    my $self = shift;
+
+    $self->Set('ExternalAuth', 1);
+    require RT::Authen::ExternalAuth;
+    return;
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit 2bd8b433bf6ba39d357384fee1bb53ba35e6066f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 5 16:12:29 2016 -0400

    Add messages to log ExternalAuth configuration errors

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 65da8c5..44453ea 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1069,11 +1069,18 @@ our %META;
             $self->EnableExternalAuth();
 
             if (not @values) {
+                $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
                 $self->Set( 'ExternalAuthPriority', \@values );
                 return;
             }
-
-            my %settings = %{ $self->Get('ExternalSettings') };
+            my %settings;
+            if ( $self->Get('ExternalSettings') ){
+                %settings = %{ $self->Get('ExternalSettings') };
+            }
+            else{
+                $RT::Logger->error("ExternalSettings not defined. ExternalAuth requires the ExternalSettings configuration option to operate properly");
+                return;
+            }
             for my $key (grep {not $settings{$_}} @values) {
                 $RT::Logger->error("Removing '$key' from ExternalAuthPriority, as it is not defined in ExternalSettings");
             }
@@ -1094,7 +1101,14 @@ our %META;
                 return;
             }
 
-            my %settings = %{ $self->Get('ExternalSettings') };
+            my %settings;
+            if ( $self->Get('ExternalSettings') ){
+                %settings = %{ $self->Get('ExternalSettings') };
+            }
+            else{
+                $RT::Logger->error("ExternalSettings not defined. ExternalAuth requires the ExternalSettings configuration option to operate properly");
+                return;
+            }
             for my $key (grep {not $settings{$_}} @values) {
                 $RT::Logger->error("Removing '$key' from ExternalInfoPriority, as it is not defined in ExternalSettings");
             }

commit 401df010df49d13e66905644b1f1157f6d9051c1
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 5 16:20:51 2016 -0400

    Add tests for ExternalAuth configuration processing

diff --git a/t/externalauth/auth_config.t b/t/externalauth/auth_config.t
new file mode 100644
index 0000000..dda38c6
--- /dev/null
+++ b/t/externalauth/auth_config.t
@@ -0,0 +1,59 @@
+use strict;
+use warnings;
+use RT;
+my $config;
+BEGIN{
+    $config = <<'END';
+Set($ExternalSettings, {
+        'My_LDAP'       =>  {
+            'type'             =>  'ldap',
+            'server'           =>  'ldap.example.com',
+            # By not passing 'user' and 'pass' we are using an anonymous
+            # bind, which some servers to not allow
+            'base'             =>  'ou=Staff,dc=example,dc=com',
+            'filter'           =>  '(objectClass=inetOrgPerson)',
+            # Users are allowed to log in via email address or account
+            # name
+            'attr_match_list'  => [
+                'Name',
+                'EmailAddress',
+            ],
+            # Import the following properties of the user from LDAP upon
+            # login
+            'attr_map' => {
+                'Name'         => 'sAMAccountName',
+                'EmailAddress' => 'mail',
+                'RealName'     => 'cn',
+                'WorkPhone'    => 'telephoneNumber',
+                'Address1'     => 'streetAddress',
+                'City'         => 'l',
+                'State'        => 'st',
+                'Zip'          => 'postalCode',
+                'Country'      => 'co',
+            },
+        },
+    } );
+
+END
+
+}
+use RT::Test nodb => 1, tests => undef, config => $config;
+use Test::Warn;
+
+diag "Test ExternalAuth configuration processing";
+my $auth_settings = RT::Config->Get('ExternalSettings');
+ok( $auth_settings, 'Got ExternalSettings');
+is( $auth_settings->{'My_LDAP'}{'type'}, 'ldap', 'External Auth type is ldap');
+ok( RT::Config->Get('ExternalAuth'), 'ExternalAuth activated automatically');
+
+ok( RT::Config->Set('ExternalAuthPriority', ['My_LDAP']),'Set ExternalAuthPriority');
+ok( RT::Config->Set('ExternalInfoPriority', ['My_LDAP']),'Set ExternalInfoPriority');
+
+ok( RT::Config->Set( 'ExternalSettings', undef ), 'unset ExternalSettings' );
+ok( !(RT::Config->Get('ExternalSettings')), 'ExternalSettings removed');
+
+warnings_like {RT::Config->PostLoadCheck} [qr/ExternalSettings not defined/,
+    qr/ExternalSettings not defined/],
+  'Correct warnings with ExternalSettings missing';
+
+done_testing;

commit 8946b1b85418f25364a4ad41a2467bc434759720
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 5 16:29:33 2016 -0400

    Remove explicit setting of ExternalAuth in tests

diff --git a/t/externalauth/ldap.t b/t/externalauth/ldap.t
index 994563c..34dcf83 100644
--- a/t/externalauth/ldap.t
+++ b/t/externalauth/ldap.t
@@ -27,8 +27,6 @@ my $entry    = {
 $ldap->add( $base );
 $ldap->add( $dn, attr => [%$entry] );
 
-RT->Config->Set( ExternalAuth => 1 );
-
 RT->Config->Set( ExternalAuthPriority        => ['My_LDAP'] );
 RT->Config->Set( ExternalInfoPriority        => ['My_LDAP'] );
 RT->Config->Set( AutoCreateNonExternalUsers  => 0 );
diff --git a/t/externalauth/ldap_escaping.t b/t/externalauth/ldap_escaping.t
index cce4e0c..0442313 100644
--- a/t/externalauth/ldap_escaping.t
+++ b/t/externalauth/ldap_escaping.t
@@ -48,8 +48,6 @@ $ldap->add(
     ],
 );
 
-RT->Config->Set( ExternalAuth => 1 );
-
 RT->Config->Set( ExternalAuthPriority        => ['My_LDAP'] );
 RT->Config->Set( ExternalInfoPriority        => ['My_LDAP'] );
 RT->Config->Set( AutoCreateNonExternalUsers  => 0 );
diff --git a/t/externalauth/ldap_group.t b/t/externalauth/ldap_group.t
index ede53a2..a3d87a8 100644
--- a/t/externalauth/ldap_group.t
+++ b/t/externalauth/ldap_group.t
@@ -55,8 +55,6 @@ $ldap->add(
     ],
 );
 
-RT->Config->Set( ExternalAuth => 1 );
-
 #RT->Config->Set( Plugins                     => 'RT::Authen::ExternalAuth' );
 RT->Config->Set( ExternalAuthPriority        => ['My_LDAP'] );
 RT->Config->Set( ExternalInfoPriority        => ['My_LDAP'] );
diff --git a/t/externalauth/ldap_privileged.t b/t/externalauth/ldap_privileged.t
index 26f1862..fe5e05a 100644
--- a/t/externalauth/ldap_privileged.t
+++ b/t/externalauth/ldap_privileged.t
@@ -26,8 +26,6 @@ my $entry    = {
 $ldap->add( $base );
 $ldap->add( $dn, attr => [%$entry] );
 
-RT->Config->Set( ExternalAuth => 1 );
-
 RT->Config->Set( ExternalAuthPriority        => ['My_LDAP'] );
 RT->Config->Set( ExternalInfoPriority        => ['My_LDAP'] );
 RT->Config->Set( AutoCreateNonExternalUsers  => 0 );
diff --git a/t/externalauth/sessions.t b/t/externalauth/sessions.t
index 98eca0c..9b3ec36 100644
--- a/t/externalauth/sessions.t
+++ b/t/externalauth/sessions.t
@@ -94,8 +94,6 @@ sub setup_auth_source {
             SQL
     }
 
-    RT->Config->Set( ExternalAuth => 1 );
-
     RT->Config->Set( ExternalAuthPriority        => ['My_SQLite'] );
     RT->Config->Set( ExternalInfoPriority        => ['My_SQLite'] );
     RT->Config->Set( AutoCreateNonExternalUsers  => 0 );
diff --git a/t/externalauth/sqlite.t b/t/externalauth/sqlite.t
index 3214b7d..9e7c2cc 100644
--- a/t/externalauth/sqlite.t
+++ b/t/externalauth/sqlite.t
@@ -33,8 +33,6 @@ $dbh->do(
 "INSERT INTO $table VALUES ( 'testuser', '$password', 'testuser\@invalid.tld')"
 );
 
-RT->Config->Set( ExternalAuth => 1 );
-
 RT->Config->Set( ExternalAuthPriority        => ['My_SQLite'] );
 RT->Config->Set( ExternalInfoPriority        => ['My_SQLite'] );
 RT->Config->Set( AutoCreateNonExternalUsers  => 0 );

commit 97d8d9172b678db0cea911d994a395692a09f569
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu May 5 16:32:32 2016 -0400

    Remove ExternalAuth config option from upgrade notes

diff --git a/docs/UPGRADING-4.4 b/docs/UPGRADING-4.4
index f19c493..b641915 100644
--- a/docs/UPGRADING-4.4
+++ b/docs/UPGRADING-4.4
@@ -21,9 +21,8 @@ L<RT::Authen::ExternalAuth::DBI> documentation.
 
 Users of the existing
 L<RT::Authen::ExternalAuth|https://metacpan.org/pod/RT::Authen::ExternalAuth>
-extension should remove C<RT::Authen::ExternalAuth> from the plugins list,
-and add C<Set($ExternalAuth, 1);> to the F<RT_SiteConfig.pm> file. Please
-also remove F<local/plugins/RT-Authen-ExternalAuth> from your RT
+extension should remove C<RT::Authen::ExternalAuth> from the plugins list.
+Please also remove F<local/plugins/RT-Authen-ExternalAuth> from your RT
 installation.
 
 =item *

commit d91597ed06e582e533dcf57f51b3ec59882c8e31
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri May 6 15:03:52 2016 -0400

    Add ExternalAuth, LDAPImport options to RT_Config
    
    Fixes: I#31464

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 6852ff2..2861123 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2372,6 +2372,104 @@ Set($MinimumPasswordLength, 5);
 
 =back
 
+=head2 External Authentication and Authorization
+
+RT has a built-in module for integrating with a directory service like
+LDAP or Active Directory for authentication (login) and authorization
+(enabling/disabling users and setting user attributes). The core configuration
+settings for the service are listed here. Additional details are available
+in the L<RT::Authen::ExternalAuth> module documentation.
+
+=over 4
+
+=item C<$ExternalSettings>
+
+This option, along with the following options, activate and configure authentication
+via a resource external to RT. All of the configuration for your external authentication
+service, like LDAP or Active Directory, are defined in a data structure in this option.
+You can find full details on the configuration
+options in the L<RT::Authen::ExternalAuth> documentation.
+
+=cut
+
+# No defaults are set for ExternalAuth because this is an optional feature.
+
+=item C<$ExternalAuthPriority>
+
+Sets the priority of authentication resources if you have multiple configured.
+RT will attempt authorization with each resource, in order, until one succeeds or
+no more remain. See L<RT::Authen::ExternalAuth> for details.
+
+=item C<$ExternalInfoPriority>
+
+Sets the order of resources for querying user information if you have multiple
+configured. RT will query each resource, in order, until one succeeds or
+no more remain. See L<RT::Authen::ExternalAuth> for details.
+
+=item C<$UserAutocreateDefaultsOnLogin>
+
+A hashref of options to set for users who are autocreated on login via
+ExternalAuth. For example, you can automatically make "Privileged" users
+who were authenticated and created from LDAP or Active Directory.
+See L<RT::Authen::ExternalAuth> for details.
+
+=item C<$AutoCreateNonExternalUsers>
+
+Users should still be autocreated by RT as internal users if they
+fail to exist in an external service; this is so requestors who
+are not in LDAP can still be created when they email in.
+See L<RT::Authen::ExternalAuth> for details.
+
+=back
+
+=head2 Syncing Users and Groups with LDAP or AD
+
+In addition to the authentication services described above, RT also
+has a utility you can schedule to periodicially sync from your
+directory service additional user attributes, new users,
+disabled users, and group membership. Options for the
+LDAPImport tool are listed here. Additional information is
+available in the L<RT::LDAPImport> documentation.
+
+=over 4
+
+=item C<$LDAPHost>
+
+Your LDAP server hostname.
+
+=item C<$LDAPUser>
+
+The LDAP user to log in with.
+
+=item C<$LDAPPassword>
+
+Password for LDAPUser.
+
+=item C<$LDAPFilter>
+
+The filter to use when querying LDAP for the set of users to sync.
+
+=item C<$LDAPMapping>
+
+Mapping to apply between LDAP attributes retrieved and RT user
+record attributes. See the L<RT::LDAPImport> documentation
+for details.
+
+=item C<$LDAPGroupBase>
+
+The base for the LDAP group search.
+
+=item C<$LDAPGroupFilter>
+
+The filter to use when querying LDAP for groups to sync.
+
+=item C<$LDAPGroupMapping>
+
+Mapping to apply between LDAP group member attributes retrieved and
+RT groups. See the L<RT::LDAPImport> documentation
+for details.
+
+=back
 
 =head1 Internationalization
 

commit 1978c53cc7d2e07f7791467520eb7f2a05c3523a
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 10 14:09:38 2016 -0400

    Fix broken pod link in SLA docs

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 2861123..aac41e3 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -3543,7 +3543,7 @@ Read more about how to describe a schedule in L<Business::Hours>.
 
 =over 4
 
-=item Defining different business hours for service levels
+=item Configuring business hours
 
 Each level supports BusinessHours option to specify your own business
 hours.

commit f9bb838d6549dfae6780e53e97cecbb02ec98548
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 11 20:18:57 2016 +0000

    Failing tests for setting SLA during RT::Queue->Create

diff --git a/t/api/queue.t b/t/api/queue.t
index 71efb4d..e3c4fd2 100644
--- a/t/api/queue.t
+++ b/t/api/queue.t
@@ -2,7 +2,21 @@
 use strict;
 use warnings;
 use RT;
-use RT::Test nodata => 1, tests => undef;
+use RT::Test nodata => 1, tests => undef, config => <<'CONFIG';
+Set( %ServiceAgreements, (
+    Default => 'standard',
+    Levels => {
+        'standard' => {
+            Starts => { RealMinutes => 0 },
+            Resolve => { RealMinutes => 8*60 },
+        },
+        'urgent' => {
+            Starts => { RealMinutes => 0 },
+            Resolve => { RealMinutes => 2*60 },
+        },
+    },
+));
+CONFIG
 
 
 {
@@ -86,4 +100,19 @@ ok ($group->Id, "Found the AdminCc object for this Queue");
 
 }
 
+{
+    my $NoSLA = RT::Queue->new(RT->SystemUser);
+    my ($id, $msg) = $NoSLA->Create(Name => "NoSLA");
+    ok($id, "created queue NoSLA");
+    is($NoSLA->SLA, undef, 'No SLA for NoSLA');
+
+    my $WithSLA = RT::Queue->new(RT->SystemUser);
+    ($id, $msg) = $WithSLA->Create(Name => "WithSLA", SLA => 'urgent');
+    ok($id, "created queue WithSLA");
+    is($WithSLA->SLA, 'urgent', 'SLA is set');
+
+    $WithSLA->SetSLA('standard');
+    is($WithSLA->SLA, 'standard', 'SLA is updated');
+}
+
 done_testing;

commit 21a1ea186b47dc9f2f46697372c675f9a9f0b0bc
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 11 20:14:58 2016 +0000

    Allow setting SLA in RT::Queue->Create
    
    Fixes: I#31823

diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index b3940b3..2a80a6c 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -159,6 +159,7 @@ sub Create {
         Sign              => undef,
         SignAuto          => undef,
         Encrypt           => undef,
+        SLA               => undef,
         _RecordTransaction => 1,
         @_
     );
@@ -198,7 +199,7 @@ sub Create {
     }
     $RT::Handle->Commit;
 
-    for my $attr (qw/Sign SignAuto Encrypt/) {
+    for my $attr (qw/Sign SignAuto Encrypt SLA/) {
         next unless defined $args{$attr};
         my $set = "Set" . $attr;
         my ($status, $msg) = $self->$set( $args{$attr} );

commit 0c7fddb7704a40618466172c279124646e6167fb
Merge: 330ef55 1978c53
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 16:08:34 2016 -0400

    Merge branch '4.4/update-external-auth-config' into 4.4-trunk


commit cf294390ca12c98cc60dd29aa29e4815deee5571
Merge: 0c7fddb 707e101
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 16:16:09 2016 -0400

    Merge branch '4.4/update-auth-documentation' into 4.4-trunk


commit 43f8e76a23b5f00384032586c697d63598d53240
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 20:44:14 2016 +0000

    Upgrade step for fixing incompletely-saved charts

diff --git a/etc/upgrade/4.4.1/content b/etc/upgrade/4.4.1/content
index 61f9327..73e992b 100644
--- a/etc/upgrade/4.4.1/content
+++ b/etc/upgrade/4.4.1/content
@@ -49,5 +49,28 @@ our @Initial = (
         }
         return 1;
     },
+    # fix incompletely-saved charts
+    sub {
+        my $attrs = RT::Attributes->new(RT->SystemUser);
+        $attrs->Limit( FIELD => 'Name', VALUE => 'SavedSearch' );
+        while ( my $attr = $attrs->Next ) {
+            my $content = $attr->Content;
+
+            next unless lc($content->{SearchType}) eq 'chart';
+
+            next if $content->{ChartStyle}
+                 && $content->{GroupBy}
+                 && $content->{ChartFunction};
+
+            $content->{ChartStyle} ||= 'bar+table+sql';
+            $content->{GroupBy} ||= ['Status'];
+            $content->{ChartFunction} ||= ['COUNT'];
+
+            my ($ret, $msg) = $attr->SetContent($content);
+            unless ( $ret ) {
+                RT->Logger->error("Failed to update chart for SavedSearch #" . $attr->id . ": $msg");
+            }
+        }
+    },
 );
 

commit 648581f6a5fe44a01fc7551970449cd222640486
Merge: cf29439 21a1ea1
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Fri May 13 21:03:32 2016 +0000

    Merge branch '4.4/queue-create-sla' into 4.4-trunk


commit a02bfc3c465e3c26063f3da391a31db10d07f6ee
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 21:38:33 2016 +0000

    Enable ExternalAuth only if the related settings have values
    
        PostLoadCheck is called even if the setting isn't explicitly
        set, causing us to unconditionally load ExternalAuth.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 99eeeab..e885643 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1025,7 +1025,8 @@ our %META;
         PostLoadCheck => sub {
             my $self = shift;
             my $settings = shift || {};
-            $self->EnableExternalAuth();
+
+            $self->EnableExternalAuth() if keys %$settings > 0;
 
             my $remove = sub {
                 my ($service) = @_;
@@ -1075,7 +1076,8 @@ our %META;
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };
-            $self->EnableExternalAuth();
+
+            $self->EnableExternalAuth() if @values;
 
             if (not @values) {
                 $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
@@ -1102,7 +1104,8 @@ our %META;
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };
-            $self->EnableExternalAuth();
+
+            $self->EnableExternalAuth() if @values;
 
             if (not @values) {
                 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");

commit 30de01288f6cd50c28eaf700c2c01578923a5dc2
Merge: a02bfc3 43f8e76
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Fri May 13 22:13:59 2016 +0000

    Merge branch '4.4/dashboard-charts' into 4.4-trunk


commit 5634d7cf2718fa001272bcd21585b9edbe83e453
Merge: 30de012 6a41722
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 18:23:48 2016 -0400

    Merge branch '4.2-trunk' into 4.4-trunk

diff --cc sbin/rt-fulltext-indexer.in
index 2c34e80,ceffc97..fd8bb1d
--- a/sbin/rt-fulltext-indexer.in
+++ b/sbin/rt-fulltext-indexer.in
@@@ -213,11 -203,19 +215,19 @@@ sub process_bulk_insert 
          my ($attachments) = @_;
          my @insert;
          my $found = 0;
+ 
          while ( my $a = $attachments->Next ) {
              debug("Found attachment #". $a->id );
+             if ( $max_size and $a->ContentLength > $max_size ){
+                 debug("Attachment #" . $a->id . " is " . $a->ContentLength .
+                       " bytes which is larger than configured MaxFulltextAttachmentSize " .
+                       " of " . $max_size . ", skipping");
+                 next;
+             }
+ 
              my $text = $a->Content // "";
              HTML::Entities::decode_entities($text) if $a->ContentType eq "text/html";
 -            push @insert, $text, $a->id;
 +            push @insert, join("\n", $a->Subject // "", $text), $a->id;
              $found++;
          }
          return unless $found;
diff --cc share/html/Dashboards/Subscription.html
index 61d38cc,83680ef..cb5ba16
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@@ -174,22 -170,16 +174,23 @@@
  </select>
  </td></tr>
  
 -<tr><td class="label">
 -<&|/l&>Recipient</&>:
 -</td><td class="value">
 -<input name="Recipient" id="Recipient" size="30" value="<%$fields{Recipient} ? $fields{Recipient} : ''%>" />
 -<div class="hints"><% loc("Leave blank to send to your current email address ([_1])", $session{'CurrentUser'}->EmailAddress) %></div>
 +<tr><td align="right"><input type="checkbox" id="SuppressIfEmpty" name="SuppressIfEmpty" value="1" <% $fields{'SuppressIfEmpty'} ? 'checked="checked"' : "" |n %> /></td>
 +<td><label for="SuppressIfEmpty"><&|/l&>Suppress if empty (Check this to avoid sending mail if all searches have no results)</&></label><br />
 +<input type="hidden"class="hidden" name="SuppressIfEmpty-Magic" value=1 />
  </td></tr>
- 
+ % $m->callback( %ARGS, CallbackName => 'SubscriptionFormEnd', FieldsRef => \%fields,
+ %     SubscriptionObj => $SubscriptionObj, DashboardObj => $Dashboard );
  </table>
  </&>
 +
 +<&| /Widgets/TitleBox, title => loc('Recipients') &>
 +<& Elements/SubscriptionRecipients,
 +    UserField => $UserField, UserString => $UserString, UserOp => $UserOp,
 +    GroupString => $GroupString, GroupOp => $GroupOp, GroupField => $GroupField,
 +    Recipients => $fields{Recipients},
 +    IsFirstSubscription => $SubscriptionObj ? 0 : 1 &>
 +</&>
 +
  </td>
  </tr>
  </table>
@@@ -231,12 -213,14 +232,15 @@@ my %fields = 
      Dow         => 'Monday',
      Dom         => 1,
      Rows        => 20,
 -    Recipient   => '',
 +    Recipients  => { Users => [], Groups => [] },
      Fow         => 1,
      Counter     => 0,
 +    SuppressIfEmpty => 0,
  );
  
+ $m->callback( %ARGS, CallbackName => 'SubscriptionFields', FieldsRef => \%fields,
+      SubscriptionObj => $SubscriptionObj, DashboardObj => $Dashboard);
+ 
  # update any fields with the values from the subscription object
  if ($SubscriptionObj) {
      for my $field (keys %fields) {
@@@ -248,66 -232,22 +252,69 @@@
  for my $field (keys %fields) {
      next if $field eq 'DashboardId'; # but this one is immutable
      $fields{$field} = $ARGS{$field}
 -        if defined($ARGS{$field});
 +        if defined($ARGS{$field}) || $ARGS{$field.'-Magic'};
  }
  
+ $m->callback( %ARGS, CallbackName => 'MassageSubscriptionFields', FieldsRef => \%fields,
+      SubscriptionObj => $SubscriptionObj, DashboardObj => $Dashboard);
+ 
  # this'll be defined on submit
  if (defined $ARGS{Save}) {
 -    my $ok = 1;
 -
 -    # validation
 -    if ($fields{Recipient}) {
 -        my @addresses = Email::Address->parse($fields{Recipient});
 -        if (@addresses == 0) {
 -            push @results, loc('Recipient must be an email address');
 -            $ok = 0;
 +    # update recipients
 +    for my $key (keys %ARGS) {
 +        my $val = $ARGS{$key};
 +        if ( $key =~ /^Dashboard-Subscription-Email-\d+$/ && $val ) {
 +            my @recipients = @{ $fields{Recipients}->{Users} };
 +
 +            for ( RT::EmailParser->ParseEmailAddress( $val ) ) {
 +                my ( $email, $name ) = ( $_->address, $_->name );
 +
 +                my $user = RT::User->new($session{CurrentUser});
 +                $user->LoadOrCreateByEmail(
 +                    EmailAddress => $email,
 +                    RealName     => $name,
 +                    Comments     => 'Autocreated when added as a dashboard subscription recipient',
 +                );
 +
 +                my $is_prev_recipient = grep { $_ == $user->id } @recipients;
 +                if ( not $is_prev_recipient ) {
 +                    push @recipients, $user->id;
 +                    push @results, loc("[_1] added to dashboard subscription recipients", $email);
 +                }
 +            }
 +            @{ $fields{Recipients}->{Users} } = uniq @recipients;
 +
 +        } elsif ($key =~ /^Dashboard-Subscription-(Users|Groups)-(\d+)$/) {
 +            my ($mode, $type, $id) = ('', $1, $2);
 +            my @recipients = @{ $fields{Recipients}->{$type} };
 +
 +            # find out proper value for user/group checkbox
 +            my $add_keep_recipient = ref $ARGS{$key} eq 'ARRAY' ?
 +                grep { $_ } @{ $ARGS{$key} } :
 +                $ARGS{$key};
 +
 +            my $record; # hold user/group object
 +            if ($type eq 'Users') {
 +                my $user = RT::User->new($session{CurrentUser});
 +                $user->Load( $id );
 +                $record = $user;
 +            } elsif ($type eq 'Groups') {
 +                my $group = RT::Group->new($session{CurrentUser});
 +                $group->Load( $id );
 +                $record = $group;
 +            }
 +
 +            my $is_prev_recipient = grep { $_ == $id } @recipients;
 +
 +            if ($add_keep_recipient and not $is_prev_recipient) { # Add User/Group
 +                push @recipients, $id;
 +                push @results, loc("[_1] added to dashboard subscription recipients", $record->Name);
 +            } elsif (not $add_keep_recipient and $is_prev_recipient) { # Remove User/Group
 +                @recipients = grep { $_ != $id } @recipients;
 +                push @results, loc("[_1] removed from dashboard subscription recipients", $record->Name);
 +            }
 +
 +            @{ $fields{Recipients}->{$type} } = uniq @recipients;
          }
      }
  
diff --cc share/html/Elements/BulkCustomFields
index 30b3624,2c3af38..88b9f58
--- a/share/html/Elements/BulkCustomFields
+++ b/share/html/Elements/BulkCustomFields
@@@ -81,13 -81,17 +81,17 @@@
  <td><& /Elements/EditCustomFieldWikitext, @add &></td>
  <td>
  % } elsif ($cf->Type eq 'Date') {
 -<td><& /Elements/EditCustomFieldDate, @add, Default => undef &></td>
 -<td><& /Elements/EditCustomFieldDate, @del, Default => undef &><br />
 +<td><& /Elements/EditCustomFieldDate, @add &></td>
 +<td><& /Elements/EditCustomFieldDate, @del &><br />
  % } elsif ($cf->Type eq 'DateTime') {
  % # Pass datemanip format to prevent another tz date conversion
- <td><& /Elements/EditCustomFieldDateTime, @add, Format => 'datemanip' &></td>
- <td><& /Elements/EditCustomFieldDateTime, @del, Format => 'datemanip' &><br />
+ <td><& /Elements/EditCustomFieldDateTime, @add, Default => undef, Format => 'datemanip' &></td>
+ <td><& /Elements/EditCustomFieldDateTime, @del, Default => undef, Format => 'datemanip' &><br />
+ % } elsif ($cf->Type eq 'Autocomplete') {
+ <td><& /Elements/EditCustomFieldAutocomplete, @add &></td>
+ <td><& /Elements/EditCustomFieldAutocomplete, @del &><br />
  % } else {
+     <td colspan="2"><em><&|/l&>(Unsupported custom field type)</&></em></td>
  %   $RT::Logger->crit("Unknown CustomField type: " . $cf->Type);
  %   next
  % }
diff --cc share/html/Elements/ShowTransactionAttachments
index d9aa8ba,e0a4b50..34a6ad1
--- a/share/html/Elements/ShowTransactionAttachments
+++ b/share/html/Elements/ShowTransactionAttachments
@@@ -56,15 -56,14 +56,19 @@@ foreach my $message ( @{ $Attachments->
              );
  
      my $name = defined $message->Filename && length $message->Filename ?  $message->Filename : '';
-     if ( $message->ContentLength or $name ) {
+     my $should_render_download = $message->ContentLength || $name;
+ 
+     $m->callback(CallbackName => 'BeforeAttachment', ARGSRef => \%ARGS, Object => $Object, Transaction => $Transaction, Attachment => $message, Name => $name, ShouldRenderDownload => \$should_render_download);
+ 
+     if ($should_render_download) {
  </%PERL>
  <div class="downloadattachment">
 -<a href="<% $AttachmentPath %>/<% $Transaction->Id %>/<% $message->Id %>/<% $name | un%>"><&|/l&>Download</&> <% length $name ? $name : loc('(untitled)') %></a>\
 +% if (my $url = RT->System->ExternalStorageURLFor($message)) {
 +<a href="<% $url %>">
 +% } else {
- <a href="<% $AttachmentPath %>/<% $Transaction->Id %>/<% $message->Id %>/<% $name | u%>">
++<a href="<% $AttachmentPath %>/<% $Transaction->Id %>/<% $message->Id %>/<% $name | un %>">
 +% }
 +<&|/l&>Download</&> <% length $name ? $name : loc('(untitled)') %></a>\
  % if ( $DownloadableHeaders && ! length $name && $message->ContentType =~ /text/  ) {
   / <a href="<% $AttachmentPath %>/WithHeaders/<% $message->Id %>"><% loc('with headers') %></a>
  % }
diff --cc share/html/SelfService/Helpers/Upload/Add
index 53c3dab,0000000..2ab9854
mode 100644,000000..100644
--- a/share/html/SelfService/Helpers/Upload/Add
+++ b/share/html/SelfService/Helpers/Upload/Add
@@@ -1,58 -1,0 +1,58 @@@
 +%# BEGIN BPS TAGGED BLOCK {{{
 +%#
 +%# COPYRIGHT:
 +%#
- %# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
++%# 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 }}}
 +<%args>
 +$Token => ''
 +</%args>
 +
 +<%init>
 +
 +ProcessAttachments( Token => $Token, ARGSRef => \%ARGS );
 +$r->content_type('application/json; charset=utf-8');
 +$m->out( JSON({status => 'success'}) );
 +$m->abort;
 +</%init>

commit feedd19778ab6e7a9ec622b8cbce400bf525a457
Merge: 5634d7c 865dfbf
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 18:50:34 2016 -0400

    Merge branch '4.4/assets-semicolons' into 4.4-trunk


commit e9cbfd81e9d2b9af5f123504f82736751379ea32
Merge: feedd19 e03aa32
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 13 18:51:06 2016 -0400

    Merge branch '4.4/transition-swap' into 4.4-trunk


commit 5ce4df493d3dc249a0f6e9b525278cb2bea13e6d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon May 16 09:33:40 2016 -0400

    Note the __Active__ option for RT's saved searches
    
    6ebcea11d updates initialdata for new RT's to now use the
    __Active__ placeholder for RT's saved searches to handle
    custom lifecycles. Add a note to the UPGRADING file for
    users to upgrade existing instances if they have
    custom lifecycles.

diff --git a/docs/UPGRADING-4.4 b/docs/UPGRADING-4.4
index f19c493..3492aa7 100644
--- a/docs/UPGRADING-4.4
+++ b/docs/UPGRADING-4.4
@@ -407,5 +407,22 @@ Searching and updating handle both representations as you'd expect.
 
 =back
 
+=head1 UPGRADING FROM 4.4.0 AND EARLIER
+
+=over 4
+
+=item *
+
+We updated default RT searches that previously specified 'new' and 'open' status
+to use the new '__Active__' and '__Inactive__' syntax where appropriate. For new
+RT's, this means the default RT at a Glance searches will now work with custom
+lifecycles without updating these searches.
+
+For existing RTs, you may want to update the "highest priority tickets I own" and
+'newest unowned tickets' system searches to use '__Active__' rather than specific
+statuses if you have a custom lifecycle.
+
+=back
+
 =cut
 

commit 116283645129a2a352c8dcb0ed198e1a2e9c8537
Merge: e9cbfd8 5ce4df4
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 16 12:27:09 2016 -0400

    Merge branch '4.4/use-status-active' into 4.4-trunk


commit 82d34efd671989fe5ffd136e721f2f6bab876efc
Merge: 1162836 244bf30
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon May 16 16:35:21 2016 -0400

    Merge branch '4.4/toggle-unset-fields' into 4.4-trunk


commit 776bb9ca1a84fca9cad03a7641882a3b187b6dbc
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Mon May 16 22:32:36 2016 +0000

    Add queue.sortorder column sooner in the 4.4 upgrade
    
    This fixes errors during the upgrade from RT 4.2 to RT 4.4 while RTIR
    3.2 is installed. RTIR 3.2.0 provides a RT::Queue::HasRight which
    includes the following code:
    
        my $queues = RT::Queues->new( RT->SystemUser );
        $queues->Limit( FIELD => 'Name', OPERATOR => 'STARTSWITH', VALUE => "$name - ", CASESENSITIVE => 0 );
        $equiv_objects = $queues->ItemsArrayRef;
    
    However, enumerating the list of queues like this is broken (throwing
    database error "column main.sortorder does not exist") with the RT 4.4
    codebase _until_ the sortorder column is added to the queue table schema
    in upgrade step 4.3.4.  The upgrade step that tickles this error is
    4.3.2 which itself handled the issue by emptying out the default sort
    order for queues like so:
    
        # override the default order by SortOrder, because it doesn't exist yet
        $queues->OrderByCols();
    
    Unfortunately adding such a hack doesn't make much sense for RTIR, and
    it's too late anyway since it'd need to have been included in RTIR
    3.2.0, since users upgrade RT first then RTIR.  So instead, move the
    schema change adding queue.sortorder to be part of the 4.3.2 upgrade
    step. This obviates the need for the above hack (since 4.3.2/schema is
    run before 4.3.2/content), and allows the RTIR HasRight override to run
    correctly during the RT upgrade.
    
    This specific solution was chosen (rather than reordering 4.3.2, 4.3.3,
    and 4.3.4) to be the least disruptive and different for installations
    upgrading from RT 4.2 to 4.4.0 vs 4.4.1.

diff --git a/etc/upgrade/4.3.2/content b/etc/upgrade/4.3.2/content
index 9673e6c..3cfadfe 100644
--- a/etc/upgrade/4.3.2/content
+++ b/etc/upgrade/4.3.2/content
@@ -7,8 +7,6 @@ our @Initial = (
         my $queues = RT::Queues->new(RT->SystemUser);
         $queues->UnLimit;
         $queues->{'find_disabled_rows'} = 1;
-        # override the default order by SortOrder, because it doesn't exist yet
-        $queues->OrderByCols();
         while ( my $queue = $queues->Next ) {
             next if $queue->FirstAttribute('DefaultValues');
             my %default;
diff --git a/etc/upgrade/4.3.4/schema.Oracle b/etc/upgrade/4.3.2/schema.Oracle
similarity index 100%
rename from etc/upgrade/4.3.4/schema.Oracle
rename to etc/upgrade/4.3.2/schema.Oracle
diff --git a/etc/upgrade/4.3.4/schema.Pg b/etc/upgrade/4.3.2/schema.Pg
similarity index 100%
rename from etc/upgrade/4.3.4/schema.Pg
rename to etc/upgrade/4.3.2/schema.Pg
diff --git a/etc/upgrade/4.3.4/schema.SQLite b/etc/upgrade/4.3.2/schema.SQLite
similarity index 100%
rename from etc/upgrade/4.3.4/schema.SQLite
rename to etc/upgrade/4.3.2/schema.SQLite
diff --git a/etc/upgrade/4.3.4/schema.mysql b/etc/upgrade/4.3.2/schema.mysql
similarity index 100%
rename from etc/upgrade/4.3.4/schema.mysql
rename to etc/upgrade/4.3.2/schema.mysql

commit 282664d5365a3ff3bf5c25730cb607fbb9eb7833
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 10:00:58 2016 -0400

    Clarify the SLA upgrade process

diff --git a/docs/UPGRADING-4.4 b/docs/UPGRADING-4.4
index 371f476..dc8276c 100644
--- a/docs/UPGRADING-4.4
+++ b/docs/UPGRADING-4.4
@@ -60,17 +60,47 @@ F<local/plugins/RT-Extension-Assets> from your RT installation.
 
 =item *
 
-SLA tracking is in core now, so C<SLA> became a core field. Please see the SLA
-section in F<RT_Config.pm> as well as F<docs/customizing/sla.pod>.
+As of 4.4, SLA tracking is also a core feature, so C<SLA> is a core field
+on tickets for queues on which SLA tracking is enabled. Please see the SLA
+section in F<RT_Config.pm> as well as F<docs/customizing/sla.pod> for
+details on configuring RT's SLA feature.
 
-Users who are currently using
+Users currently using
 L<RT::Extension::SLA|https://metacpan.org/pod/RT::Extension::SLA> should
-remove C<RT::Extension::SLA> from the plugin list, and run
-F<etc/upgrade/upgrade-sla>. Please also remove
-F<local/plugins/RT-Extension-SLA> from your RT installation. Note that with
-core SLA, all queues share the same set of levels defined in
+do the following to migrate to core SLA functionality after running
+the main RT code and database upgrade steps successfully:
+
+=over 4
+
+=item * Remove C<RT::Extension::SLA> from your plugin list in C<RT_SiteConfig.pm>
+
+=item * Run the upgrade script F<etc/upgrade/upgrade-sla>
+
+=item * Update the format of your C<%ServiceAgreements> configuration
+
+You can keep the same configuration, but it is now set with a C<Set>
+call like all standard RT features, the leading C<RT::> is removed,
+and the C<=> becomes a C<,>. You can
+see examples in F<docs/customizing/sla.pod>.
+
+=item * Update the format of your C<%ServiceBusinessHours> configuration
+
+If you have a Business Hours configuration, update your configuration
+in C<RT_SiteConfig.pm> with the same changes as described above for
 C<%ServiceAgreements>.
 
+=item * (Optional) Remove the directory F<local/plugins/RT-Extension-SLA>
+
+You can remove this directory and all of its contents from your RT
+installation to uninstall the previous extension code
+
+=back
+
+Note that since SLA is now a core ticket value, it is currently not possible
+to have different levels per queue as was previously possible when using
+multiple SLA custom fields. Currently all queues share the same set of levels
+defined in C<%ServiceAgreements>.
+
 =item *
 
 RT can now natively store attachments outside the database, either on disk, in

commit fd40febd3e805820004897db1877716c3db6b07e
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 10:36:57 2016 -0400

    Add additional output to give feedback while running
    
    If run with no arguments, Init sets the log level to
    warning, so the user won't see any of the info messages
    that display what the script is doing. Add a start and
    end print to send something to the terminal.
    
    Also add a log line for the ticket update step.

diff --git a/etc/upgrade/upgrade-sla.in b/etc/upgrade/upgrade-sla.in
index d88ed92..91649be 100644
--- a/etc/upgrade/upgrade-sla.in
+++ b/etc/upgrade/upgrade-sla.in
@@ -56,7 +56,9 @@ use lib "@RT_LIB_PATH@";
 use RT::Interface::CLI qw(Init);
 Init();
 
+print "Starting SLA upgrade process...\n";
 {
+    RT->Logger->info("Updating tickets with SLA custom field");
     local *RT::Ticket::_SetLastUpdated = sub {
         return (1, "Migrating SLA from CF to core field silently");
     };
@@ -160,3 +162,4 @@ for my $item ( @old_actions ) {
         }
     }
 }
+print "\nDone.\n";

commit 6d99bf7951c0f025415f877a637cf9d977b9c9da
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 10:46:40 2016 -0400

    Add some POD for upgrade-sla

diff --git a/etc/upgrade/upgrade-sla.in b/etc/upgrade/upgrade-sla.in
index 91649be..ee1dfb9 100644
--- a/etc/upgrade/upgrade-sla.in
+++ b/etc/upgrade/upgrade-sla.in
@@ -163,3 +163,23 @@ for my $item ( @old_actions ) {
     }
 }
 print "\nDone.\n";
+
+__END__
+
+=head1 NAME
+
+upgrade-sla - upgrade SLA extension installs to core SLA feature
+
+=head1 SYNOPSIS
+
+    # Run upgrade after upgrading RT to 4.4.x
+    upgrade-sla
+    upgrade-sla --verbose # see more output while running the upgrade
+
+=head1 DESCRIPTION
+
+This upgrade script examines data from RTs that previously used C<RT::Extension::SLA>
+and migrates the data to the configuration required for core SLA functionality.
+This includes moving SLA values from the SLA custom field to the core RT field
+and enabling the SLA feature for queues that appear to have it configured.
+Some old scrips, actions, and conditions are also cleaned up.

commit 6ff31343b8c199d6bb2aee0de3f4edd330df825d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 10:53:13 2016 -0400

    Add queue name to output logs
    
    Users don't generally know queues by the id. Names
    are much easier to recognize.

diff --git a/etc/upgrade/upgrade-sla.in b/etc/upgrade/upgrade-sla.in
index ee1dfb9..4b71e3c 100644
--- a/etc/upgrade/upgrade-sla.in
+++ b/etc/upgrade/upgrade-sla.in
@@ -91,19 +91,19 @@ while ( my $queue = $queues->Next ) {
         $cfs_to_disable{$cf->id} ||= $cf;
          my ($ret, $msg) = $queue->SetSLADisabled(0);
          if ( $ret ) {
-             RT->Logger->info("Enabled SLA for queue #" . $queue->id);
+             RT->Logger->info("Enabled SLA for queue #" . $queue->id . ", " . $queue->Name);
          }
          else {
-             RT->Logger->error("Failed to enable SLA for queue #" . $queue->id . ": $msg");
+             RT->Logger->error("Failed to enable SLA for queue #" . $queue->id . ", " . $queue->Name . ": $msg");
          }
     }
     elsif ( !$queue->SLADisabled ) {
         my ($ret, $msg) = $queue->SetSLADisabled(1);
         if ( $ret ) {
-            RT->Logger->info("Disabled SLA for queue #" . $queue->id . " because it doesn't have custom field SLA applied");
+            RT->Logger->info("Disabled SLA for queue #" . $queue->id . ", " . $queue->Name . ", because it doesn't have custom field SLA applied");
         }
         else {
-            RT->Logger->error("Failed to disable SLA for queue #" . $queue->id . ": $msg");
+            RT->Logger->error("Failed to disable SLA for queue #" . $queue->id . ", " . $queue->Name . ": $msg");
         }
     }
 }

commit 9b284d66bf534fff921aba257a4ea3107153d4c4
Merge: 82d34ef 282664d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue May 17 11:42:32 2016 -0400

    Merge branch '4.4/clarify-sla-upgrade-notes' into 4.4-trunk


commit 71a67111411e7ef1c479215bb96c582bc5f1948c
Merge: 9b284d6 6ff3134
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue May 17 11:43:17 2016 -0400

    Merge branch '4.4/sla-upgrade-dates' into 4.4-trunk


commit 6ea4762646ec1f48f9539eb6a5ce6ea1f941992a
Merge: 71a6711 6270f13
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 14:06:18 2016 -0400

    Merge branch '4.4/externalauth-hyperlink' into 4.4-trunk


commit 74bb4cfe3c7dbb275e680867245d9d5ce9fdf3b0
Merge: 6ea4762 776bb9c
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 14:53:46 2016 -0400

    Merge branch '4.4/queue-sortorder-error' into 4.4-trunk


commit f61d0353ef2cb8fe7e42a975d732eb9c5d1a2b38
Merge: 74bb4cf 9e7da56
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Tue May 17 19:12:59 2016 +0000

    Merge branch '4.4/dashboard-language' into 4.4-trunk
    
    Conflicts:
    	share/html/Dashboards/Subscription.html

diff --cc share/html/Dashboards/Subscription.html
index cb5ba16,73f2557..262f853
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@@ -235,12 -237,9 +244,13 @@@ my %fields = 
      Recipients  => { Users => [], Groups => [] },
      Fow         => 1,
      Counter     => 0,
+     Language    => '',
 +    SuppressIfEmpty => 0,
  );
  
 +$m->callback( %ARGS, CallbackName => 'SubscriptionFields', FieldsRef => \%fields,
 +     SubscriptionObj => $SubscriptionObj, DashboardObj => $Dashboard);
 +
  # update any fields with the values from the subscription object
  if ($SubscriptionObj) {
      for my $field (keys %fields) {

commit b483a1c70d8c8c9054b1982b3f41c6f3140902aa
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Tue May 17 19:29:36 2016 +0000

    change dashboard subscription user loading to use RT->SystemUser and output any user load/create failure message

diff --git a/share/html/Dashboards/Subscription.html b/share/html/Dashboards/Subscription.html
index 262f853..7304b53 100644
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@ -279,18 +279,23 @@ if (defined $ARGS{Save}) {
             for ( RT::EmailParser->ParseEmailAddress( $val ) ) {
                 my ( $email, $name ) = ( $_->address, $_->name );
 
-                my $user = RT::User->new($session{CurrentUser});
-                $user->LoadOrCreateByEmail(
+                my $user = RT::User->new(RT->SystemUser);
+                ($ok, $msg) = $user->LoadOrCreateByEmail(
                     EmailAddress => $email,
                     RealName     => $name,
                     Comments     => 'Autocreated when added as a dashboard subscription recipient',
                 );
 
-                my $is_prev_recipient = grep { $_ == $user->id } @recipients;
-                if ( not $is_prev_recipient ) {
-                    push @recipients, $user->id;
-                    push @results, loc("[_1] added to dashboard subscription recipients", $email);
+                unless ($ok) {
+                    push @results, loc("Could not add [_1] as a recipient: [_2]", $email, $msg);
+                    next;
                 }
+
+                my $is_prev_recipient = grep { $_ == $user->id } @recipients;
+                next if $is_prev_recipient;
+
+                push @recipients, $user->id;
+                push @results, loc("[_1] added to dashboard subscription recipients", $email);
             }
             @{ $fields{Recipients}->{Users} } = uniq @recipients;
 

commit bf6f1a259f8d1d387db092ae476e41669c09b33e
Author: Dustin Graves <dustin at bestpractical.com>
Date:   Tue May 17 19:34:48 2016 +0000

    don't set RealName on dashboard subscription user creation
    
    this is to be consistent with other RT behavior, e.g.
    user at domain.com was setting RealName to 'user', which RT typically does not do

diff --git a/share/html/Dashboards/Subscription.html b/share/html/Dashboards/Subscription.html
index 7304b53..34aaa33 100644
--- a/share/html/Dashboards/Subscription.html
+++ b/share/html/Dashboards/Subscription.html
@@ -277,12 +277,11 @@ if (defined $ARGS{Save}) {
             my @recipients = @{ $fields{Recipients}->{Users} };
 
             for ( RT::EmailParser->ParseEmailAddress( $val ) ) {
-                my ( $email, $name ) = ( $_->address, $_->name );
+                my $email = $_->address;
 
                 my $user = RT::User->new(RT->SystemUser);
                 ($ok, $msg) = $user->LoadOrCreateByEmail(
                     EmailAddress => $email,
-                    RealName     => $name,
                     Comments     => 'Autocreated when added as a dashboard subscription recipient',
                 );
 

commit 55b399008235e1d404324697a5116e46960f7fb1
Merge: f61d035 bf6f1a2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue May 17 15:41:36 2016 -0400

    Merge branch '4.4/dashboard-subscription-user-group-fix' into 4.4-trunk


commit 08f58eb1cf194a1af147fd086de76faf51463fa8
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 16:03:09 2016 -0400

    Limit ExternalAuth enabled setting to ExternalSettings
    
    While ExternalAuthPriority and ExternalInfoPriority are
    important, the ExternalSettings config is essential to making
    ExternalAuth work, so automatically enable it based on
    the presence of that configuration setting only.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 6c2c842..8fef004 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1078,8 +1078,6 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
-            $self->EnableExternalAuth() if @values;
-
             if (not @values) {
                 $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
                 $self->Set( 'ExternalAuthPriority', \@values );
@@ -1106,8 +1104,6 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
-            $self->EnableExternalAuth() if @values;
-
             if (not @values) {
                 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");
                 $self->Set( 'ExternalInfoPriority', \@values );

commit 43c13180c02a09251666d88ee1672c3ca30f1fc3
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 16:06:49 2016 -0400

    Only attempt to set External Info/Auth if values are available
    
    The ExternalAuthPriority and ExternalInfoPriority options
    attempt to set values based on configuration from the
    ExternalSettings configuration option if no values are passed
    explicitly. If no values are passed and the ExternalSettings
    option is not set, bail and don't issue debug messages since
    the user clearly isn't using ExternalAuth.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 8fef004..70df38f 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1078,6 +1078,8 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
+            return unless @values or $self->Get('ExternalSettings');
+
             if (not @values) {
                 $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
                 $self->Set( 'ExternalAuthPriority', \@values );
@@ -1104,6 +1106,8 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
+            return unless @values or $self->Get('ExternalSettings');
+
             if (not @values) {
                 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");
                 $self->Set( 'ExternalInfoPriority', \@values );

commit 9ab09be6696f937313fb78ec0c2d280b0644cd4c
Merge: 55b3990 43c1318
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue May 17 16:48:34 2016 -0400

    Merge branch '4.4/refine-external-auth-config' into 4.4-trunk


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


More information about the rt-commit mailing list