[Rt-commit] rt branch, 4.4/saved-search-links, created. rt-4.4.4-90-g5c47bd5dff
? sunnavy
sunnavy at bestpractical.com
Thu Jan 16 17:26:29 EST 2020
The branch, 4.4/saved-search-links has been created
at 5c47bd5dff2cfd28c9a3218871d1354dc01b6ebf (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 8c3cad2c3fb12451417dd2d48b0b9c43119c3a65
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..b37198291c 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];
}
@@ -313,8 +314,15 @@ sub SetContent {
return(0, "Content couldn't be frozen");
}
}
+
+ my $name = $self->__Value('Name');
+ my $old_value = $self->__Value('Content') if $name =~ /^(?:Dashboard|(?:Pref-)?HomepageSettings)$/;
+
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 +419,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 +453,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 +1099,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 422300510d2678b375bc20830ca31c38d8e42734
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 0a7668d1404f100e25a9cc8530d2689177c5e008
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 5f94009e9f83c8806f688593595f7dc612d222e5
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 5c47bd5dff2cfd28c9a3218871d1354dc01b6ebf
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 b37198291c..62c8cfefcd 100644
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@ -865,6 +865,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