[Rt-commit] rt branch, 4.4/saved-search-links, created. rt-4.4.4-90-gd2fdc4b347

? sunnavy sunnavy at bestpractical.com
Thu Jan 16 17:47:07 EST 2020


The branch, 4.4/saved-search-links has been created
        at  d2fdc4b347cd23a5490c99d8b9f71a0a081eca4b (commit)

- Log -----------------------------------------------------------------
commit 957c290eb21a3040beaaf83c5ece54320b944a86
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 16 15:58:59 2020 +0800

    Add attribute link support
    
    This is initially to record relationships of saved searches and
    dashboards/homepages the former are included.

diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm
index 6d992848bc..dc35774135 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -57,6 +57,7 @@ sub Table {'Attributes'}
 
 use Storable qw/nfreeze thaw/;
 use MIME::Base64;
+use RT::URI::attribute;
 
 
 =head1 NAME
@@ -1061,6 +1062,18 @@ sub Serialize {
     return %store;
 }
 
+=head2 URI
+
+Returns this attribute's URI
+
+=cut
+
+sub URI {
+    my $self = shift;
+    my $uri  = RT::URI::attribute->new( $self->CurrentUser );
+    return $uri->URIForObject($self);
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/URI/attribute.pm b/lib/RT/URI/attribute.pm
new file mode 100644
index 0000000000..1d5d894d9a
--- /dev/null
+++ b/lib/RT/URI/attribute.pm
@@ -0,0 +1,226 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::URI::attribute;
+use base qw/RT::URI::base/;
+
+require RT::Attribute;
+
+=head1 NAME
+
+RT::URI::attribute - Internal URIs for linking to an L<RT::Attribute>
+
+=head1 DESCRIPTION
+
+This class should rarely be used directly, but via L<RT::URI> instead.
+
+Represents, parses, and generates internal RT URIs such as:
+
+    attribute:42
+    attribute://example.com/42
+
+These URIs are used to link between objects in RT such as associating an
+attribute with another attribute.
+
+=head1 METHODS
+
+Much of the interface below is dictated by L<RT::URI> and L<RT::URI::base>.
+
+=head2 Scheme
+
+Return the URI scheme for attributes
+
+=cut
+
+sub Scheme {"attribute"}
+
+=head2 LocalURIPrefix
+
+Returns the site-specific prefix for a local attribute URI
+
+=cut
+
+sub LocalURIPrefix {
+    my $self = shift;
+    return $self->Scheme . "://" . RT->Config->Get('Organization');
+}
+
+=head2 IsLocal
+
+Returns a true value, the attribute ID, if this object represents a local attribute,
+undef otherwise.
+
+=cut
+
+sub IsLocal {
+    my $self   = shift;
+    my $prefix = $self->LocalURIPrefix;
+    return $1 if $self->{uri} =~ qr!^\Q$prefix\E/(\d+)!i;
+    return undef;
+}
+
+=head2 URIForObject RT::Attribute
+
+Returns the URI for a local L<RT::Attribute> object
+
+=cut
+
+sub URIForObject {
+    my $self = shift;
+    my $obj  = shift;
+    return $self->LocalURIPrefix . '/' . $obj->Id;
+}
+
+=head2 ParseURI URI
+
+Primarily used by L<RT::URI> to set internal state.
+
+Figures out from an C<attribute:> URI whether it refers to a local attribute and the
+attribute ID.
+
+Returns the attribute ID if local, otherwise returns false.
+
+=cut
+
+sub ParseURI {
+    my $self = shift;
+    my $uri  = shift;
+
+    my $scheme = $self->Scheme;
+
+    # canonicalize "42" and "attribute:42" -> attribute://example.com/42
+    if ( $uri =~ /^(?:\Q$scheme\E:)?(\d+)$/i ) {
+        my $attribute_obj = RT::Attribute->new( $self->CurrentUser );
+        my ( $ret, $msg ) = $attribute_obj->Load($1);
+
+        if ($ret) {
+            $self->{'uri'}    = $attribute_obj->URI;
+            $self->{'object'} = $attribute_obj;
+        }
+        else {
+            RT::Logger->error("Unable to load attribute for id: $1: $msg");
+            return;
+        }
+    }
+    else {
+        $self->{'uri'} = $uri;
+    }
+
+    my $attribute = RT::Attribute->new( $self->CurrentUser );
+    if ( my $id = $self->IsLocal ) {
+        $attribute->Load($id);
+
+        if ( $attribute->id ) {
+            $self->{'object'} = $attribute;
+        }
+        else {
+            RT->Logger->error("Can't load Attribute #$id by URI '$uri'");
+            return;
+        }
+    }
+    return $attribute->id;
+}
+
+=head2 Object
+
+Returns the object for this URI, if it's local. Otherwise returns undef.
+
+=cut
+
+sub Object {
+    my $self = shift;
+    return $self->{'object'};
+}
+
+=head2 HREF
+
+If this is a local dashboard, return an HTTP URL for it.
+
+Otherwise, return its URI.
+
+=cut
+
+sub HREF {
+    my $self = shift;
+    if ( $self->IsLocal and $self->Object ) {
+        if ( $self->Object->Name eq 'Dashboard' ) {
+            return RT->Config->Get('WebURL') . "Dashboards/" . $self->Object->Id . '/' . $self->Object->Description;
+        }
+    }
+    return $self->URI;
+}
+
+=head2 AsString
+
+Returns a description of this object
+
+=cut
+
+sub AsString {
+    my $self = shift;
+    if ( $self->IsLocal and $self->Object ) {
+        my $object = $self->Object;
+        if ( $object->Name eq 'Dashboard' ) {
+            return $self->loc( 'Dashboard #[_1]: [_2]', $object->id, $object->Description );
+        }
+        elsif ( $object->Name eq 'SavedSearch' ) {
+            return $self->loc( 'Saved Search #[_1]: [_2]', $object->id, $object->Description );
+        }
+        else {
+            return $self->loc( 'Attribute #[_1]: [_2]', $object->id, $object->Name );
+        }
+    }
+    else {
+        return $self->SUPER::AsString(@_);
+    }
+}
+
+1;

commit f9958ef94b2861309b8004bef1dfb681b517f7a1
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 16 15:59:15 2020 +0800

    Link dashboards/homepages to saved searches they include
    
    Thus we can easily find dashboards/homepages that depend on saved
    searches, and warn users when people try to delete saved searches.

diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm
index dc35774135..3491093937 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -217,6 +217,7 @@ sub Create {
         }
     }
 
+    $self->_SyncLinks if $return[0];
     return wantarray ? @return : $return[0];
 }
 
@@ -314,7 +315,10 @@ sub SetContent {
         }
     }
     my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
-    return ($ok, $self->loc("Attribute updated")) if $ok;
+    if ($ok) {
+        $self->_SyncLinks;
+        return ( $ok, $self->loc("Attribute updated") );
+    }
     return ($ok, $msg);
 }
 
@@ -411,9 +415,17 @@ sub Delete {
         return ( 0, $self->loc('Permission Denied') );
     }
 
-    # Get values even if current user doesn't have right to see
-    $args{'RecordTransaction'} //= 1 if $self->__Value('Name') =~ /^(?:SavedSearch|Dashboard|Subscription)$/;
+    my $name = $self->__Value('Name');
+    my @links;
+    if ( $name =~ /^(Dashboard|(?:Pref-)?HomepageSettings)$/ ) {
+        push @links, @{ $self->DependsOn->ItemsArrayRef };
+    }
+    elsif ( $name eq 'SavedSearch' ) {
+        push @links, @{ $self->DependedOnBy->ItemsArrayRef };
+    }
 
+    # Get values even if current user doesn't have right to see
+    $args{'RecordTransaction'} //= 1 if $name =~ /^(?:SavedSearch|Dashboard|Subscription)$/;
     $RT::Handle->BeginTransaction if $args{'RecordTransaction'};
 
     my @return = $self->SUPER::Delete(@_);
@@ -437,6 +449,15 @@ sub Delete {
         }
     }
 
+    if ( $return[0] ) {
+        for my $link (@links) {
+            my ( $ret, $msg ) = $link->Delete;
+            if ( !$ret ) {
+                RT->Logger->error( "Couldn't delete link #" . $link->id . ": $msg" );
+            }
+        }
+    }
+
     return @return;
 }
 
@@ -1074,6 +1095,76 @@ sub URI {
     return $uri->URIForObject($self);
 }
 
+
+sub _SyncLinks {
+    my $self = shift;
+    my $name = $self->__Value('Name');
+
+    my $success;
+
+    if ( $name =~ /^(Dashboard|(?:Pref-)?HomepageSettings)$/ ) {
+        my $type    = $1;
+        my $content = $self->_DeserializeContent( $self->__Value('Content') );
+
+        my %searches;
+        if ( $type eq 'Dashboard' ) {
+            %searches
+                = map { $_->{id} => 1 } grep { $_->{portlet_type} eq 'search' } @{ $content->{Panes}{body} },
+                @{ $content->{Panes}{sidebar} };
+        }
+        else {
+            for my $item ( @{ $content->{body} }, @{ $content->{sidebar} } ) {
+                if ( $item->{type} eq 'saved' ) {
+                    if ( $item->{name} =~ /SavedSearch-(\d+)/ ) {
+                        $searches{$1} ||= 1;
+                    }
+                }
+                elsif ( $item->{type} eq 'system' ) {
+                    if ( my $attr
+                        = RT::System->new( $self->CurrentUser )->FirstAttribute( 'Search - ' . $item->{name} ) )
+                    {
+                        $searches{ $attr->id } ||= 1;
+                    }
+                    else {
+                        my $attrs = RT::System->new( $self->CurrentUser )->Attributes;
+                        $attrs->Limit( FIELD => 'Name',        VALUE => 'SavedSearch' );
+                        $attrs->Limit( FIELD => 'Description', VALUE => $item->{name} );
+                        if ( my $attr = $attrs->First ) {
+                            $searches{ $attr->id } ||= 1;
+                        }
+
+                    }
+                }
+            }
+        }
+
+        my $links = $self->DependsOn;
+        while ( my $link = $links->Next ) {
+            next if delete $searches{ $link->TargetObj->id };
+            my ( $ret, $msg ) = $link->Delete;
+            if ( !$ret ) {
+                RT->Logger->error( "Couldn't delete link #" . $link->id . ": $msg" );
+                $success //= 0;
+            }
+        }
+
+        for my $id ( keys %searches ) {
+            my $link = RT::Link->new( $self->CurrentUser );
+            my $attribute = RT::Attribute->new( $self->CurrentUser );
+            $attribute->Load($id);
+            if ( $attribute->id ) {
+                my ( $ret, $msg )
+                    = $link->Create( Type => 'DependsOn', Base => 'attribute:' . $self->id, Target => "attribute:$id" );
+                if ( !$ret ) {
+                    RT->Logger->error( "Couldn't create link for attribute #:" . $self->id . ": $msg" );
+                    $success //= 0;
+                }
+            }
+        }
+    }
+    return $success // 1;
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit 4450ffb6b688866451c66dbdbe1334f32d96bbbd
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 16 05:29:10 2020 +0800

    Add upgrade step to link dashboards/homepages to user saved searches they have

diff --git a/etc/upgrade/4.4.5/content b/etc/upgrade/4.4.5/content
new file mode 100644
index 0000000000..ed72073218
--- /dev/null
+++ b/etc/upgrade/4.4.5/content
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+
+our @Final = (
+    sub {
+        my $attrs = RT::Attributes->new( RT->SystemUser );
+        $attrs->Limit(
+            FIELD    => 'Name',
+            VALUE    => [ 'Dashboard', 'HomepageSettings', 'Pref-HomepageSettings' ],
+            OPERATOR => 'IN',
+        );
+        while ( my $attr = $attrs->Next ) {
+            my ( $ret, $msg ) = $attr->_SyncLinks;
+            if ( !$ret ) {
+                die "Couldn't sync links for attribute #" . $attr->id . ": $msg";
+            }
+        }
+    }
+);

commit abf07c2312bbf044e11c4fe8fb8bd0e404aa6a5e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 17 00:42:25 2020 +0800

    Add $LinkTarget param to ShowUser to specify the target
    
    This is mainly to support to open user links in new tabs by specifying
    $LinkTarget to "_blank"

diff --git a/share/html/Elements/ShowUser b/share/html/Elements/ShowUser
index 4dc41af810..74f3aaab4f 100644
--- a/share/html/Elements/ShowUser
+++ b/share/html/Elements/ShowUser
@@ -83,10 +83,11 @@ $User => undef
 $Address => undef
 $style => undef
 $Link => 1
+$LinkTarget => ''
 </%ARGS>
 <span class="user" <% $User && $User->id ? 'data-user-id="'.$User->id.'"' : "" |n %>>\
 % if ($Link and $User and $User->id and not $system_user{$User->id} and $session{CurrentUser}->Privileged) {
-<a href="<% RT->Config->Get("WebPath") %>/User/Summary.html?id=<% $User->id %>">\
+<a <% $LinkTarget ? "target=$LinkTarget" : '' |n %> href="<% RT->Config->Get("WebPath") %>/User/Summary.html?id=<% $User->id %>">\
 <% $display %>\
 </a>\
 % } else {

commit 3a4e338387c11bd6855b9298eb71857f797e8d2a
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 16 05:32:00 2020 +0800

    Confirm before deleting saved searches that are depended on by others

diff --git a/share/html/Search/Elements/EditSearches b/share/html/Search/Elements/EditSearches
index 6e7220eb13..69b46a8cf4 100644
--- a/share/html/Search/Elements/EditSearches
+++ b/share/html/Search/Elements/EditSearches
@@ -61,7 +61,7 @@
 % if ( $Dirty ) {
 <input type="submit" class="button" name="SavedSearchRevert" value="<%loc('Revert')%>" />
 % }
-<input type="submit" class="button" name="SavedSearchDelete" value="<%loc('Delete')%>" />
+<input type="submit" class="button <% $Object->DependedOnBy->Count ? 'confirm' : '' %>" name="SavedSearchDelete" value="<%loc('Delete')%>" />
 % if ( $AllowCopy ) {
 <input type="submit" class="button" name="SavedSearchCopy"   value="<%loc('Save as New')%>" />
 % }
@@ -81,6 +81,31 @@
 
 </&>
 </div>
+
+% if ( $Object && $Object->Id && $Object->DependedOnBy->Count ) {
+    <div class="delete-confirm hidden">
+      <p>
+        <&|/l&>This search is used in these dashboards/homepages, really delete?</&>
+      </p>
+      <ul>
+% my $links = $Object->DependedOnBy;
+% while ( my $link = $links->Next ) {
+        <li>
+% if ( $link->BaseObj->Name eq 'Dashboard' ) {
+          <a href="<% $link->BaseURI->Resolver->HREF %>" target="_blank"><% $link->BaseURI->AsString %></a>
+% } elsif ( $link->BaseObj->ObjectType eq 'RT::System' ) {
+          <% loc('Global') %>
+% } elsif ( $link->BaseObj->ObjectType eq 'RT::User' ) {
+          <% loc('User') %>: <& /Elements/ShowUser, User => $link->BaseObj->Object, LinkTarget => '_blank' &>
+% } else {
+          <% $link->BaseObj->ObjectType %>: #<% $link->BaseObj->ObjectId %>
+% }
+        </li>
+% }
+      </ul>
+      <& /Elements/Submit, Name => 'SavedSearchDelete', Label => loc('Delete') &>
+    </div>
+% }
 <%INIT>
 return unless $session{'CurrentUser'}->HasRight(
     Right  => 'LoadSavedSearch',
@@ -175,6 +200,7 @@ if ( $ARGS{'SavedSearchLoad'} ) {
         } else {
             push @results, loc('Loaded saved search "[_1]"', $SavedSearch->{'Description'} );
         }
+        push @results, loc('This search is used in dashboards/homepages.') if $search->DependedOnBy->Count;
     }
     else {
         push @results, loc( 'Can not load saved search "[_1]"',
diff --git a/share/static/js/util.js b/share/static/js/util.js
index d5bf84562a..dc67d7d659 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -275,6 +275,21 @@ jQuery(function() {
             };
         };
     });
+    jQuery('input[name=SavedSearchDelete].confirm').click(function() {
+        if ( jQuery(this).hasClass('confirmed') ) {
+            return;
+        }
+
+        jQuery("<div class='modal'></div>")
+            .append(jQuery(this).closest('form').find('div.delete-confirm').clone(true).removeClass('hidden')).appendTo("body")
+            .bind('modal:close', function(ev,modal) { modal.elm.remove(); })
+            .modal();
+        return false;
+    });
+
+    jQuery('.delete-confirm input[name=SavedSearchDelete]').click(function() {
+        jQuery('input[name=SavedSearchDelete].confirm').addClass('confirmed').click();
+    });
 });
 
 function textToHTML(value) {

commit d2fdc4b347cd23a5490c99d8b9f71a0a081eca4b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 17 05:43:14 2020 +0800

    Include related links for attribute serialization

diff --git a/lib/RT/Attribute.pm b/lib/RT/Attribute.pm
index 3491093937..b57782fd1a 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -861,6 +861,17 @@ sub FindDependencies {
         $attr->LoadById($content->{DashboardId});
         $deps->Add( out => $attr );
     }
+
+    # Links
+    my $links = RT::Links->new( $self->CurrentUser );
+    $links->Limit(
+        SUBCLAUSE       => "either",
+        FIELD           => $_,
+        VALUE           => $self->URI,
+        ENTRYAGGREGATOR => 'OR',
+        )
+        for qw/Base Target/;
+    $deps->Add( in => $links );
 }
 
 sub PreInflate {

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


More information about the rt-commit mailing list