[Rt-commit] rt branch, 4.4/saved-search-links, created. rt-4.4.4-90-gf73e5c340
? sunnavy
sunnavy at bestpractical.com
Tue Mar 17 16:22:34 EDT 2020
The branch, 4.4/saved-search-links has been created
at f73e5c340cc10f11fb43ab3301fca72a48ac5750 (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 6d992848b..dc3577413 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 000000000..1d5d894d9
--- /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 00aaad5733fa19d2cd268e3068b554eafd338c52
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 dc3577413..85dadf526 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);
}
@@ -412,8 +416,16 @@ sub Delete {
}
# 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 };
+ }
+ $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,85 @@ sub URI {
return $uri->URIForObject($self);
}
+
+=head2 _SyncLinks
+
+For dashboard and homepage attributes, keep links to saved searches they
+include up to date. It does nothing for other attributes.
+
+Returns 1 on success and 0 on failure.
+
+=cut
+
+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 4dad4d797da641ee84b724c81931b9eb8432b192
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 000000000..ed7207321
--- /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 99c330d9afcf60589e0bb7aa85d53856fb19e17f
Author: sunnavy <sunnavy at bestpractical.com>
Date: Fri Jan 17 00:42:25 2020 +0800
Add $LinkTarget param to ShowUser so we can specify the target
Thus we can open user links in new tabs by specifying $LinkTarget to
"_blank"
diff --git a/share/html/Elements/ShowUser b/share/html/Elements/ShowUser
index 4dc41af81..74f3aaab4 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 dc0ebd6ce24002aaae0524cf8cd8caf4b077f811
Author: sunnavy <sunnavy at bestpractical.com>
Date: Thu Jan 16 05:32:00 2020 +0800
Show dependencies and confirm before deleting saved searches
Usually it's wrong to delete saved searches that are depended on by
other attributes(dashboards/homepage prefs), let's show these
dependencies and confirm in case the deletion is not intentional.
diff --git a/share/html/Search/Elements/EditSearches b/share/html/Search/Elements/EditSearches
index 6e7220eb1..cc661772e 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 && $Object->Id && $Object->DependedOnBy->Count ? 'confirm' : '' %>" name="SavedSearchDelete" value="<%loc('Delete')%>" />
% if ( $AllowCopy ) {
<input type="submit" class="button" name="SavedSearchCopy" value="<%loc('Save as New')%>" />
% }
@@ -74,6 +74,13 @@
%}
% }
<br />
+
+% if ( $Object && $Object->Id && $Object->DependedOnBy->Count ) {
+<span class="label"><&|/l&>Depended on by</&>:</span>
+<a href="#" class="view-saved-search-depended-on-by-list"><% loc('View') %></a>
+<br />
+% }
+
<hr />
<span class="label"><&|/l&>Load saved search</&>:</span>
<& SelectSearchesForObjects, Name => 'SavedSearchLoad', Objects => \@LoadObjects, SearchType => $Type &>
@@ -81,6 +88,31 @@
</&>
</div>
+
+% if ( $Object && $Object->Id && $Object->DependedOnBy->Count ) {
+ <div class="delete-saved-search-confirm hidden">
+ <p>
+ <&|/l&>This search is used in these dashboards/homepages, really delete?</&>
+ </p>
+ <ul class="saved-search-depended-on-by-list">
+% 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',
diff --git a/share/static/js/util.js b/share/static/js/util.js
index d5bf84562..660dffb7b 100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@ -275,6 +275,29 @@ 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('.delete-saved-search-confirm').clone(true).removeClass('hidden')).appendTo("body")
+ .bind('modal:close', function(ev,modal) { modal.elm.remove(); })
+ .modal();
+ return false;
+ });
+
+ jQuery('a.view-saved-search-depended-on-by-list').click(function() {
+ jQuery("<div class='modal'></div>")
+ .append(jQuery(this).closest('form').find('.saved-search-depended-on-by-list').clone(true)).appendTo("body")
+ .bind('modal:close', function(ev,modal) { modal.elm.remove(); })
+ .modal();
+ return false;
+ });
+
+ jQuery('.delete-saved-search-confirm input[name=SavedSearchDelete]').click(function() {
+ jQuery('input[name=SavedSearchDelete].confirm').addClass('confirmed').click();
+ });
});
function textToHTML(value) {
commit f73e5c340cc10f11fb43ab3301fca72a48ac5750
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 85dadf526..30c3dbc45 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