[Rt-commit] rt branch, 4.4/remove-user-info, created. rt-4.4.3-163-g733604309b

Craig Kaiser craig at bestpractical.com
Thu Jan 3 12:36:59 EST 2019


The branch, 4.4/remove-user-info has been created
        at  733604309bb623be59fa4ba7da093d5543c44c09 (commit)

- Log -----------------------------------------------------------------
commit 5d3e802e0f257104f4d21a170f3a2abc01c0ea60
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Mar 16 11:55:37 2017 -0400

    Add test for contents of user session

diff --git a/t/web/session.t b/t/web/session.t
new file mode 100644
index 0000000000..1f0bb0bfd7
--- /dev/null
+++ b/t/web/session.t
@@ -0,0 +1,60 @@
+
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+plan skip_all => 'SQLite has shared file sessions' if RT->Config->Get('DatabaseType') eq 'SQLite';
+
+my ($baseurl, $agent) = RT::Test->started_ok;
+my $url = $agent->rt_base_url;
+
+diag "Test server running at $baseurl";
+
+# get the top page
+{
+    $agent->get($url);
+    is ($agent->status, 200, "Loaded a page");
+}
+
+# test a login
+{
+    $agent->login('root' => 'password');
+    # the field isn't named, so we have to click link 0
+    is( $agent->status, 200, "Fetched the page ok");
+    $agent->content_contains("Logout", "Found a logout link");
+}
+
+my $ids_ref = RT::Interface::Web::Session->Ids();
+
+# Should only have one session id at this point.
+is( scalar @$ids_ref, 1, 'Got just one session id');
+
+diag 'Load session for root user';
+my %session;
+tie %session, 'RT::Interface::Web::Session', $ids_ref->[0];
+is ( $session{'_session_id'}, $ids_ref->[0], 'Got session id ' . $ids_ref->[0] );
+is ( $session{'CurrentUser'}->Name, 'root', 'Session is for root user' );
+
+diag 'Test queues cache';
+my $user_id = $session{'CurrentUser'}->Id;
+ok ( $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}, 'Queues cached for create ticket');
+is ( $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}{'objects'}->[0]{'Name'},
+    'General', 'General queue is in cached list' );
+
+my $last_updated = $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}{'lastupdated'};
+ok( $last_updated, "Got a lastupdated timestamp of $last_updated");
+
+untie(%session);
+# Wait for 1 sec so we can confirm lastupdated doesn't change
+sleep 1;
+$agent->get($url);
+is ($agent->status, 200, "Loaded a page");
+
+tie %session, 'RT::Interface::Web::Session', $ids_ref->[0];
+is ( $session{'_session_id'}, $ids_ref->[0], 'Got session id ' . $ids_ref->[0] );
+is ( $session{'CurrentUser'}->Name, 'root', 'Session is for root user' );
+is ($last_updated, $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}{'lastupdated'},
+    "lastupdated is still $last_updated");
+
+done_testing;

commit f82c158637dd1d1de10e089845f4369af82e670e
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Mar 16 12:51:53 2017 -0400

    Move session cache code to a function for easier access

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 66fe3135c0..f70e910a07 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4344,6 +4344,96 @@ sub ProcessAssetsSearchArguments {
     );
 }
 
+=head3 SetObjectSessionCache
+
+Convenience method to stash per-user query results in the user session. This is used
+for rights-intensive queries that change infrequently, such as generating the list of
+queues a user has access to.
+
+The method handles populating the session cache and clearing it based on CacheNeedsUpdate.
+It returns the cache key so callers can use $session directly after it has been created
+or updated.
+
+Parameters:
+
+=over
+
+=item * ObjectType, required, the object for which to fetch values
+
+=item * CheckRight, the right to check for the current user in the query
+
+=item * ShowAll, boolean, ignores the rights check
+
+=item * Default, for dropdowns, a default selected value
+
+=item * CacheNeedsUpdate, date indicating when an update happened requiring a cache clear
+
+=cut
+
+sub SetObjectSessionCache {
+    my %args = (
+        CheckRight => undef,
+        ShowAll => 1,
+        Default => 0,
+        CacheNeedsUpdate => undef,
+        @_ );
+
+    my $ObjectType = $args{'ObjectType'};
+    $ObjectType = "RT::$ObjectType" unless $ObjectType =~ /::/;
+    my $CheckRight = $args{'CheckRight'};
+    my $ShowAll = $args{'ShowAll'};
+    my $CacheNeedsUpdate = $args{'CacheNeedsUpdate'};
+
+    my $cache_key = join "---", "SelectObject", $ObjectType,
+        $session{'CurrentUser'}->Id, $CheckRight || "", $ShowAll;
+
+    if ( defined $session{$cache_key} && !$session{$cache_key}{id} ) {
+        delete $session{$cache_key};
+    }
+
+    if ( defined $session{$cache_key}
+         && ref $session{$cache_key} eq 'ARRAY') {
+        delete $session{$cache_key};
+    }
+    if ( defined $session{$cache_key} && defined $CacheNeedsUpdate &&
+        $session{$cache_key}{lastupdated} <= $CacheNeedsUpdate ) {
+        delete $session{$cache_key};
+    }
+
+    if ( not defined $session{$cache_key} ) {
+        my $collection = "${ObjectType}s"->new($session{'CurrentUser'});
+        $collection->UnLimit;
+
+        $HTML::Mason::Commands::m->callback( CallbackName => 'ModifyCollection',
+            CallbackPage => '/Elements/Quicksearch',
+            ARGSRef => \%args, Collection => $collection, ObjectType => $ObjectType );
+
+        # This is included for continuity in the 4.2 series. It will be removed in 4.6.
+        $HTML::Mason::Commands::m->callback( CallbackName => 'SQLFilter',
+            CallbackPage => '/Elements/QueueSummaryByLifecycle', Queues => $collection )
+            if $ObjectType eq "RT::Queue";
+
+        $session{$cache_key}{id} = {};
+
+        while (my $object = $collection->Next) {
+            if ($ShowAll
+                or not $CheckRight
+                or $session{CurrentUser}->HasRight( Object => $object, Right => $CheckRight ))
+            {
+                push @{$session{$cache_key}{objects}}, {
+                    Id          => $object->Id,
+                    Name        => $object->Name,
+                    Description => $object->_Accessible("Description" => "read") ? $object->Description : undef,
+                };
+                $session{$cache_key}{id}{ $object->id } = 1;
+            }
+        }
+        $session{$cache_key}{lastupdated} = time();
+    }
+
+    return $cache_key;
+}
+
 =head2 _load_container_object ( $type, $id );
 
 Instantiate container object for saving searches.
diff --git a/share/html/Elements/SelectObject b/share/html/Elements/SelectObject
index 89f5f01640..f50348bb77 100644
--- a/share/html/Elements/SelectObject
+++ b/share/html/Elements/SelectObject
@@ -89,42 +89,15 @@ $CacheNeedsUpdate => undef
 $ObjectType = "RT::$ObjectType" unless $ObjectType =~ /::/;
 $Class    ||= "select-" . CSSClass("\L$1") if $ObjectType =~ /RT::(.+)$/;
 
-my $cache_key = join "---", "SelectObject", $ObjectType,
-    $session{'CurrentUser'}->Id, $CheckRight || "", $ShowAll;
-
-if ( defined $session{$cache_key} && ref $session{$cache_key} eq 'ARRAY') {
-    delete $session{$cache_key};
-}
-if ( defined $session{$cache_key} && defined $CacheNeedsUpdate &&
-     $session{$cache_key}{lastupdated} <= $CacheNeedsUpdate ) {
-    delete $session{$cache_key};
-}
-if ( defined $session{$cache_key} && !$session{$cache_key}{id} ) {
-    delete $session{$cache_key};
-}
-
-if ( not defined $session{$cache_key} and not $Lite ) {
-    my $collection = "${ObjectType}s"->new($session{'CurrentUser'});
-    $collection->UnLimit;
-
-    $m->callback( CallbackName => 'ModifyCollection', ARGSRef => \%ARGS,
-                  Collection => $collection, ObjectType => $ObjectType );
-
-    $session{$cache_key}{id} = {};
-    while (my $object = $collection->Next) {
-        if ($ShowAll
-            or not $CheckRight
-            or $session{CurrentUser}->HasRight( Object => $object, Right => $CheckRight ))
-        {
-            push @{$session{$cache_key}{objects}}, {
-                Id          => $object->Id,
-                Name        => $object->Name,
-                Description => $object->_Accessible("Description" => "read") ? $object->Description : undef,
-            };
-            $session{$cache_key}{id}{ $object->id } = 1;
-        }
-    }
-    $session{$cache_key}{lastupdated} = time();
+my $cache_key;
+if ( not $Lite ) {
+    $cache_key = SetObjectSessionCache(
+        ObjectType => $ObjectType,
+        CheckRight => $CheckRight,
+        ShowAll => $ShowAll,
+        Default => $Default,
+        CacheNeedsUpdate => $CacheNeedsUpdate,
+    );
 }
 
 my $default_entry;

commit 901d14030881fa0b554b8be4b3a62d0b6238ece5
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Sep 28 13:33:28 2018 -0400

    Add caching to the queue list portlet
    
    On larger RTs with many queues, and users attached to many
    tickets over time, the rights check to generate the appropriate
    queue list for a user as been observed to take a significant
    amount of time (20+ seconds on one system). Cache this list
    much like the queue list in the "Create ticket" dropdown since
    it changes infrequently from page load to page load.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index f70e910a07..a096d96db1 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4368,6 +4368,10 @@ Parameters:
 
 =item * CacheNeedsUpdate, date indicating when an update happened requiring a cache clear
 
+=item * Exclude, hashref ({ Name => 1 }) of object Names to exclude from the cache
+
+=back
+
 =cut
 
 sub SetObjectSessionCache {
@@ -4376,6 +4380,7 @@ sub SetObjectSessionCache {
         ShowAll => 1,
         Default => 0,
         CacheNeedsUpdate => undef,
+        Exclude => undef,
         @_ );
 
     my $ObjectType = $args{'ObjectType'};
@@ -4420,10 +4425,12 @@ sub SetObjectSessionCache {
                 or not $CheckRight
                 or $session{CurrentUser}->HasRight( Object => $object, Right => $CheckRight ))
             {
+                next if $args{'Exclude'} and exists $args{'Exclude'}->{$object->Name};
                 push @{$session{$cache_key}{objects}}, {
                     Id          => $object->Id,
                     Name        => $object->Name,
                     Description => $object->_Accessible("Description" => "read") ? $object->Description : undef,
+                    Lifecycle   => $object->_Accessible("Lifecycle" => "read") ? $object->Lifecycle : undef,
                 };
                 $session{$cache_key}{id}{ $object->id } = 1;
             }
diff --git a/share/html/Elements/QueueList b/share/html/Elements/QueueList
index a207fdd951..ce1481477f 100644
--- a/share/html/Elements/QueueList
+++ b/share/html/Elements/QueueList
@@ -53,13 +53,21 @@
     titleright_href => RT->Config->Get('WebPath').'/Prefs/QueueList.html',
 &>
 <& $comp,
-   queue_filter => sub { $_->CurrentUserHasRight('ShowTicket') && !exists $unwanted->{$_->Name} },
+   queues => $session{$cache_key}{objects},
 &>
 </&>
 </div>
 <%INIT>
 my $unwanted = $session{'CurrentUser'}->UserObj->Preferences('QueueList', {});
 my $comp = $SplitByLifecycle? '/Elements/QueueSummaryByLifecycle' : '/Elements/QueueSummaryByStatus';
+my $cache_key = SetObjectSessionCache(
+    ObjectType => 'RT::Queue',
+    CheckRight => 'ShowTicket',
+    ShowAll => 0,
+    CacheNeedsUpdate => RT->System->QueueCacheNeedsUpdate,
+    Exclude => $unwanted,
+);
+
 </%INIT>
 <%ARGS>
 $SplitByLifecycle => 1
diff --git a/share/html/Elements/QueueSummaryByLifecycle b/share/html/Elements/QueueSummaryByLifecycle
index 74d7fa85b4..d589b6a532 100644
--- a/share/html/Elements/QueueSummaryByLifecycle
+++ b/share/html/Elements/QueueSummaryByLifecycle
@@ -62,7 +62,7 @@
 
 <%PERL>
 my $i = 0;
-for my $queue (@queues) {
+for my $queue (@$queues) {
     next if lc($queue->{Lifecycle} || '') ne lc $lifecycle->Name;
 
     $i++;
@@ -75,7 +75,7 @@ for my $queue (@queues) {
 
 %   for my $status (@cur_statuses) {
 <td align="right">
-    <a href="<% $link_status->($queue, $status) %>"><% $data->{$queue->{id}}->{lc $status} || '-' %></a>
+    <a href="<% $link_status->($queue, $status) %>"><% $data->{$queue->{Id}}->{lc $status} || '-' %></a>
 </td>
 %   }
 </tr>
@@ -110,25 +110,12 @@ $m->callback(
     link_status         => \$link_status,
 );
 
-my $Queues = RT::Queues->new( $session{'CurrentUser'} );
-$Queues->UnLimit();
-$m->callback( CallbackName => 'SQLFilter', Queues => $Queues );
-
-my @queues = grep $queue_filter->($_), @{ $Queues->ItemsArrayRef };
-$m->callback( CallbackName => 'Filter', Queues => \@queues );
-
- at queues = map {
-    {  id          => $_->Id,
-       Name        => $_->Name,
-       Description => $_->Description || '',
-       Lifecycle   => $_->Lifecycle,
-    }
-} grep $_, @queues;
-
 my %lifecycle;
 
-for my $queue (@queues) {
+for my $queue (@$queues) {
     my $cycle = RT::Lifecycle->Load( Name => $queue->{'Lifecycle'} );
+    RT::Logger->error('Unable to load lifecycle for ' . $queue->{'Lifecycle'})
+        unless $cycle;
     $lifecycle{ lc $cycle->Name } = $cycle;
 }
 
@@ -147,17 +134,17 @@ use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( RT->SystemUser );
 my $query =
     "(Status = '__Active__') AND (".
-    join(' OR ', map "Queue = ".$_->{id}, @queues)
+    join(' OR ', map "Queue = ".$_->{Id}, @$queues)
     .")";
-$query = 'id < 0' unless @queues;
+$query = 'id < 0' unless @$queues;
 $report->SetupGroupings( Query => $query, GroupBy => [qw(Status Queue)] );
 
 while ( my $entry = $report->Next ) {
     $data->{ $entry->__Value("Queue") }->{ $entry->__Value("Status") }
-        = $entry->__Value('id');
+        = $entry->__Value('Id');
     $statuses->{ $entry->__Value("Status") } = 1;
 }
 </%INIT>
 <%ARGS>
-$queue_filter => undef
+$queues => undef  # Arrayref of hashes with cached queue info
 </%ARGS>
diff --git a/share/html/Elements/QueueSummaryByStatus b/share/html/Elements/QueueSummaryByStatus
index 6388c17523..ce8a45e55b 100644
--- a/share/html/Elements/QueueSummaryByStatus
+++ b/share/html/Elements/QueueSummaryByStatus
@@ -56,7 +56,7 @@
 
 <%PERL>
 my $i = 0;
-for my $queue (@queues) {
+for my $queue (@$queues) {
     $i++;
     my $lifecycle = $lifecycle{ lc $queue->{'Lifecycle'} };
 </%PERL>
@@ -71,7 +71,7 @@ for my $queue (@queues) {
    if ( $lifecycle->IsValid( $status ) ) {
 </%perl>
 <td align="right">
-    <a href="<% $link_status->($queue, $status) %>"><% $data->{$queue->{id}}->{lc $status} || '-' %></a>
+    <a href="<% $link_status->($queue, $status) %>"><% $data->{$queue->{Id}}->{lc $status} || '-' %></a>
 </td>
 %   } else {
 <td align="right">-</td>
@@ -108,25 +108,12 @@ $m->callback(
     link_status         => \$link_status,
 );
 
-my $Queues = RT::Queues->new( $session{'CurrentUser'} );
-$Queues->UnLimit();
-$m->callback( CallbackName => 'SQLFilter', Queues => $Queues );
-
-my @queues = grep $queue_filter->($_), @{ $Queues->ItemsArrayRef };
-$m->callback( CallbackName => 'Filter', Queues => \@queues );
-
- at queues = map {
-    {  id          => $_->Id,
-       Name        => $_->Name,
-       Description => $_->Description || '',
-       Lifecycle   => $_->Lifecycle,
-    }
-} grep $_, @queues;
-
 my %lifecycle;
 
-for my $queue (@queues) {
+for my $queue (@$queues) {
     my $cycle = RT::Lifecycle->Load( Name => $queue->{'Lifecycle'} );
+    RT::Logger->error('Unable to load lifecycle for ' . $queue->{'Lifecycle'})
+        unless $cycle;
     $lifecycle{ lc $cycle->Name } = $cycle;
 }
 
@@ -145,9 +132,9 @@ use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( RT->SystemUser );
 my $query =
     "(Status = '__Active__') AND (".
-    join(' OR ', map "Queue = ".$_->{id}, @queues)
+    join(' OR ', map "Queue = ".$_->{Id}, @$queues)
     .")";
-$query = 'id < 0' unless @queues;
+$query = 'id < 0' unless @$queues;
 $report->SetupGroupings( Query => $query, GroupBy => [qw(Status Queue)] );
 
 while ( my $entry = $report->Next ) {
@@ -157,5 +144,5 @@ while ( my $entry = $report->Next ) {
 }
 </%INIT>
 <%ARGS>
-$queue_filter => undef
+$queues => undef
 </%ARGS>

commit 7b5ae70bcb40c9b97e99a946e8ccf984f9412049
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Mon Apr 10 16:21:54 2017 -0400

    Update other portlets using SummaryByStatus

diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 52b9f10330..85cd6ba8fe 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -90,7 +90,7 @@ RT::ACE->RegisterCacheHandler(sub {
     );
 
     return unless $args{Action}    =~ /^(Grant|Revoke)$/i
-              and $args{RightName} =~ /^(SeeQueue|CreateTicket)$/;
+              and $args{RightName} =~ /^(SeeQueue|CreateTicket|AdminQueue)$/;
 
     RT->System->QueueCacheNeedsUpdate(1);
 });
diff --git a/share/html/Elements/MyAdminQueues b/share/html/Elements/MyAdminQueues
index 3f96924530..c270485266 100644
--- a/share/html/Elements/MyAdminQueues
+++ b/share/html/Elements/MyAdminQueues
@@ -47,6 +47,14 @@
 %# END BPS TAGGED BLOCK }}}
 <&|/Widgets/TitleBox, title => loc("Queues I administer"), bodyclass => "" &>
 <& /Elements/QueueSummaryByStatus,
-   queue_filter => sub { $_->CurrentUserHasRight('AdminQueue') },
+   queues => $session{$cache_key}{objects},
 &>
 </&>
+<%INIT>
+my $cache_key = SetObjectSessionCache(
+    ObjectType => 'RT::Queue',
+    CheckRight => 'AdminQueue',
+    ShowAll => 0,
+    CacheNeedsUpdate => RT->System->QueueCacheNeedsUpdate,
+);
+</%INIT>
diff --git a/share/html/Elements/MySupportQueues b/share/html/Elements/MySupportQueues
index ed8e6f05c5..1b4e375d42 100644
--- a/share/html/Elements/MySupportQueues
+++ b/share/html/Elements/MySupportQueues
@@ -47,6 +47,26 @@
 %# END BPS TAGGED BLOCK }}}
 <&|/Widgets/TitleBox, title => loc("Queues I'm an AdminCc for"), bodyclass => "" &>
 <& /Elements/QueueSummaryByStatus,
-   queue_filter => sub { $_->IsAdminCc($session{'CurrentUser'}->Id) },
+    queues => \@queues,
 &>
 </&>
+<%INIT>
+my $Queues = RT::Queues->new( $session{'CurrentUser'} );
+$Queues->UnLimit();
+$m->callback( CallbackName => 'SQLFilter', Queues => $Queues );
+
+my @queues;
+foreach my $queue ( @{ $Queues->ItemsArrayRef } ){
+    next unless $queue->IsAdminCc($session{'CurrentUser'}->Id);
+
+    if ( $queue->Id ) {
+        push @queues, {
+            Id          => $queue->Id,
+            Name        => $queue->Name,
+            Description => $queue->_Accessible("Description" => "read") ? $queue->Description : undef,
+            Lifecycle   => $queue->_Accessible("Lifecycle" => "read") ? $queue->Lifecycle : undef,
+        };
+    }
+}
+
+</%INIT>

commit a09da6a339481392f47ad892300cf17ea23cd9c6
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Oct 26 10:06:01 2018 -0400

    Clear queue list caches after pref change

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index a096d96db1..e80c0fb060 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4389,8 +4389,8 @@ sub SetObjectSessionCache {
     my $ShowAll = $args{'ShowAll'};
     my $CacheNeedsUpdate = $args{'CacheNeedsUpdate'};
 
-    my $cache_key = join "---", "SelectObject", $ObjectType,
-        $session{'CurrentUser'}->Id, $CheckRight || "", $ShowAll;
+    my $cache_key = GetObjectSessionCacheKey( ObjectType => $ObjectType,
+        CheckRight => $CheckRight, ShowAll => $ShowAll );
 
     if ( defined $session{$cache_key} && !$session{$cache_key}{id} ) {
         delete $session{$cache_key};
@@ -4441,6 +4441,23 @@ sub SetObjectSessionCache {
     return $cache_key;
 }
 
+sub GetObjectSessionCacheKey {
+    my %args = (
+        CurrentUser => undef,
+        ObjectType => '',
+        CheckRight => '',
+        ShowAll => 1,
+        @_ );
+
+    my $cache_key = join "---", "SelectObject",
+        $args{'ObjectType'},
+        $session{'CurrentUser'}->Id,
+        $args{'CheckRight'},
+        $args{'ShowAll'};
+
+    return $cache_key;
+}
+
 =head2 _load_container_object ( $type, $id );
 
 Instantiate container object for saving searches.
diff --git a/share/html/Prefs/QueueList.html b/share/html/Prefs/QueueList.html
index d9d4268ab0..100e4b1398 100644
--- a/share/html/Prefs/QueueList.html
+++ b/share/html/Prefs/QueueList.html
@@ -104,6 +104,19 @@ if ($ARGS{'Save'}) {
 
     my ($ok, $msg) = $user->SetPreferences('QueueList', $unwanted);
     push @actions, $ok ? loc('Preferences saved.') : $msg;
+
+    # Clear queue caches
+    if ( $ok ){
+        # Clear for 'CreateTicket'
+        my $cache_key = GetObjectSessionCacheKey( ObjectType => 'RT::Queue',
+            CheckRight => 'CreateTicket', ShowAll => 0 );
+        delete $session{$cache_key};
+
+        # Clear for 'ShowTicket'
+        $cache_key = GetObjectSessionCacheKey( ObjectType => 'RT::Queue',
+            CheckRight => 'ShowTicket', ShowAll => 0 );
+        delete $session{$cache_key};
+    }
 }
 
 </%INIT>

commit 7cb23c4e581c819953b3cab94bd4d2f5be834504
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Dec 21 01:49:25 2018 +0800

    Show system default value for Timezone
    
    "System Default" without a value is not useful to users.

diff --git a/share/html/Elements/SelectTimezone b/share/html/Elements/SelectTimezone
index 1ade6d4d99..f373bbd72b 100644
--- a/share/html/Elements/SelectTimezone
+++ b/share/html/Elements/SelectTimezone
@@ -60,7 +60,7 @@ for ( @names ) {
 </%ONCE>
 <select name="<% $Name %>">
 % if ( $ShowNullOption ) {
-<option value=""><&|/l&>System Default</&></option>
+<option value=""><&|/l&>System Default</&> (<% RT->Config->Get('Timezone') %>)</option>
 % }
 % foreach my $tz (@names) {
 <option value="<% $tz %>" <% ($Default||'') eq $tz? 'selected="selected"' :''
diff --git a/share/html/Prefs/Elements/ShowAboutMe b/share/html/Prefs/Elements/ShowAboutMe
index 268eb3a5a4..0ce0aac115 100644
--- a/share/html/Prefs/Elements/ShowAboutMe
+++ b/share/html/Prefs/Elements/ShowAboutMe
@@ -77,7 +77,7 @@
 % if ( $UserObj->Timezone ) {
           <td class="value"><%$UserObj->Timezone%></td>
 % } else {
-          <td class="value"><&|/l&>System Default</&></td>
+          <td class="value"><&|/l&>System Default</&> (<% RT->Config->Get('Timezone') %>)</td>
 % }
         </tr>
       <& /Elements/ShowCustomFields, Object => $UserObj, Grouping => 'Identity', InTable => 1 &>

commit c83ca7b87171dad2c486bd79240fed65875f2a2c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Dec 21 01:49:38 2018 +0800

    Show system default value for Lang
    
    "-" or "System Default" without a value is not useful to users.

diff --git a/share/html/Elements/SelectLang b/share/html/Elements/SelectLang
index e21eaf1bd8..9458a758ae 100644
--- a/share/html/Elements/SelectLang
+++ b/share/html/Elements/SelectLang
@@ -47,7 +47,7 @@
 %# END BPS TAGGED BLOCK }}}
 <select name="<%$Name%>">
 % if ($ShowNullOption) {
-<option value="">-</option>
+<option value="">- (<% I18N::LangTags::List::name($session{CurrentUser}->LanguageHandle->language_tag) %>)</option>
 % }
 % foreach my $lang (@lang) {
 <option value="<%$lang%>"<%(defined($Default) && ($lang eq $Default)) && qq[ selected="selected"] |n%>><% $lang_to_desc{$lang} %>
diff --git a/share/html/Prefs/Elements/ShowAboutMe b/share/html/Prefs/Elements/ShowAboutMe
index 0ce0aac115..6312417de0 100644
--- a/share/html/Prefs/Elements/ShowAboutMe
+++ b/share/html/Prefs/Elements/ShowAboutMe
@@ -69,7 +69,7 @@
 % if ( $UserObj->Lang ) {
           <td class="value"><&|/l, $lang &>[_1]</&></td>
 % } else {
-          <td class="value"><&|/l&>System Default</&></td>
+          <td class="value"><&|/l&>System Default</&> (<% I18N::LangTags::List::name($session{CurrentUser}->LanguageHandle->language_tag) %>)</td>
 % }
         </tr>
         <tr>

commit 209f0c1bf2d7e91cffb60e9b5d7eb253998e72db
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Dec 28 22:29:40 2018 +0800

    Remove user related data download links on "About Me" page of priviged users
    
    These download links are created for GDPR, privileged users usually
    don't have this issue.

diff --git a/share/html/Prefs/AboutMe.html b/share/html/Prefs/AboutMe.html
index e2e046711f..df90996e8b 100644
--- a/share/html/Prefs/AboutMe.html
+++ b/share/html/Prefs/AboutMe.html
@@ -56,13 +56,6 @@
 
 </form>
 
-<& /User/Elements/RelatedData,
-    UserObj           => $UserObj,
-    UserDataButton    => loc( 'Download My Data' ),
-    UserTicketsButton => loc( 'Download My Tickets' ),
-    UserTxnButton     => loc( 'Download My Transaction Data' ),
-&>
-
 <%INIT>
 
 my $UserObj = RT::User->new( $session{'CurrentUser'} );

commit d2036040de491f87f4634efb5511f4dac1a887df
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Dec 13 11:33:06 2018 -0500

    Allow TSVExport filename to be set through Filename arg

diff --git a/share/html/Elements/TSVExport b/share/html/Elements/TSVExport
index df7105f794..3f6d5d8136 100644
--- a/share/html/Elements/TSVExport
+++ b/share/html/Elements/TSVExport
@@ -50,6 +50,7 @@ $Class => undef
 $Collection
 $Format
 $PreserveNewLines => 0
+$Filename  => undef
 </%ARGS>
 <%ONCE>
 my $no_html = HTML::Scrubber->new( deny => '*' );
@@ -59,6 +60,7 @@ require HTML::Entities;
 $Class ||= $Collection->ColumnMapClassName;
 
 $r->content_type('application/vnd.ms-excel');
+$r->header_out( 'Content-disposition' => "attachment; filename=$Filename" ) if $Filename;
 
 my $DisplayFormat = $m->comp('/Elements/ScrubHTML', Content => $Format);
 

commit 47cdd92123f66f731bda445c7b3401858c42d0b5
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Dec 13 11:33:06 2018 -0500

    Set filename of downloaded user related tsv data

diff --git a/share/html/Search/Results.tsv b/share/html/Search/Results.tsv
index 1bbdded741..ad536944da 100644
--- a/share/html/Search/Results.tsv
+++ b/share/html/Search/Results.tsv
@@ -51,6 +51,7 @@ $Query => ''
 $OrderBy => 'id'
 $Order => 'ASC'
 $PreserveNewLines => 0
+$UserData => 0
 </%ARGS>
 <%INIT>
 my $Tickets = RT::Tickets->new( $session{'CurrentUser'} );
@@ -68,5 +69,6 @@ else {
     $Tickets->OrderBy( FIELD => $OrderBy, ORDER => $Order );
 }
 
-$m->comp( "/Elements/TSVExport", Collection => $Tickets, Format => $Format, PreserveNewLines => $PreserveNewLines );
+my $filename = $UserData ? 'UserTicketData.tsv' : undef;
+$m->comp( "/Elements/TSVExport", Collection => $Tickets, Format => $Format, PreserveNewLines => $PreserveNewLines, Filename => $filename );
 </%INIT>
diff --git a/share/html/User/Elements/RelatedData b/share/html/User/Elements/RelatedData
index 051112d969..cad301c714 100644
--- a/share/html/User/Elements/RelatedData
+++ b/share/html/User/Elements/RelatedData
@@ -52,7 +52,7 @@
 
 <div>
     <a href="/User/RelatedData.tsv?Type=User&id=<% $UserObj->id %>" class="button"><% $UserDataButton %></a>
-    <a href="/Search/Results.tsv?Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>" class="button"><% $UserTicketsButton %></a>
+    <a href="/Search/Results.tsv?UserData=1&Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>" class="button"><% $UserTicketsButton %></a>
     <a href="/User/RelatedData.tsv?Type=Transaction&id=<% $UserObj->id %>" class="button"><% $UserTxnButton %></a>
 </div>
 </&>
diff --git a/share/html/User/RelatedData.tsv b/share/html/User/RelatedData.tsv
index 25804686dd..e6e79d0ac1 100644
--- a/share/html/User/RelatedData.tsv
+++ b/share/html/User/RelatedData.tsv
@@ -63,15 +63,18 @@ if ( $session{'CurrentUser'}->id ne $id ) {
 }
 
 my $Collection;
+my $filename;
 
 if ( $Type eq 'User' ) {
     $Format = RT->Config->Get('UserDataResultFormat') unless $Format;
+    $filename = 'UserData.tsv';
 
     $Collection = RT::Users->new( $session{'CurrentUser'} );
     $Collection->Limit( FIELD => 'id', VALUE => $id );
 
 } elsif ( $Type eq 'Transaction' ) {
     $Format = RT->Config->Get('UserTransactionDataResultFormat') unless $Format;
+    $filename = 'UserTransactionData.tsv';
 
     $Collection = RT::Transactions->new( $session{'CurrentUser'} );
     $Collection->Limit( FIELD => 'ObjectType', VALUE => 'RT::Ticket' );
@@ -81,5 +84,5 @@ if ( $Type eq 'User' ) {
     $Collection->Limit( FIELD => 'Type',       VALUE => 'Comment' );
 }
 
-$m->comp( "/Elements/TSVExport", Collection => $Collection, Format => $Format, PreserveNewLines => $PreserveNewLines );
+$m->comp( "/Elements/TSVExport", Collection => $Collection, Format => $Format, PreserveNewLines => $PreserveNewLines, Filename => $filename );
 </%INIT>

commit c2c7b589ea3f87452e4a4fa75c4e774a91dff340
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Dec 17 12:10:20 2018 -0500

    Add Timezone info to user related data

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 17d912e698..6ab8463bbb 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1403,7 +1403,7 @@ Set($UserDataResultFormat, "'__id__', '__Name__', '__EmailAddress__', '__RealNam
                             '__NickName__', '__Organization__', '__HomePhone__', '__WorkPhone__',\
                             '__MobilePhone__', '__PagerPhone__', '__Address1__', '__Address2__',\
                             '__City__', '__State__','__Zip__', '__Country__', '__Gecos__', '__Lang__',\
-                            '__FreeFormContactInfo__'");
+                            '__Timezone__', '__FreeFormContactInfo__'");
 
 =item C<$UserTransactionDataResultFormat>
 
diff --git a/share/html/Elements/RT__User/ColumnMap b/share/html/Elements/RT__User/ColumnMap
index b4746e39b4..1e6d268c1b 100644
--- a/share/html/Elements/RT__User/ColumnMap
+++ b/share/html/Elements/RT__User/ColumnMap
@@ -146,6 +146,11 @@ my $COLUMN_MAP = {
         title     => 'Status', # loc
         value     => sub { return $_[0]->Disabled? $_[0]->loc('Disabled'): $_[0]->loc('Enabled') },
     },
+    Timezone => {
+        title     => 'Timezone', # loc
+        attribute => 'Timezone',
+        value     => sub { return $_[0]->Timezone },
+    },
 };
 
 </%ONCE>

commit 9e524c99bb8ee483df6b06679487fabf5d6fffa4
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Tue Dec 18 08:28:49 2018 -0500

    Update title of ObjectId in $UserTransactionDataResultFormat

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 6ab8463bbb..56d16c9f8b 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1411,7 +1411,7 @@ This is the format of the user transaction search result for "Download User Tran
 
 =cut
 
-Set($UserTransactionDataResultFormat, "'__ObjectId__', '__id__', '__Created__', '__Description__',\
+Set($UserTransactionDataResultFormat, "'__ObjectId__/TITLE:Ticket Id', '__id__', '__Created__', '__Description__',\
                                         '__OldValue__', '__NewValue__', '__Content__'");
 
 

commit 432139e9fdc0e2fda61cef1cbe8a0d5fdffcd145
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Dec 21 21:40:52 2018 +0800

    No need to show "Download User ..." buttons on user create page

diff --git a/share/html/Admin/Users/Modify.html b/share/html/Admin/Users/Modify.html
index d5e331c294..ef06d3d381 100644
--- a/share/html/Admin/Users/Modify.html
+++ b/share/html/Admin/Users/Modify.html
@@ -232,7 +232,9 @@
 % }
 </form>
 
+% unless ( $Create ) {
 <& /User/Elements/RelatedData, UserObj => $UserObj &>
+% }
 <%INIT>
 
 my $UserObj = RT::User->new($session{'CurrentUser'});

commit 4a75abe33a91c53dfecac0315f80c925abbdf188
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Fri Dec 21 16:24:09 2018 -0500

    Move User related info portlet into side column

diff --git a/share/html/Admin/Users/Modify.html b/share/html/Admin/Users/Modify.html
index ef06d3d381..4b1181fd28 100644
--- a/share/html/Admin/Users/Modify.html
+++ b/share/html/Admin/Users/Modify.html
@@ -125,6 +125,9 @@
 
 <& /Elements/EditCustomFields, Object => $UserObj, Grouping => 'Access control' &>
 
+</&>
+<&| /Widgets/TitleBox, title => loc('Comments about this user'), class => 'user-info-comments' &>
+<textarea class="comments" name="Comments" cols="80" rows="5" wrap="virtual"><%$UserObj->Comments//$ARGS{Comments}//''%></textarea>
 </&>
 % $m->callback( %ARGS, CallbackName => 'LeftColumnBottom', UserObj => $UserObj );
 </td>
@@ -207,13 +210,14 @@
 
 <& /Elements/EditCustomFieldCustomGroupings, Object => $UserObj &>
 
+% unless ( $Create ) {
+<& /User/Elements/RelatedData, UserObj => $UserObj &>
+% }
+
 % $m->callback( %ARGS, CallbackName => 'RightColumnBottom', UserObj => $UserObj );
 </td></tr>
 <tr>
 <td colspan="2">
-<&| /Widgets/TitleBox, title => loc('Comments about this user'), class => 'user-info-comments' &>
-<textarea class="comments" name="Comments" cols="80" rows="5" wrap="virtual"><%$UserObj->Comments//$ARGS{Comments}//''%></textarea>
-</&>
 %if (!$Create && $UserObj->Privileged) {
 <br />
 <&| /Widgets/TitleBox, title => loc('Signature'), class => 'user-info-signature' &>
@@ -232,9 +236,6 @@
 % }
 </form>
 
-% unless ( $Create ) {
-<& /User/Elements/RelatedData, UserObj => $UserObj &>
-% }
 <%INIT>
 
 my $UserObj = RT::User->new($session{'CurrentUser'});

commit 0353ae0a78984042c815c5f4b96fde36fa69ed45
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Dec 13 09:47:30 2018 -0500

    Create method AnonymizeUser in User.pm
    
    Create method 'AnonymizeUser' that will remove the personal
    identifying information from a user record, but keep the record alive.

diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index daae994e75..2d6a8cb357 100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -281,6 +281,109 @@ sub ValidateName {
     }
 }
 
+=head2 GenerateAnonymousName
+
+Generate a random username proceeded by 'anon_' and then a
+random string, Returns the AnonymousName string.
+
+=cut
+
+sub GenerateAnonymousName {
+    my $self = shift;
+
+    my $name;
+    do {
+        $name = 'anon_' . Digest::MD5::md5_hex( time . {} . rand() );
+    } while !$self->ValidateName($name);
+
+    return $name;
+}
+
+=head2 AnonymizeUser { ClearCustomfields => 1|0 }
+
+Remove all personal identifying information on the user record, but keep
+the user record alive. Additionally replace the username with an
+anonymous name.  Submit ClearCustomfields in a paramhash, if true all
+customfield values applied to the user record will be cleared.
+
+=cut
+
+sub AnonymizeUser {
+    my $self = shift;
+    my %args = (
+        ClearCustomFields => undef,
+        @_,
+    );
+
+    my %skip_clear = map { $_ => 1 } qw/Name Password AuthToken/;
+    my @user_identifying_info
+      = grep { !$skip_clear{$_} && $self->_Accessible( $_, 'write' ) } keys %{ $self->_CoreAccessible() };
+
+    $RT::Handle->BeginTransaction();
+
+    # Remove identifying user information from record
+    foreach my $attr (@user_identifying_info) {
+        if ( defined $self->$attr && length $self->$attr ) {
+            my $method = 'Set' . $attr;
+            my ( $ret, $msg ) = $self->$method('');
+            unless ($ret) {
+                RT::Logger->error( "Could not clear user $attr: " . $msg );
+                $RT::Handle->Rollback();
+                return ( $ret, $self->loc( "Couldn't clear user [_1]", $self->loc($attr) ) );
+            }
+        }
+    }
+
+    # Do not do anything if password is already unset
+    if ( $self->HasPassword ) {
+        my ( $ret, $msg ) = $self->_Set( Field => 'Password', Value => '*NO-PASSWORD*' );
+        unless ($ret) {
+            RT::Logger->error("Could not clear user password: $msg");
+            $RT::Handle->Rollback();
+            return ( $ret, "Could not clear user Password" );
+        }
+    }
+
+    # Generate the random anon username
+    my ( $ret, $msg ) = $self->SetName( $self->GenerateAnonymousName );
+    unless ($ret) {
+        RT::Logger->error( "Could not anonymize user Name: " . $msg );
+        $RT::Handle->Rollback();
+        return ( $ret, $self->loc( "Could not anonymize user [_1]", $self->loc('Name') ) );
+    }
+
+    # Clear AuthToken
+    if ( $self->_Value('AuthToken') ) {
+        my ( $ret, $msg ) = $self->SetAuthToken('');
+        unless ($ret) {
+            RT::Logger->error( "Could not clear user AuthToken: " . $msg );
+            $RT::Handle->Rollback();
+            return ( $ret, $self->loc( "Couldn't clear user [_1]", $self->loc('AuthToken') ) );
+        }
+    }
+
+    # Remove user customfield values
+    if ( $args{'ClearCustomFields'} ) {
+        my $cfs = RT::CustomFields->new( RT->SystemUser );
+        $cfs->LimitToLookupType('RT::User');
+
+        while ( my $cf = $cfs->Next ) {
+            my $ocfvs = $self->CustomFieldValues($cf);
+            while ( my $ocfv = $ocfvs->Next ) {
+                my ( $ret, $msg ) = $ocfv->Delete;
+                unless ($ret) {
+                    RT::Logger->error( "Could not delete ocfv #" . $ocfv->id . ": $msg" );
+                    $RT::Handle->Rollback();
+                    return ( $ret, $self->loc( "Could not clear user custom field [_1]", $cf->Name ) );
+                }
+            }
+        }
+    }
+    $RT::Handle->Commit();
+
+    return ( 1, $self->loc('User successfully anonymized') );
+}
+
 =head2 ValidatePassword STRING
 
 Returns either (0, "failure reason") or 1 depending on whether the given

commit 5aa4123c2be492d6448b4206bceb303f63a1e6cb
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Dec 13 09:49:00 2018 -0500

    Create modal mason component

diff --git a/share/html/Elements/Modal b/share/html/Elements/Modal
new file mode 100644
index 0000000000..2c36e8af00
--- /dev/null
+++ b/share/html/Elements/Modal
@@ -0,0 +1,71 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2018 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 }}}
+<div id="<% $ModalId %>" class="<% $Class %>" align="center">
+    <form action="<% $Action %>" method="<% $Method %>" id="<% $ModalId %>" name="<% $Name %>" >
+% foreach my $field (@Fields) {
+        <p><% $field->{'Label'} %>
+%   if ( $field->{'Input'} ) {
+        <input type="<% $field->{'Input'} %>" class="<% $field->{'Class'} %>" name="<% $field->{'Name'} %>" value="<% $field->{'Value'} %>">
+%   }
+        </p>
+% }
+        <a href="#" rel="modal:close" class="button"><% $Cancel %></a>
+        <button type="Submit" class="button"><% $Accept %></button>
+    </form>
+</div>
+
+<%ARGS>
+ at Fields   => ()
+$Name     => ''
+$ModalId  => ''
+$Class    => 'modal'
+$Action   => ''
+$Method   => 'GET'
+$Accept   => loc('OK')
+$Cancel   => loc('Cancel')
+</%ARGS>

commit f7b0ddb75e385270cd287cc15f7a8069b639ea83
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Dec 13 09:50:15 2018 -0500

    Enhance "User related info" with actions of clearing user info
    
    The added actions are 'Anonymize user' and 'Replace User'.  Anonymize
    user will call the 'AnonymizeUser' method to clear identifying
    information from the user record. 'Replace User' will link to the
    shredder page with a pre formatted search.
    
    With added actions, "Manage user data" is a more appropriate name.

diff --git a/share/html/Admin/Users/Modify.html b/share/html/Admin/Users/Modify.html
index 4b1181fd28..569b263168 100644
--- a/share/html/Admin/Users/Modify.html
+++ b/share/html/Admin/Users/Modify.html
@@ -236,6 +236,21 @@
 % }
 </form>
 
+% if ( $UserObj->Id ) {
+    <& /Elements/Modal, ModalId => "user-info-modal", Method => 'POST', Action => RT->Config->Get('WebPath') . '/Admin/Users/Modify.html', Fields => [
+    { Label   => loc("Are you sure you want to anonymize user: [_1]?", $UserObj->Name) },
+    { Input => 'Hidden', Value => $UserObj->Id, Name => 'id' },
+    { Input => 'Hidden', Value => 1, Name => 'Anonymize' },
+    {
+        Label    => loc("Check to clear user custom fields") . ":",
+        Input    => 'checkbox',
+        Class    => 'checkbox',
+        Name     => 'clear_customfields',
+        Value    => 'On',
+    },
+]
+&>
+% }
 <%INIT>
 
 my $UserObj = RT::User->new($session{'CurrentUser'});
@@ -299,6 +314,11 @@ if ($Create) {
     }
 }
 
+if ( $ARGS{'Anonymize'} and $UserObj->Id ) {
+    my ($ret, $msg) = $UserObj->AnonymizeUser(ClearCustomFields => $ARGS{'clear_customfields'});
+    push @results, $msg;
+}
+
 if ( $UserObj->Id ) {
     # Deal with Password field
     my ($status, $msg) = $UserObj->SafeSetPassword(
diff --git a/share/html/User/Elements/RelatedData b/share/html/User/Elements/RelatedData
index cad301c714..4d1ed31e4f 100644
--- a/share/html/User/Elements/RelatedData
+++ b/share/html/User/Elements/RelatedData
@@ -47,13 +47,45 @@
 %# END BPS TAGGED BLOCK }}}
 <&|/Widgets/TitleBox,
     class => 'user-related-info',
-    title => loc("User related info"),
+    title => loc("Manage user data"),
 &>
 
-<div>
-    <a href="/User/RelatedData.tsv?Type=User&id=<% $UserObj->id %>" class="button"><% $UserDataButton %></a>
-    <a href="/Search/Results.tsv?UserData=1&Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>" class="button"><% $UserTicketsButton %></a>
-    <a href="/User/RelatedData.tsv?Type=Transaction&id=<% $UserObj->id %>" class="button"><% $UserTxnButton %></a>
+<div id="manage-user-data">
+    <div class="title"><&|/l&>Download User Information</&></div>
+    <div class="download-user-data-buttons inline-row">
+        <div class="inline-cell">
+            <a class="button" href="/User/RelatedData.tsv?Type=User&id=<% $UserObj->id %>"><% $UserDataButton %></a>
+            <i class="label"><&|/l&>The basic user info</&></i>
+        </div>
+        <div class="inline-cell">
+            <a class="button" href="/Search/Results.tsv?UserData=1&Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>"><% $UserTicketsButton %></a>
+            <i class="label"><&|/l&>Tickets with the user as a requestor</&></i>
+        </div>
+        <div class="inline-cell">
+            <a class="button" href="/User/RelatedData.tsv?Type=Transaction&id=<% $UserObj->id %>"><% $UserTxnButton %></a>
+            <i class="label"><&|/l&>Ticket transactions the user created</&></i>
+        </div>
+    </div>
+
+% if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'AdminUsers' ) ) {
+    <div class="title"><&|/l&>Remove User Information</&></div>
+    <div class="inline-row">
+        <div class="inline-cell">
+            <a class="button" href="#user-info-modal" rel="modal:open" name="anonymize_user"><&|/l&>Anonymize User</&></a>
+            <i class="label"><&|/l&>Remove user information with anonymous username</&></i>
+        </div>
+% if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUser' ) ) {
+        <div class="inline-cell">
+            <a class="button" href="<%RT->Config->Get('WebPath')%>/Admin/Tools/Shredder/index.html?Plugin=Users&Users%3Astatus=<% $UserObj->Disabled ? 'disabled' : 'enabled' %>&Users%3Aname=<% $UserObj->Name %>&Users%3Areplace_relations=Nobody&Search=Search" name="replace-user"><&|/l&>Replace User</&></a>
+            <i class="label"><&|/l&>Replace user links in the database with "Nobody" user</&></i>
+        </div>
+        <div class="inline-cell">
+            <a class="button" href="<%RT->Config->Get('WebPath')%>/Admin/Tools/Shredder/index.html?Plugin=Users&Users%3Astatus=<% $UserObj->Disabled ? 'disabled' : 'enabled' %>&Users%3Aname=<% $UserObj->Name %>&Users&Search=Search" name="replace-user"><&|/l&>Replace User</&></a>
+            <i class="label"><&|/l&>Remove all references to user (Tickets linked to this user must be shredded first)</&></i>
+        </div>
+% }
+    </div>
+% }
 </div>
 </&>
 
@@ -63,7 +95,7 @@ my $Format = RT->Config->Get('UserTicketDataResultFormat') || RT->Config->Get('D
 
 <%ARGS>
 $UserObj
-$UserDataButton    => loc( 'Download User Data' )
-$UserTicketsButton => loc( 'Download User Tickets' )
-$UserTxnButton     => loc( 'Download User Transaction Data' )
+$UserDataButton    => loc( 'User Data' )
+$UserTicketsButton => loc( 'User Tickets' )
+$UserTxnButton     => loc( 'User Transactions' )
 </%ARGS>
diff --git a/share/static/css/base/admin.css b/share/static/css/base/admin.css
index 95c5878031..eaa185c368 100644
--- a/share/static/css/base/admin.css
+++ b/share/static/css/base/admin.css
@@ -82,3 +82,29 @@ table.upgrade-history .upgrade-history-parent .widget a {
 table.upgrade-history .upgrade-history-parent .widget a.rolled-up {
     background-image: url(../../../static/images/css/rolldown-arrow.gif);
 }
+
+#manage-user-data div.title {
+    margin-bottom: 5px;
+    font-weight: bold;
+}
+
+div.inline-row {
+    margin-bottom: 10px;
+    display: inline-flex;
+    width: 100%;
+}
+
+div.inline-row div {
+    max-width: 175px;
+    display: block;
+}
+
+div.inline-row a {
+    text-align: center;
+    width: 85%;
+}
+
+div.inline-row i {
+    text-align: left;
+    width: 85%;
+}

commit 767e5468f5be6e50074e87bcda6407f5c6c5acb8
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Dec 13 09:52:42 2018 -0500

    Create test for remove user information

diff --git a/t/web/remove_user_info.t b/t/web/remove_user_info.t
new file mode 100644
index 0000000000..4003f32d99
--- /dev/null
+++ b/t/web/remove_user_info.t
@@ -0,0 +1,121 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+RT::Config->Set( 'ShredderStoragePath', RT::Test->temp_directory . '' );
+
+my ( $baseurl, $agent ) = RT::Test->started_ok;
+
+diag("Test server running at $baseurl");
+my $url = $agent->rt_base_url;
+
+# Login
+$agent->login( 'root' => 'password' );
+
+# Anonymize User
+{
+    my %skip_clear = map { $_ => 1 } qw/Name Password AuthToken/;
+    my @user_identifying_info
+      = grep { !$skip_clear{$_} && RT::User->_Accessible( $_, 'write' ) } keys %{ RT::User->_CoreAccessible() };
+
+    my $user = RT::Test->load_or_create_user(
+        map( { $_ => 'test_string' } @user_identifying_info, 'AuthToken' ),
+        Name         => 'Test User',
+        EmailAddress => 'test at example.com',
+    );
+    ok( $user && $user->id );
+
+    foreach my $attr (@user_identifying_info) {
+        ok( $user->$attr, 'Attribute ' . $attr . ' is set' );
+    }
+
+    my $user_id = $user->id;
+
+    $agent->get_ok( $url . "Admin/Users/Modify.html?id=" . $user_id );
+    $agent->follow_link_ok( { text => 'Anonymize User' } );
+
+    $agent->submit_form_ok( { form_id => 'user-info-modal', }, "Anonymize user" );
+
+    # UserId is still the same, but all other records should be anonimyzed for TestUser
+    my ( $ret, $msg ) = $user->Load($user_id);
+    ok($ret);
+
+    like( $user->Name, qr/anon_/, 'Username replaced with anon name' );
+
+    $user->Load($user_id);
+
+    # Ensure that all other user fields are unset
+    foreach my $attr (@user_identifying_info) {
+        ok( !$user->$attr, 'Attribute ' . $attr . ' is unset' );
+    }
+
+    ok( !$user->HasPassword, 'Password is unset' );
+    # Can't call AuthToken here because it creates new one automatically
+    ok( !$user->_Value('AuthToken'), 'Authtoken is unset' );
+
+    # Test that customfield values are removed with anonymize user action
+    my $customfield = RT::CustomField->new( RT->SystemUser );
+    ( $ret, $msg ) = $customfield->Create(
+        Name       => 'TestCustomfield',
+        LookupType => 'RT::User',
+        Type       => 'FreeformSingle',
+    );
+    ok( $ret, $msg );
+
+    ( $ret, $msg ) = $customfield->AddToObject($user);
+    ok( $ret, "Added CF to user object - " . $msg );
+
+    ( $ret, $msg ) = $user->AddCustomFieldValue(
+        Field => 'TestCustomfield',
+        Value => 'Testing'
+    );
+    ok( $ret, $msg );
+
+    is( $user->FirstCustomFieldValue('TestCustomfield'), 'Testing', 'Customfield exists and has value for user.' );
+
+    $agent->get_ok( $url . "Admin/Users/Modify.html?id=" . $user->id );
+    $agent->follow_link_ok( { text => 'Anonymize User' } );
+
+    $agent->submit_form_ok(
+        {   form_id => 'user-info-modal',
+            fields  => { clear_customfields => 'On' },
+        },
+        "Anonymize user and customfields"
+    );
+
+    is( $user->FirstCustomFieldValue('TestCustomfield'), undef, 'Customfield value cleared' );
+}
+
+# Test replace user
+{
+    my $user = RT::Test->load_or_create_user(
+        Name       => 'user',
+        Password   => 'password',
+        Privileged => 1
+    );
+    ok( $user && $user->id );
+    my $id = $user->id;
+
+    ok( RT::Test->set_rights( { Principal => $user, Right => [qw(SuperUser)] }, ), 'set rights' );
+
+    ok( $agent->logout );
+    ok( $agent->login( 'root' => 'password' ) );
+
+    $agent->get_ok( $url . "Admin/Users/Modify.html?id=" . $user->id );
+    $agent->follow_link_ok( { text => 'Replace User' } );
+
+    $agent->submit_form_ok(
+        {   form_id => 'shredder-search-form',
+            fields  => { WipeoutObject => 'RT::User-' . $user->Name, },
+            button  => 'Wipeout'
+        },
+        "Replace user"
+    );
+
+    my ( $ret, $msg ) = $user->Load($id);
+
+    is( $ret, 0, 'User successfully deleted with replace' );
+}
+
+done_testing();

commit fe113f92255949fb5d38a1549ddd79e616b8a40e
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Mon Dec 17 16:36:51 2018 -0500

    Update download user info tests
    
    Adding Timzeone and ObjectId for transactions requires tests to be
    updated.

diff --git a/share/html/SelfService/User/Elements/RelatedData b/share/html/SelfService/User/Elements/RelatedData
index 4ad2469cda..f83413a1d8 100644
--- a/share/html/SelfService/User/Elements/RelatedData
+++ b/share/html/SelfService/User/Elements/RelatedData
@@ -47,13 +47,23 @@
 %# END BPS TAGGED BLOCK }}}
 <&|/Widgets/TitleBox,
     class => 'user-related-info',
-    title => loc("User related info"),
+    title => loc("Download My Data"),
 &>
 
 <div>
-    <a href="/SelfService/User/RelatedData.tsv?Type=User&id=<% $UserObj->id %>" class="button"><% $UserDataButton %></a>
-    <a href="/SelfService/Search/Results.tsv?Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>" class="button"><% $UserTicketsButton %></a>
-    <a href="/SelfService/User/RelatedData.tsv?Type=Transaction&id=<% $UserObj->id %>" class="button"><% $UserTxnButton %></a>
+<div id="download-my-data" class="inline-row">
+    <div class="inline-cell">
+        <a href="/SelfService/User/RelatedData.tsv?Type=User&id=<% $UserObj->id %>" class="button"><% $UserDataButton %></a>
+        <i class="label"><&|/l&>Base user data</&></i>
+    </div>
+    <div class="inline-cell">
+        <a href="/SelfService/Search/Results.tsv?Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>" class="button"><% $UserTicketsButton %></a>
+        <i class="label"><&|/l&>Tickets with you as a requestor</&></i>
+    </div>
+    <div class="inline-cell">
+        <a href="/SelfService/User/RelatedData.tsv?Type=Transaction&id=<% $UserObj->id %>" class="button"><% $UserTxnButton %></a>
+        <i class="label"><&|/l&>Replies you sent</&></i>
+    </div>
 </div>
 </&>
 
@@ -63,7 +73,7 @@ my $Format = RT->Config->Get('UserTicketDataResultFormat') || RT->Config->Get('D
 
 <%ARGS>
 $UserObj
-$UserDataButton    => loc( 'Download My Data' )
-$UserTicketsButton => loc( 'Download My Tickets' )
-$UserTxnButton     => loc( 'Download My Transaction Data' )
+$UserDataButton    => loc( 'My Personal Data' )
+$UserTicketsButton => loc( 'My Tickets' )
+$UserTxnButton     => loc( 'My Transactions' )
 </%ARGS>
diff --git a/share/html/User/Elements/RelatedData b/share/html/User/Elements/RelatedData
index 4d1ed31e4f..e56f8e97c7 100644
--- a/share/html/User/Elements/RelatedData
+++ b/share/html/User/Elements/RelatedData
@@ -55,15 +55,15 @@
     <div class="download-user-data-buttons inline-row">
         <div class="inline-cell">
             <a class="button" href="/User/RelatedData.tsv?Type=User&id=<% $UserObj->id %>"><% $UserDataButton %></a>
-            <i class="label"><&|/l&>The basic user info</&></i>
+            <i class="label"><&|/l&>Core user data</&></i>
         </div>
         <div class="inline-cell">
             <a class="button" href="/Search/Results.tsv?UserData=1&Query=Requestor.id=<% $UserObj->id %>&Format=<% $Format | un %>"><% $UserTicketsButton %></a>
-            <i class="label"><&|/l&>Tickets with the user as a requestor</&></i>
+            <i class="label"><&|/l&>Tickets with this user as a requestor</&></i>
         </div>
         <div class="inline-cell">
             <a class="button" href="/User/RelatedData.tsv?Type=Transaction&id=<% $UserObj->id %>"><% $UserTxnButton %></a>
-            <i class="label"><&|/l&>Ticket transactions the user created</&></i>
+            <i class="label"><&|/l&>Ticket transactions this user created</&></i>
         </div>
     </div>
 
@@ -72,16 +72,16 @@
     <div class="inline-row">
         <div class="inline-cell">
             <a class="button" href="#user-info-modal" rel="modal:open" name="anonymize_user"><&|/l&>Anonymize User</&></a>
-            <i class="label"><&|/l&>Remove user information with anonymous username</&></i>
+            <i class="label"><&|/l&>Clear core user data, set anonymous username</&></i>
         </div>
 % if ( $session{'CurrentUser'}->HasRight( Object => RT->System, Right => 'SuperUser' ) ) {
         <div class="inline-cell">
             <a class="button" href="<%RT->Config->Get('WebPath')%>/Admin/Tools/Shredder/index.html?Plugin=Users&Users%3Astatus=<% $UserObj->Disabled ? 'disabled' : 'enabled' %>&Users%3Aname=<% $UserObj->Name %>&Users%3Areplace_relations=Nobody&Search=Search" name="replace-user"><&|/l&>Replace User</&></a>
-            <i class="label"><&|/l&>Replace user links in the database with "Nobody" user</&></i>
+            <i class="label"><&|/l&>Replace this user's activity records with "Nobody" user</&></i>
         </div>
         <div class="inline-cell">
-            <a class="button" href="<%RT->Config->Get('WebPath')%>/Admin/Tools/Shredder/index.html?Plugin=Users&Users%3Astatus=<% $UserObj->Disabled ? 'disabled' : 'enabled' %>&Users%3Aname=<% $UserObj->Name %>&Users&Search=Search" name="replace-user"><&|/l&>Replace User</&></a>
-            <i class="label"><&|/l&>Remove all references to user (Tickets linked to this user must be shredded first)</&></i>
+            <a class="button" href="<%RT->Config->Get('WebPath')%>/Admin/Tools/Shredder/index.html?Plugin=Users&Users%3Astatus=<% $UserObj->Disabled ? 'disabled' : 'enabled' %>&Users%3Aname=<% $UserObj->Name %>&Users&Search=Search" name="replace-user"><&|/l&>Delete User</&></a>
+            <i class="label"><&|/l&>Delete this user, tickets associated with this user must be shredded first</&></i>
         </div>
 % }
     </div>
diff --git a/share/static/css/base/admin.css b/share/static/css/base/admin.css
index eaa185c368..fb8a17862d 100644
--- a/share/static/css/base/admin.css
+++ b/share/static/css/base/admin.css
@@ -95,7 +95,7 @@ div.inline-row {
 }
 
 div.inline-row div {
-    max-width: 175px;
+    max-width: 150px;
     display: block;
 }
 
diff --git a/t/web/download_user_info.t b/t/web/download_user_info.t
index cc100d686e..44155d49a7 100644
--- a/t/web/download_user_info.t
+++ b/t/web/download_user_info.t
@@ -46,11 +46,11 @@ $agent->login( 'root' => 'password' );
 
     # TSV file for user record information
     $agent->get_ok( $url . "Admin/Users/Modify.html?id=" . $root->id );
-    $agent->follow_link_ok( { text => 'Download User Data' } );
+    $agent->follow_link_ok( { text => 'User Data' } );
 
     my $user_info_tsv = <<EOF;
-id\tName\tEmailAddress\tRealName\tNickName\tOrganization\tHomePhone\tWorkPhone\tMobilePhone\tPagerPhone\tAddress1\tAddress2\tCity\tState\tZip\tCountry\tGecos\tLang\tFreeFormContactInfo
-14\troot\troot\@localhost\tEnoch Root\t\t\t\t\t\t\t\t\t\t\t\t\troot\t\t
+id\tName\tEmailAddress\tRealName\tNickName\tOrganization\tHomePhone\tWorkPhone\tMobilePhone\tPagerPhone\tAddress1\tAddress2\tCity\tState\tZip\tCountry\tGecos\tLang\tTimezone\tFreeFormContactInfo
+14\troot\troot\@localhost\tEnoch Root\t\t\t\t\t\t\t\t\t\t\t\t\troot\t\t\t
 EOF
 
     is $agent->content, $user_info_tsv,
@@ -58,10 +58,10 @@ EOF
 
     # TSV file for Transactions
     $agent->get_ok( $url . "Admin/Users/Modify.html?id=" . $root->id );
-    $agent->follow_link_ok( { text => 'Download User Transaction Data' } );
+    $agent->follow_link_ok( { text => 'User Transactions' } );
 
     my $transaction_info_tsv = <<EOF;
-ObjectId\tid\tCreated\tDescription\tOldValue\tNewValue\tContent
+Ticket Id\tid\tCreated\tDescription\tOldValue\tNewValue\tContent
 1\t30\t$date_created\tTicket created\t\t\tThis transaction appears to have no content
 1\t32\t$date_commented\tComments added\t\t\tTest - Comment
 1\t33\t$date_correspondence\tCorrespondence added\t\t\tTest - Reply
@@ -72,7 +72,7 @@ EOF
 
     # TSV file for user's Tickets
     $agent->get_ok( $url . "Admin/Users/Modify.html?id=" . $root->id );
-    $agent->follow_link_ok( { text => 'Download User Tickets' } );
+    $agent->follow_link_ok( { text => 'User Tickets' } );
 
     my $ticket_info_tsv = <<EOF;
 id\tSubject\tStatus\tQueueName\tOwner\tPriority\tRequestors

commit 733604309bb623be59fa4ba7da093d5543c44c09
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Jan 3 11:19:36 2019 -0500

    Clarify self service download config docs

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 56d16c9f8b..6daff76fd2 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -1881,9 +1881,10 @@ Set($SelfServiceRequestUpdatePortlet, 0);
 
 =item C<$SelfServiceDownloadUserData>
 
-Allow Self Service users to download their user information, ticket data
-and transaction data as a .tsv file. When enabled, these three options
-will appear on the /SelfService/Prefs.html page.
+Allow Self Service users to download their user information, ticket data,
+and transaction data as a .tsv file. When enabled, these options
+will appear in the self service interface at Logged in as > Preferences.
+Users also need the ModifySelf right to have access to this page.
 
 =cut
 

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


More information about the rt-commit mailing list