[Bps-public-commit] assets branch, query-builder, created. 1.05-1-g9b1256a
? sunnavy
sunnavy at bestpractical.com
Thu Jun 18 11:57:21 EDT 2015
The branch, query-builder has been created
at 9b1256aa57d7b6216b8e64b76d18b98ac558f05d (commit)
- Log -----------------------------------------------------------------
commit 9b1256aa57d7b6216b8e64b76d18b98ac558f05d
Author: sunnavy <sunnavy at bestpractical.com>
Date: Thu Jun 18 23:27:37 2015 +0800
first version of asset query builder
this is mostly a clone from ticket's query builder.
in 4.4 when asset is in core, we shall refactor it to reduce code duplication.
diff --git a/MANIFEST b/MANIFEST
index 117b325..4670f5c 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -40,7 +40,9 @@ html/Asset/Elements/CreateLinkedTicket
html/Asset/Elements/EditBasics
html/Asset/Elements/EditDates
html/Asset/Elements/EditPeople
+html/Asset/Elements/SelectAttachmentField
html/Asset/Elements/SelectCatalog
+html/Asset/Elements/SelectDateType
html/Asset/Elements/SelectStatus
html/Asset/Elements/ShowBasics
html/Asset/Elements/ShowCatalog
@@ -59,8 +61,28 @@ html/Asset/ModifyCFs.html
html/Asset/ModifyDates.html
html/Asset/ModifyLinks.html
html/Asset/ModifyPeople.html
+html/Asset/Search/Build.html
html/Asset/Search/Bulk.html
+html/Asset/Search/Edit.html
+html/Asset/Search/Elements/BuildFormatString
+html/Asset/Search/Elements/ConditionRow
+html/Asset/Search/Elements/DisplayOptions
+html/Asset/Search/Elements/EditFormat
+html/Asset/Search/Elements/EditQuery
+html/Asset/Search/Elements/EditSearches
+html/Asset/Search/Elements/EditSort
+html/Asset/Search/Elements/NewListActions
+html/Asset/Search/Elements/PickAssetCFs
+html/Asset/Search/Elements/PickBasics
+html/Asset/Search/Elements/PickCriteria
+html/Asset/Search/Elements/SearchPrivacy
+html/Asset/Search/Elements/SelectAndOr
+html/Asset/Search/Elements/SelectLinks
+html/Asset/Search/Elements/SelectPersonType
+html/Asset/Search/Elements/SelectSearchesForObjects
+html/Asset/Search/Elements/SelectSearchObject
html/Asset/Search/index.html
+html/Asset/Search/Results.html
html/Asset/Search/Results.tsv
html/Callbacks/RT-Extension-Assets/Elements/AddLinks/ExtraLinkInstructions
html/Callbacks/RT-Extension-Assets/Elements/Tabs/Privileged
@@ -112,6 +134,7 @@ Makefile.PL
MANIFEST This list of files
MANIFEST.SKIP
META.yml
+patches/assets-query-builder.patch
patches/rt-4.2.1-4.2.2.patch
po/assets.pot
po/en.po
diff --git a/README b/README
index 45c99b4..43603b2 100644
--- a/README
+++ b/README
@@ -10,6 +10,8 @@ INSTALLATION
This step may require root permissions.
Patch your RT
+ patch -d /opt/rt4 -p1 < patches/assets-query-builder.patch
+
Assets requires a small patch to work on versions of RT prior to
4.2.3. To patch RT, run:
diff --git a/etc/Assets_Config.pm b/etc/Assets_Config.pm
index d97b5a5..89f1092 100644
--- a/etc/Assets_Config.pm
+++ b/etc/Assets_Config.pm
@@ -131,7 +131,7 @@ something like:
Set($AssetSearchFormat, q[
'<a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a>/TITLE:Name',
Description,
- '__Status__ (__Catalog__)/TITLE:Status',
+ '__Status__ (__Catalog__)/ATTRIBUTE:Status',
Owner,
HeldBy,
Contacts,
@@ -149,7 +149,7 @@ name.
Set($AssetSummaryFormat, q[
'<a href="__WebHomePath__/Asset/Display.html?id=__id__">__Name__</a>/TITLE:Name',
Description,
- '__Status__ (__Catalog__)/TITLE:Status',
+ '__Status__ (__Catalog__)/ATTRIBUTE:Status',
Owner,
HeldBy,
Contacts,
diff --git a/html/Asset/Elements/AssetSearchBasics b/html/Asset/Elements/AssetSearchBasics
index ec1742b..346c1ba 100644
--- a/html/Asset/Elements/AssetSearchBasics
+++ b/html/Asset/Elements/AssetSearchBasics
@@ -54,7 +54,7 @@
</td></tr>
<tr class="asset-status"><td class="label"><label for="Status"><&|/l&>Status</&></label></td>
<td class="value" colspan="3">
-<& /Asset/Elements/SelectStatus, Name => 'Status', CatalogObj => $CatalogObj, DefaultValue => 1,
+<& /Asset/Elements/SelectStatus, Name => 'Status', Catalogs => { $CatalogObj->id => 1 }, DefaultValue => 0,
Default => ($ARGS{'Status'} || '') &>
</td></tr>
<tr class="asset-name"><td class="label"><label for="Name"><&|/l&>Name</&></label></td>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Elements/SelectAttachmentField
similarity index 75%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Elements/SelectAttachmentField
index 48e9df0..fba322c 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Elements/SelectAttachmentField
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,10 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<select name="<%$Name%>">
+<option value="Name"><&|/l&>Name</&></option>
+<option value="Description"><&|/l&>Description</&></option>
+</select>
+<%ARGS>
+$Name => 'AttachmentField'
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Elements/SelectCatalog
index 48e9df0..dfb2bde 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Elements/SelectCatalog
@@ -48,7 +48,7 @@
<& /Elements/SelectObject,
Name => "Catalog",
ShowAll => $ShowAll,
- ShowNullOption => 0,
+ ShowNullOption => $ShowNullOption // 0,
CheckRight => "CreateAsset",
%ARGS,
ObjectType => "Catalog",
@@ -59,6 +59,7 @@
$ShowAll => 0
$Default => undef
$UpdateSession => 1
+$ShowNullOption => undef
</%args>
<%init>
my $catalog_obj = LoadDefaultCatalog($Default || '');
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Elements/SelectDateType
similarity index 75%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Elements/SelectDateType
index 48e9df0..9d5a091 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Elements/SelectDateType
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,10 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<select name="<%$Name%>">
+<option value="Created"><&|/l&>Created</&></option>
+<option value="LastUpdated"><&|/l&>Last Updated</&></option>
+</select>
+<%ARGS>
+$Name => 'DateType'
+</%ARGS>
diff --git a/html/Asset/Elements/SelectStatus b/html/Asset/Elements/SelectStatus
index f16c804..c056325 100644
--- a/html/Asset/Elements/SelectStatus
+++ b/html/Asset/Elements/SelectStatus
@@ -45,22 +45,27 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectStatus, %ARGS &>
+<& /Elements/SelectStatus, %ARGS, Type => 'asset', Object => $AssetObj && $AssetObj->id ? $AssetObj : $CatalogObj, Lifecycles => \@Lifecycles &>
<%init>
-if ($AssetObj and $AssetObj->Id) {
+my @Lifecycles;
+for my $id (keys %Catalogs) {
+ my $catalog = RT::Catalog->new($session{'CurrentUser'});
+ $catalog->Load($id);
+ push @Lifecycles, $catalog->LifecycleObj if $catalog->id;
+}
+
+if ($AssetObj && $AssetObj->id) {
$ARGS{DefaultValue} = 0;
$ARGS{Default} = $DECODED_ARGS->{Status} || $ARGS{Default};
$ARGS{Object} = $AssetObj;
-} else {
- my $lifecycle = ($CatalogObj || "RT::Catalog")->LifecycleObj;
- if ( not $ARGS{DefaultValue} ){
- $ARGS{DefaultValue} = 0;
- $ARGS{Default} ||= $DECODED_ARGS->{Status} || $lifecycle->DefaultOnCreate;
- }
- $ARGS{Statuses} = [ $AssetObj ? $lifecycle->Transitions("") : $lifecycle->Valid ];
+} elsif ( $CatalogObj ) {
+ my $lifecycle = $CatalogObj->LifecycleObj;
+ $ARGS{DefaultValue} = 0;
+ $ARGS{Default} ||= $DECODED_ARGS->{Status} || $lifecycle->DefaultOnCreate;
}
</%init>
<%args>
$AssetObj => undef
$CatalogObj => undef
+%Catalogs => ()
</%args>
diff --git a/html/Asset/Search/Build.html b/html/Asset/Search/Build.html
new file mode 100644
index 0000000..7aa0a95
--- /dev/null
+++ b/html/Asset/Search/Build.html
@@ -0,0 +1,336 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+%#
+%# Data flow here:
+%# The page receives a Query from the previous page, and maybe arguments
+%# corresponding to actions. (If it doesn't get a Query argument, it pulls
+%# one out of the session hash. Also, it could be getting just a raw query from
+%# Build/Edit.html (Advanced).)
+%#
+%# After doing some stuff with default arguments and saved searches, the ParseQuery
+%# function (which is similar to, but not the same as, _parser in lib/RT/Assets.pm)
+%# converts the Query into a RT::Interface::Web::QueryBuilder::Tree. This mason file
+%# then adds stuff to or modifies the tree based on the actions that had been requested
+%# by clicking buttons. It then calls GetQueryAndOptionList on the tree to generate
+%# the SQL query (which is saved as a hidden input) and the option list for the Clauses
+%# box in the top right corner.
+%#
+%# Worthwhile refactoring: the tree manipulation code for the actions could use some cleaning
+%# up. The node-adding code is different in the "add" actions from in ParseQuery, which leads
+%# to things like ParseQuery correctly not quoting numbers in numerical fields, while the "add"
+%# action does quote it (this breaks SQLite).
+%#
+<& /Elements/Header, Title => $title &>
+<& /Elements/Tabs, %TabArgs &>
+
+<form method="post" action="Build.html" name="BuildQuery" id="BuildQuery">
+<input type="hidden" class="hidden" name="SavedSearchId" value="<% $saved_search{'Id'} %>" />
+<input type="hidden" class="hidden" name="Query" value="<% $query{'Query'} %>" />
+<input type="hidden" class="hidden" name="Format" value="<% $query{'Format'} %>" />
+
+
+
+
+<div id="pick-criteria">
+ <& Elements/PickCriteria, query => $query{'Query'}, catalogs => $catalogs &>
+</div>
+<& /Elements/Submit, Label => loc('Add these terms'), SubmitId => 'AddClause', Name => 'AddClause'&>
+<& /Elements/Submit, Label => loc('Add these terms and Search'), SubmitId => 'DoSearch', Name => 'DoSearch'&>
+
+
+<div id="editquery">
+<& Elements/EditQuery,
+ %ARGS,
+ actions => \@actions,
+ optionlist => $optionlist,
+ Description => $saved_search{'Description'},
+ &>
+</div>
+<div id="editsearches">
+ <& Elements/EditSearches, %saved_search, CurrentSearch => \%query &>
+</div>
+
+<span id="display-options">
+<& Elements/DisplayOptions,
+ %ARGS, %query,
+ AvailableColumns => $AvailableColumns,
+ CurrentFormat => $CurrentFormat,
+&>
+</span>
+<& /Elements/Submit, Label => loc('Update format and Search'), Name => 'DoSearch', id=>"formatbuttons"&>
+</form>
+
+<%INIT>
+use RT::Interface::Web::QueryBuilder;
+use RT::Interface::Web::QueryBuilder::Tree;
+
+my $title = loc("Asset Query Builder");
+
+my %query;
+for( qw(Query Format OrderBy Order RowsPerPage) ) {
+ $query{$_} = $ARGS{$_};
+}
+
+my %saved_search;
+my @actions = $m->comp( 'Elements/EditSearches:Init', %ARGS, Query => \%query, SavedSearch => \%saved_search);
+
+if ( $NewQuery ) {
+
+ # Wipe all data-carrying variables clear if we want a new
+ # search, or we're deleting an old one..
+ %query = ();
+ %saved_search = ( Id => 'new', Type => 'Asset', );
+
+ # ..then wipe the session out..
+ delete $session{'CurrentAssetSearchHash'};
+
+ # ..and the search results.
+ $session{'assets'}->CleanSlate if defined $session{'assets'};
+}
+
+{ # Attempt to load what we can from the session and preferences, set defaults
+
+ my $current = $session{'CurrentAssetSearchHash'};
+ my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {};
+ my $default = { Query => '',
+ Format => '',
+ OrderBy => RT->Config->Get('DefaultSearchResultOrderBy'),
+ Order => RT->Config->Get('DefaultSearchResultOrder'),
+ RowsPerPage => 50 };
+
+ for( qw(Query Format OrderBy Order RowsPerPage) ) {
+ $query{$_} = $current->{$_} unless defined $query{$_};
+ $query{$_} = $prefs->{$_} unless defined $query{$_};
+ $query{$_} = $default->{$_} unless defined $query{$_};
+ }
+
+ for( qw(Order OrderBy) ) {
+ if (ref $query{$_} eq "ARRAY") {
+ $query{$_} = join( '|', @{ $query{$_} } );
+ }
+ }
+ if ( $query{'Format'} ) {
+ # Clean unwanted junk from the format
+ $query{'Format'} = $m->comp( '/Elements/ScrubHTML', Content => $query{'Format'} );
+ }
+}
+
+my $ParseQuery = sub {
+ my ($string, $results) = @_;
+
+ my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
+ @$results = $tree->ParseAssetSQL( Query => $string, CurrentUser => $session{'CurrentUser'} );
+
+ return $tree;
+};
+
+my @parse_results;
+my $tree = $ParseQuery->( $query{'Query'}, \@parse_results );
+
+# if parsing went poorly, send them to the edit page to fix it
+if ( @parse_results ) {
+ push @actions, @parse_results;
+ return $m->comp(
+ "Edit.html",
+ Query => $query{'Query'},
+ Format => $query{'Format'},
+ SavedSearchId => $saved_search{'Id'},
+ actions => \@actions,
+ );
+}
+
+my @options = $tree->GetDisplayedNodes;
+my @current_values = grep defined, @options[@clauses];
+my @new_values = ();
+
+my $cf_field_names =
+ join "|",
+ map quotemeta,
+ grep { $RT::Assets::FIELD_METADATA{$_}->[0] eq 'CUSTOMFIELD' }
+ sort keys %RT::Assets::FIELD_METADATA;
+
+# Try to find if we're adding a clause
+foreach my $arg ( keys %ARGS ) {
+ next unless $arg =~ m/^ValueOf(\w+|($cf_field_names).\{.*?\})$/
+ && ( ref $ARGS{$arg} eq "ARRAY"
+ ? grep $_ ne '', @{ $ARGS{$arg} }
+ : $ARGS{$arg} ne '' );
+
+ # We're adding a $1 clause
+ my $field = $1;
+
+ my ($op, $value);
+
+ #figure out if it's a grouping
+ my $keyword = $ARGS{ $field . "Field" } || $field;
+
+ my ( @ops, @values );
+ if ( ref $ARGS{ 'ValueOf' . $field } eq "ARRAY" ) {
+ # we have many keys/values to iterate over, because there is
+ # more than one CF with the same name.
+ @ops = @{ $ARGS{ $field . 'Op' } };
+ @values = @{ $ARGS{ 'ValueOf' . $field } };
+ }
+ else {
+ @ops = ( $ARGS{ $field . 'Op' } );
+ @values = ( $ARGS{ 'ValueOf' . $field } );
+ }
+ $RT::Logger->error("Bad Parameters passed into Query Builder")
+ unless @ops == @values;
+
+ for ( my $i = 0; $i < @ops; $i++ ) {
+ my ( $op, $value ) = ( $ops[$i], $values[$i] );
+ next if !defined $value || $value eq '';
+ my $rawvalue = $value;
+
+ if ( $value =~ /^NULL$/i && $op =~ /=/ ) {
+ if ( $op eq '=' ) {
+ $op = "IS";
+ }
+ elsif ( $op eq '!=' ) {
+ $op = "IS NOT";
+ }
+ }
+ elsif ($value =~ /\D/) {
+ $value =~ s/(['\\])/\\$1/g;
+ $value = "'$value'";
+ }
+
+ if ($keyword =~ s/(['\\])/\\$1/g or $keyword =~ /[^{}\w\.]/) {
+ $keyword = "'$keyword'";
+ }
+
+ my $clause = {
+ Key => $keyword,
+ Op => $op,
+ Value => $value,
+ RawValue => $rawvalue,
+ };
+
+ push @new_values, RT::Interface::Web::QueryBuilder::Tree->new($clause);
+ }
+}
+
+
+push @actions, $m->comp('Elements/EditQuery:Process',
+ %ARGS,
+ Tree => $tree,
+ Selected => \@current_values,
+ New => \@new_values,
+);
+
+# Rebuild $Query based on the additions / movements
+
+my $optionlist_arrayref;
+($query{'Query'}, $optionlist_arrayref) = $tree->GetQueryAndOptionList(\@current_values);
+
+my $optionlist = join "\n", map { qq(<option value="$_->{INDEX}" $_->{SELECTED}>)
+ . (" " x (5 * $_->{DEPTH}))
+ . $m->interp->apply_escapes($_->{TEXT}, 'h') . qq(</option>) } @$optionlist_arrayref;
+
+
+my $catalogs = $tree->GetReferencedCatalogs;
+
+# Deal with format changes
+my ( $AvailableColumns, $CurrentFormat );
+( $query{'Format'}, $AvailableColumns, $CurrentFormat ) = $m->comp(
+ 'Elements/BuildFormatString',
+ %ARGS,
+ catalogs => $catalogs,
+ Format => $query{'Format'},
+);
+
+
+# if we're asked to save the current search, save it
+push @actions, $m->comp( 'Elements/EditSearches:Save', %ARGS, Query => \%query, SavedSearch => \%saved_search);
+
+# Populate the "query" context with saved search data
+
+if ($ARGS{SavedSearchSave}) {
+ $query{'SavedSearchId'} = $saved_search{'Id'};
+}
+
+# Push the updates into the session so we don't lose 'em
+
+$session{'CurrentAssetSearchHash'} = {
+ %query,
+ SearchId => $saved_search{'Id'},
+ Object => $saved_search{'Object'},
+ Description => $saved_search{'Description'},
+};
+
+
+# Show the results, if we were asked.
+
+if ( $ARGS{'DoSearch'} ) {
+ my $redir_query_string = $m->comp(
+ '/Elements/QueryString',
+ %query,
+ SavedSearchId => $saved_search{'Id'},
+ );
+ RT::Interface::Web::Redirect(RT->Config->Get('WebURL') . '/Asset/Search/Results.html?' . $redir_query_string);
+ $m->abort;
+}
+
+
+# Build a querystring for the tabs
+
+my %TabArgs = ();
+if ($NewQuery) {
+ $TabArgs{QueryString} = 'NewQuery=1';
+}
+elsif ( $query{'Query'} ) {
+ $TabArgs{QueryArgs} = \%query;
+}
+
+</%INIT>
+
+<%ARGS>
+$NewQuery => 0
+ at clauses => ()
+</%ARGS>
diff --git a/html/Asset/Search/Bulk.html b/html/Asset/Search/Bulk.html
index 74fe7db..cb55865 100644
--- a/html/Asset/Search/Bulk.html
+++ b/html/Asset/Search/Bulk.html
@@ -56,6 +56,9 @@
% foreach my $var ( @{$search{'PassArguments'}} ) {
<input type="hidden" class="hidden" name="<% $var %>" value="<% $ARGS{$var} || '' %>" />
% }
+% foreach my $var (qw(Query Format OrderBy Order Rows Page Token)) {
+<input type="hidden" class="hidden" name="<%$var%>" value="<%$ARGS{$var} || ''%>" />
+%}
<& /Elements/CollectionList,
%search,
Collection => $assets,
@@ -152,11 +155,16 @@ my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
$ARGS{'Catalog'} = $catalog_obj->Id;
my $assets = RT::Assets->new($session{CurrentUser});
-my %search = ProcessAssetsSearchArguments(
- Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
-);
-
-my $DisplayFormat = "'__CheckBox.{UpdateAsset}__',". $search{'Format'};
+my %search;
+if ( $ARGS{Query} ) {
+ $assets->FromSQL($ARGS{Query});
+}
+else {
+ %search = ProcessAssetsSearchArguments(
+ Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+ );
+}
+my $DisplayFormat = "'__CheckBox.{UpdateAsset}__',". $ARGS{Format} || $search{'Format'};
$DisplayFormat =~ s/\s*,\s*('?__NEWLINE__'?)/,$1,''/gi;
my $asset = RT::Asset->new( $session{'CurrentUser'} );
@@ -188,7 +196,7 @@ if ( $ARGS{Update} ) {
MaybeRedirectForResults(
Actions => \@results,
- Arguments => { map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } @{$search{'PassArguments'}} },
+ Arguments => { map { $_ => $ARGS{$_} } grep { defined $ARGS{$_} } @{$search{'PassArguments'}}, qw(Query Format OrderBy Order Rows Page Token) },
);
}
</%INIT>
diff --git a/html/Asset/Search/Results.tsv b/html/Asset/Search/Edit.html
similarity index 60%
copy from html/Asset/Search/Results.tsv
copy to html/Asset/Search/Edit.html
index 41248b6..d019c26 100644
--- a/html/Asset/Search/Results.tsv
+++ b/html/Asset/Search/Edit.html
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,29 +45,43 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<%init>
-my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
-$ARGS{'Catalog'} = $catalog_obj->Id;
+<& /Elements/Header, Title => $title&>
+<& /Elements/Tabs &>
-my $assets = RT::Assets->new($session{CurrentUser});
-ProcessAssetsSearchArguments(
- Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
-);
+<& Elements/NewListActions, actions => \@actions &>
-my $Format = q|id, Name, Description, Status, Catalog, |;
+<form method="post" action="Build.html" id="BuildQueryAdvanced" name="BuildQueryAdvanced">
+<input type="hidden" class="hidden" name="SavedSearchId" value="<% $SavedSearchId %>" />
+<&|/Widgets/TitleBox, title => loc('Query'), &>
+<textarea name="Query" rows="8" cols="72"><% $Query %></textarea>
+</&>
+<&|/Widgets/TitleBox, title => loc('Format'), &>
+<textarea name="Format" rows="8" cols="72"><% $Format %></textarea>
+</&>
+<& /Elements/Submit, Label => loc("Apply"), Reset => 1, Caption => loc("Apply your changes")&>
+</form>
-$Format .= "$_, " for RT::Asset->Roles;
+<%INIT>
+my $title = loc("Edit Query");
+$Format = $m->comp('/Elements/ScrubHTML', Content => $Format);
+my $QueryString = $m->comp('/Elements/QueryString',
+ Query => $Query,
+ Format => $Format,
+ RowsPerPage => $Rows,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ );
-my $CFs = RT::CustomFields->new( $session{CurrentUser} );
-$CFs->LimitToCatalog( $catalog_obj->Id );
-$CFs->LimitToObjectId( 0 ); # LimitToGlobal but no LookupType restriction
-$Format .= "'__CF.{$_}__/TITLE:$_', " for map {$_ = $_->Name; s/['\\]/\\$1/g; $_} @{$CFs->ItemsArrayRef};
+</%INIT>
-$m->callback(CallbackName => "ModifyFormat", Format => \$Format );
-my $comp = "/Asset/Elements/TSVExport";
-$comp = "/Elements/TSVExport" if $m->comp_exists("/Elements/TSVExport");
+<%ARGS>
+$SavedSearchId => 'new'
+$Query => ''
+$Format => ''
+$Rows => '50'
+$OrderBy => 'id'
+$Order => 'ASC'
-$m->comp($comp, Collection => $assets, Format => $Format );
-
-</%init>
+ at actions => ()
+</%ARGS>
diff --git a/html/Asset/Search/Elements/BuildFormatString b/html/Asset/Search/Elements/BuildFormatString
new file mode 100644
index 0000000..8f08e7e
--- /dev/null
+++ b/html/Asset/Search/Elements/BuildFormatString
@@ -0,0 +1,210 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%ARGS>
+$Format => RT->Config->Get('AssetSearchFormat')
+
+%catalogs => ()
+
+$Face => undef
+$Size => undef
+$Link => undef
+$Title => undef
+
+$AddCol => undef
+$RemoveCol => undef
+$ColUp => undef
+$ColDown => undef
+
+$SelectDisplayColumns => undef
+$CurrentDisplayColumns => undef
+</%ARGS>
+<%init>
+# This can't be in a <once> block, because otherwise we return the
+# same \@fields every request, and keep tacking more CustomFields onto
+# it -- and it grows per request.
+
+# All the things we can display in the format string by default
+my @fields = qw(
+ id Name Description Status
+
+ Owner HeldBy Contacts
+ CreatedBy LastUpdatedBy
+
+ Created CreatedRelative
+ LastUpdated LastUpdatedRelative
+
+ RefersTo ReferredToBy
+ DependsOn DependedOnBy
+ MemberOf Members
+ Parents Children
+
+ NEWLINE
+ NBSP
+); # loc_qw
+
+# This callback will only run once and will be removed in 4.4
+# If you want to add a new item to @fields, use the Default callback below.
+$m->callback( CallbackOnce => 1, CallbackName => 'SetFieldsOnce', Fields => \@fields );
+
+my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
+foreach my $id (keys %catalogs) {
+ # Gotta load up the $catalog object, since catalogs get stored by name now.
+ my $catalog = RT::Catalog->new($session{'CurrentUser'});
+ $catalog->Load($id);
+ next unless $catalog->Id;
+ $CustomFields->LimitToCatalog($catalog->Id);
+ $CustomFields->SetContextObject( $catalog ) if keys %catalogs == 1;
+}
+$CustomFields->LimitToCatalog(0);
+
+while ( my $CustomField = $CustomFields->Next ) {
+ push @fields, "CustomField.{" . $CustomField->Name . "}";
+}
+
+$m->callback( Fields => \@fields, ARGSRef => \%ARGS );
+
+my ( @seen);
+
+$Format ||= RT->Config->Get('AssetSearchFormat');
+my @format = $m->comp('/Elements/CollectionAsTable/ParseFormat', Format => $Format);
+foreach my $field (@format) {
+ # "title" is for columns like NEWLINE, which doesn't have "attribute"
+ $field->{Column} = $field->{attribute} || $field->{title} || '<blank>';
+ push @seen, $field;
+}
+
+if ( $RemoveCol ) {
+ # we do this regex match to avoid a non-numeric warning
+ my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/;
+ if ( defined($index) ) {
+ delete $seen[$index];
+ my @temp = @seen;
+ @seen = ();
+ foreach my $element (@temp) {
+ next unless $element;
+ push @seen, $element;
+ }
+ }
+}
+elsif ( $AddCol ) {
+ if ( defined $SelectDisplayColumns ) {
+ my $selected = $SelectDisplayColumns;
+ my @columns;
+ if (ref($selected) eq 'ARRAY') {
+ @columns = @$selected;
+ } else {
+ push @columns, $selected;
+ }
+ foreach my $col (@columns) {
+ my %column = ();
+ $column{Column} = $col;
+
+ if ( $Face eq "Bold" ) {
+ $column{Prefix} .= "<b>";
+ $column{Suffix} .= "</b>";
+ }
+ if ( $Face eq "Italic" ) {
+ $column{Prefix} .= "<i>";
+ $column{Suffix} .= "</i>";
+ }
+ if ($Size) {
+ $column{Prefix} .= "<" . $m->interp->apply_escapes( $Size, 'h' ) . ">";
+ $column{Suffix} .= "</" . $m->interp->apply_escapes( $Size, 'h' ) . ">";
+ }
+
+ if ($Title) {
+ $column{Suffix} .= "/TITLE:" . $m->interp->apply_escapes( $Title, 'h' );
+ }
+ push @seen, \%column;
+ }
+ }
+}
+elsif ( $ColUp ) {
+ my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/;
+ if ( defined $index && ( $index - 1 ) >= 0 ) {
+ my $column = $seen[$index];
+ $seen[$index] = $seen[ $index - 1 ];
+ $seen[ $index - 1 ] = $column;
+ $CurrentDisplayColumns = $index - 1;
+ }
+}
+elsif ( $ColDown ) {
+ my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/;
+ if ( defined $index && ( $index + 1 ) < scalar @seen ) {
+ my $column = $seen[$index];
+ $seen[$index] = $seen[ $index + 1 ];
+ $seen[ $index + 1 ] = $column;
+ $CurrentDisplayColumns = $index + 1;
+ }
+}
+
+
+my @format_string;
+foreach my $field (@seen) {
+ next unless $field;
+ my $row = "";
+ if ( $field->{'original_string'} ) {
+ $row = $field->{'original_string'};
+ }
+ else {
+ $row .= $field->{'Prefix'} if defined $field->{'Prefix'};
+ $row .= "__$field->{'Column'}__"
+ unless ( $field->{'Column'} eq "<blank>" );
+ $row .= $field->{'Suffix'} if defined $field->{'Suffix'};
+ $row =~ s!([\\'])!\\$1!g;
+ $row = "'$row'";
+ }
+ push( @format_string, $row );
+}
+
+$Format = join(",\n", @format_string);
+
+
+return($Format, \@fields, \@seen);
+
+</%init>
diff --git a/html/Asset/Elements/SelectStatus b/html/Asset/Search/Elements/ConditionRow
similarity index 52%
copy from html/Asset/Elements/SelectStatus
copy to html/Asset/Search/Elements/ConditionRow
index f16c804..0ff3be0 100644
--- a/html/Asset/Elements/SelectStatus
+++ b/html/Asset/Search/Elements/ConditionRow
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,22 +45,55 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectStatus, %ARGS &>
-<%init>
-if ($AssetObj and $AssetObj->Id) {
- $ARGS{DefaultValue} = 0;
- $ARGS{Default} = $DECODED_ARGS->{Status} || $ARGS{Default};
- $ARGS{Object} = $AssetObj;
-} else {
- my $lifecycle = ($CatalogObj || "RT::Catalog")->LifecycleObj;
- if ( not $ARGS{DefaultValue} ){
- $ARGS{DefaultValue} = 0;
- $ARGS{Default} ||= $DECODED_ARGS->{Status} || $lifecycle->DefaultOnCreate;
+<tr>
+<td class="label"><% $handle_block->( $Condition->{'Field'}, $Condition->{'Name'} .'Field' ) |n %></td>
+<td class="operator"><% $handle_block->( $Condition->{'Op'}, $Condition->{'Name'} .'Op') |n %></td>
+<td class="value"><% $handle_block->( $Condition->{'Value'}, 'ValueOf'. $Condition->{'Name'} ) |n %></td>
+</tr>
+<%INIT>
+return unless $Condition && $Condition->{'Name'};
+
+$m->callback( Condition => \$Condition );
+return unless $Condition;
+
+my $handle_block;
+$handle_block = sub {
+ my $box = shift;
+ return $box unless ref $box;
+
+ my $name = shift;
+ if ( UNIVERSAL::isa($box, 'ARRAY') ) {
+ my $res = '';
+ $res .= $handle_block->( $_, $name ) foreach @$box;
+ return $res;
}
- $ARGS{Statuses} = [ $AssetObj ? $lifecycle->Transitions("") : $lifecycle->Valid ];
-}
-</%init>
-<%args>
-$AssetObj => undef
-$CatalogObj => undef
-</%args>
+
+ return undef unless UNIVERSAL::isa($box, 'HASH');
+ if ( $box->{'Type'} eq 'component' ) {
+ $box->{'Arguments'} ||= {},
+ return $m->scomp( $box->{'Path'}, %{ $box->{'Arguments'} }, Name => $name );
+ }
+ if ( $box->{'Type'} eq 'text' ) {
+ $box->{id} ||= $box->{name} ||= $name;
+ $box->{value} ||= delete($box->{Default}) || '';
+ return "<input ".join(" ", map{$m->interp->apply_escapes(lc($_),'h')
+ .q{="}.$m->interp->apply_escapes($box->{$_},'h').q{"}}
+ sort keys %$box)." />";
+ }
+ if ( $box->{'Type'} eq 'select' ) {
+ my $res = '';
+ $res .= qq{<select id="$name" name="$name">};
+ my @options = @{ $box->{'Options'} };
+ while( my $k = shift @options ) {
+ my $v = shift @options;
+ $res .= qq{<option value="$k">$v</option>};
+ }
+ $res .= qq{</select>};
+ return $res;
+ }
+};
+
+</%INIT>
+<%ARGS>
+$Condition => {}
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/DisplayOptions
similarity index 75%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/DisplayOptions
index 48e9df0..4bdaf1c 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/DisplayOptions
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,9 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<&| /Widgets/TitleBox, title => loc("Sorting"), id => 'sorting' &>
+<& EditSort, %ARGS &>
+</&>
+<&| /Widgets/TitleBox, title => loc("Display Columns"), id => 'columns' &>
+<& EditFormat, %ARGS &>
+</&>
diff --git a/html/Asset/Search/Elements/EditFormat b/html/Asset/Search/Elements/EditFormat
new file mode 100644
index 0000000..fffec5c
--- /dev/null
+++ b/html/Asset/Search/Elements/EditFormat
@@ -0,0 +1,135 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table class="edit-columns">
+
+<tr>
+<th><&|/l&>Add Columns</&>:</th>
+<th><&|/l&>Format</&>:</th>
+<th></th>
+<th><&|/l&>Show Columns</&>:</th>
+</tr>
+
+<tr>
+
+<td valign="top"><select size="6" name="SelectDisplayColumns" multiple="multiple">
+% my %seen;
+% foreach my $field ( grep !$seen{lc $_}++, @$AvailableColumns) {
+<option value="<% $field %>" <% $selected{$field} ? 'selected="selected"' : '' |n%>>\
+<% $field =~ /^(?:CustomField|CF)\./ ? $field : loc($field) %></option>
+% }
+</select></td>
+<td>
+<div class="row">
+<span class="label"><&|/l&>Link</&>:</span>
+<span class="value">
+<select name="Link">
+<option value="None">-</option>
+<option value="Display"><&|/l&>Display</&></option>
+<option value="Take"><&|/l&>Take</&></option>
+<option value="Respond"><&|/l&>Respond</&></option>
+<option value="Comment"><&|/l&>Comment</&></option>
+<option value="Resolve"><&|/l&>Resolve</&></option>
+</select>
+</span>
+</div>
+<div class="row">
+<span class="label"><&|/l&>Title</&>:</span>
+<span class="value"><input name="Title" size="10" /></span>
+</div>
+<div class="row">
+<span class="label"><&|/l&>Size</&>:</span>
+<span class="value"><select name="Size">
+<option value="">-</option>
+<option value="Small"><&|/l&>Small</&></option>
+<option value="Large"><&|/l&>Large</&></option>
+</select>
+</span>
+</div>
+<div class="row">
+<span class="label"><&|/l&>Style</&>:</span>
+<span class="value"><select name="Face">
+<option value="">-</option>
+<option value="Bold"><&|/l&>Bold</&></option>
+<option value="Italic"><&|/l&>Italic</&></option>
+</select>
+</span>
+</div>
+</td>
+
+<td><input type="submit" class="button" name="AddCol" value=" → " /></td>
+
+<td valign="top">
+<select size="4" name="CurrentDisplayColumns">
+% my $i=0;
+% my $current = $ARGS{CurrentDisplayColumns} || ''; $current =~ s/^\d+>//;
+% foreach my $field ( @$CurrentFormat ) {
+<option value="<% $i++ %>><% $field->{Column} %>" <% $field->{Column} eq $current ? 'selected="selected"' : '' |n%>>\
+<% $field->{Column} =~ /^(?:CustomField|CF)\./ ? $field->{Column} : loc( $field->{Column} ) %></option>
+% }
+</select>
+<br />
+<center>
+<input type="submit" class="button" name="ColUp" value=" ↑ " />
+<input type="submit" class="button" name="ColDown" value=" ↓ " />
+<input type="submit" class="button" name="RemoveCol" value="<%loc('Delete')%>" />
+</center>
+</td>
+
+</tr>
+</table>
+
+<%init>
+my $selected = $ARGS{SelectDisplayColumns};
+$selected = [ $selected ] unless ref $selected;
+my %selected;
+$selected{$_}++ for grep {defined} @{ $selected };
+</%init>
+<%ARGS>
+$CurrentFormat => undef
+$AvailableColumns => undef
+</%ARGS>
diff --git a/html/Asset/Search/Elements/EditQuery b/html/Asset/Search/Elements/EditQuery
new file mode 100644
index 0000000..7bdded1
--- /dev/null
+++ b/html/Asset/Search/Elements/EditQuery
@@ -0,0 +1,243 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& NewListActions, actions => $actions &>
+<&|/Widgets/TitleBox, title => join(': ', grep defined, loc("Current search"), $Description) &>
+
+<select size="10" name="clauses" style="width: 100%" multiple="multiple">
+% $m->out($optionlist);
+</select>
+
+<p align="center">
+<input type="submit" class="button" name="Up" value=" ↑ " />
+<input type="submit" class="button" name="Down" value=" ↓ " />
+<input type="submit" class="button" name="Left" value=" ← " />
+<input type="submit" class="button" name="Right" value=" → " />
+<input type="submit" class="button" name="Toggle" value="<&|/l&>And/Or</&>" />
+<input type="submit" class="button" name="DeleteClause" value="<&|/l&>Delete</&>" />
+%#<input type="submit" class="button" name="EditQuery" value="Advanced" />
+</p>
+
+</&>
+<%ARGS>
+$Description => undef
+$optionlist => ''
+$actions => []
+</%ARGS>
+
+<%METHOD Process>
+<%ARGS>
+$Tree
+$Selected
+ at New => ()
+</%ARGS>
+<%INIT>
+
+my @NewSelection = ();
+
+my @results;
+if ( $ARGS{'Up'} || $ARGS{'Down'} ) {
+ if (@$Selected) {
+ foreach my $value (@$Selected) {
+ my $parent = $value->getParent;
+ my $index = $value->getIndex;
+ my $newindex = $index;
+ $newindex++ if $ARGS{'Down'};
+ $newindex-- if $ARGS{'Up'};
+ if ( $newindex < 0 || $newindex >= $parent->getChildCount ) {
+ push( @results, [ loc("error: can't move up"), -1 ] ) if $ARGS{'Up'};
+ push( @results, [ loc("error: can't move down"), -1 ] ) if $ARGS{'Down'};
+ next;
+ }
+
+ $parent->removeChild( $index );
+ $parent->insertChild( $newindex, $value );
+ }
+ }
+ else {
+ push( @results, [ loc("error: nothing to move"), -1 ] );
+ }
+}
+elsif ( $ARGS{"Left"} ) {
+ if (@$Selected) {
+ foreach my $value (@$Selected) {
+ my $parent = $value->getParent;
+ if( $value->isRoot || $parent->isRoot ) {
+ push( @results, [ loc("error: can't move left"), -1 ] );
+ next;
+ }
+
+ my $grandparent = $parent->getParent;
+ if( $grandparent->isRoot ) {
+ push( @results, [ loc("error: can't move left"), -1 ] );
+ next;
+ }
+
+ my $index = $parent->getIndex;
+ $parent->removeChild($value);
+ $grandparent->insertChild( $index, $value );
+ if ( $parent->isLeaf ) {
+ $grandparent->removeChild($parent);
+ }
+ }
+ }
+ else {
+ push( @results, [ loc("error: nothing to move"), -1 ] );
+ }
+}
+elsif ( $ARGS{"Right"} ) {
+ if (@$Selected) {
+ foreach my $value (@$Selected) {
+ my $parent = $value->getParent;
+ my $index = $value->getIndex;
+
+ my $newparent;
+ if ( $index > 0 ) {
+ my $sibling = $parent->getChild( $index - 1 );
+ $newparent = $sibling unless $sibling->isLeaf;
+ }
+ $newparent ||= RT::Interface::Web::QueryBuilder::Tree->new( $ARGS{'AndOr'} || 'AND', $parent );
+
+ $parent->removeChild($value);
+ $newparent->addChild($value);
+ }
+ }
+ else {
+ push( @results, [ loc("error: nothing to move"), -1 ] );
+ }
+}
+elsif ( $ARGS{"DeleteClause"} ) {
+ if (@$Selected) {
+ my (@top);
+ my %Selected = map { $_ => 1 } @$Selected;
+ foreach my $node ( @$Selected ) {
+ my $tmp = $node->getParent;
+ while ( !$Selected{ $tmp } && !$tmp->isRoot ) {
+ $tmp = $tmp->getParent;
+ }
+ next if $Selected{ $tmp };
+ push @top, $node;
+ }
+
+ my %seen;
+ my @non_siblings_top = grep !$seen{ $_->getParent }++, @top;
+
+ foreach ( @New ) {
+ my $add = $_->clone;
+ foreach my $selected( @non_siblings_top ) {
+ my $newindex = $selected->getIndex + 1;
+ $selected->insertSibling( $newindex, $add );
+ }
+ $add->getParent->setNodeValue( $ARGS{'AndOr'} );
+ push @NewSelection, $add;
+ }
+ @New = ();
+
+ while( my $node = shift @top ) {
+ my $parent = $node->getParent;
+ $parent->removeChild($node);
+ $node->DESTROY;
+ }
+ @$Selected = ();
+ }
+ else {
+ push( @results, [ loc("error: nothing to delete"), -1 ] );
+ }
+}
+elsif ( $ARGS{"Toggle"} ) {
+ if (@$Selected) {
+ my %seen;
+ my @unique_nodes = grep !$seen{ $_ + 0 }++,
+ map ref $_->getNodeValue? $_->getParent: $_,
+ @$Selected;
+
+ foreach my $node ( @unique_nodes ) {
+ if ( $node->getNodeValue eq 'AND' ) {
+ $node->setNodeValue('OR');
+ }
+ else {
+ $node->setNodeValue('AND');
+ }
+ }
+ }
+ else {
+ push( @results, [ loc("error: nothing to toggle"), -1 ] );
+ }
+}
+
+if ( @New && @$Selected ) {
+ my %seen;
+ my @non_siblings_selected = grep !$seen{ $_->getParent }++, @$Selected;
+
+ foreach ( @New ) {
+ my $add = $_->clone;
+ foreach my $selected( @non_siblings_selected ) {
+ my $newindex = $selected->getIndex + 1;
+ $selected->insertSibling( $newindex, $add );
+ }
+ $add->getParent->setNodeValue( $ARGS{'AndOr'} );
+ push @NewSelection, $add;
+ }
+ @$Selected = ();
+}
+elsif ( @New ) {
+ foreach ( @New ) {
+ my $add = $_->clone;
+ $Tree->addChild( $add );
+ push @NewSelection, $add;
+ }
+ $Tree->setNodeValue( $ARGS{'AndOr'} );
+}
+$_->DESTROY foreach @New;
+
+push @$Selected, @NewSelection;
+
+$Tree->PruneChildlessAggregators;
+
+return @results;
+</%INIT>
+</%METHOD>
diff --git a/html/Asset/Search/Elements/EditSearches b/html/Asset/Search/Elements/EditSearches
new file mode 100644
index 0000000..491e5c4
--- /dev/null
+++ b/html/Asset/Search/Elements/EditSearches
@@ -0,0 +1,329 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="edit-saved-searches">
+<&| /Widgets/TitleBox, title => loc($Title)&>
+
+%# Hide all the save functionality if the user shouldn't see it.
+% if ( $can_modify ) {
+<span class="label"><&|/l&>Privacy</&>:</span>
+<& SelectSearchObject, Name => 'SavedSearchOwner', Objects => \@Objects, Object => ( $Object && $Object->id ) ? $Object->Object : '' &>
+<br />
+<span class="label"><&|/l&>Description</&>:</span>
+<input size="25" name="SavedSearchDescription" value="<% $Description || '' %>" />
+
+% if ($Id ne 'new') {
+<nobr>
+% if ( $Dirty ) {
+<input type="submit" class="button" name="SavedSearchRevert" value="<%loc('Revert')%>" />
+% }
+<input type="submit" class="button" name="SavedSearchDelete" value="<%loc('Delete')%>" />
+% if ( $AllowCopy ) {
+<input type="submit" class="button" name="SavedSearchCopy" value="<%loc('Save as New')%>" />
+% }
+</nobr>
+% }
+% if ( $Object && $Object->Id ) {
+<input type="submit" class="button" id="SavedSearchSave" name="SavedSearchSave" value="<%loc('Update')%>" />
+% } else {
+<input type="submit" class="button" id="SavedSearchSave" name="SavedSearchSave" value="<%loc('Save')%>" />
+%}
+% }
+<br />
+<hr />
+<span class="label"><&|/l&>Load saved search</&>:</span>
+<& SelectSearchesForObjects, Name => 'SavedSearchLoad', Objects => \@Objects, SearchType => $Type &>
+<input type="submit" value="<% loc('Load') %>" id="SavedSearchLoadSubmit" name="SavedSearchLoadSubmit" class="button" />
+
+</&>
+</div>
+<%INIT>
+return unless $session{'CurrentUser'}->HasRight(
+ Right => 'LoadSavedSearch',
+ Object => $RT::System,
+);
+
+my $can_modify = $session{'CurrentUser'}->HasRight(
+ Right => 'CreateSavedSearch',
+ Object => $RT::System,
+);
+
+use RT::SavedSearch;
+my @Objects = RT::SavedSearch->new($session{CurrentUser})->_PrivacyObjects;
+push @Objects, RT::System->new( $session{'CurrentUser'} )
+ if $session{'CurrentUser'}->HasRight( Object=> $RT::System,
+ Right => 'SuperUser' );
+
+my $is_dirty = sub {
+ my %arg = (
+ Query => {},
+ SavedSearch => {},
+ SearchFields => [qw(Query Format OrderBy Order RowsPerPage)],
+ @_
+ );
+
+ my $obj = $arg{'SavedSearch'}->{'Object'};
+ return 0 unless $obj && $obj->id;
+
+ foreach( @{ $arg{'SearchFields'} } ) {
+ return 1 if $obj->SubValue( $_ ) ne $arg{'Query'}->{$_};
+ }
+
+ return 0;
+};
+
+# If we're modifying an old query, check if it's been changed
+my $Dirty = $is_dirty->(
+ Query => $CurrentSearch,
+ SavedSearch => { Id => $Id, Object => $Object, Description => $Description },
+ SearchFields => \@SearchFields,
+);
+
+</%INIT>
+
+<%ARGS>
+$Id => 'new'
+$Object => undef
+$Type => 'Asset'
+$Description => ''
+$CurrentSearch => {}
+ at SearchFields => ()
+$AllowCopy => 1
+$Title => loc('Saved searches')
+</%ARGS>
+
+<%METHOD Init>
+<%ARGS>
+$Query => {}
+$SavedSearch => {}
+ at SearchFields => qw(Query Format OrderBy Order RowsPerPage)
+</%ARGS>
+<%INIT>
+
+$SavedSearch->{'Id'} = $ARGS{'SavedSearchId'} || 'new';
+$SavedSearch->{'Type'} = 'Asset';
+$SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'} || '';
+$SavedSearch->{'Privacy'} = $ARGS{'SavedSearchOwner'} || undef;
+
+my @results;
+
+if ( $ARGS{'SavedSearchRevert'} ) {
+ $ARGS{'SavedSearchLoad'} = $SavedSearch->{'Id'};
+}
+
+if ( $ARGS{'SavedSearchLoad'} ) {
+ my ($container, $id ) = _parse_saved_search ($ARGS{'SavedSearchLoad'});
+ if ( $container ) {
+ my $search = RT::Attribute->new( $session{'CurrentUser'} );
+ $search->Load( $id );
+ $SavedSearch->{'Id'} = $ARGS{'SavedSearchLoad'};
+ $SavedSearch->{'Object'} = $search;
+ $SavedSearch->{'Description'} = $search->Description;
+ $Query->{$_} = $search->SubValue($_) foreach @SearchFields;
+
+ if ( $ARGS{'SavedSearchRevert'} ) {
+ push @results, loc('Loaded original "[_1]" saved search', $SavedSearch->{'Description'} );
+ } else {
+ push @results, loc('Loaded saved search "[_1]"', $SavedSearch->{'Description'} );
+ }
+ }
+ else {
+ push @results, loc( 'Can not load saved search "[_1]"',
+ $ARGS{'SavedSearchLoad'} );
+ return @results;
+ }
+}
+elsif ( $ARGS{'SavedSearchDelete'} ) {
+ # We set $SearchId to 'new' above already, so peek into the %ARGS
+ my ($container, $id) = _parse_saved_search( $SavedSearch->{'Id'} );
+ if ( $container && $container->id ) {
+ # We have the object the entry is an attribute on; delete the entry...
+ my ($val, $msg) = $container->Attributes->DeleteEntry( Name => 'SavedSearch', id => $id );
+ unless ( $val ) {
+ push @results, $msg;
+ return @results;
+ }
+ }
+ $SavedSearch->{'Id'} = 'new';
+ $SavedSearch->{'Object'} = undef;
+ $SavedSearch->{'Description'} = undef;
+ push @results, loc("Deleted saved search");
+}
+elsif ( $ARGS{'SavedSearchCopy'} ) {
+ my ($container, $id ) = _parse_saved_search( $ARGS{'SavedSearchId'} );
+ $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} );
+ $SavedSearch->{'Object'}->Load( $id );
+ if ( $ARGS{'SavedSearchDescription'} && $ARGS{'SavedSearchDescription'} ne $SavedSearch->{'Object'}->Description ) {
+ $SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'};
+ } else {
+ $SavedSearch->{'Description'} = loc( "[_1] copy", $SavedSearch->{'Object'}->Description );
+ }
+ $SavedSearch->{'Id'} = 'new';
+ $SavedSearch->{'Object'} = undef;
+}
+
+if ( $SavedSearch->{'Id'} && $SavedSearch->{'Id'} ne 'new'
+ && !$SavedSearch->{'Object'} )
+{
+ my ($container, $id ) = _parse_saved_search( $ARGS{'SavedSearchId'} );
+ $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} );
+ $SavedSearch->{'Object'}->Load( $id );
+ $SavedSearch->{'Description'} ||= $SavedSearch->{'Object'}->Description;
+}
+
+return @results;
+
+</%INIT>
+</%METHOD>
+
+<%METHOD Save>
+<%ARGS>
+$Query => {}
+$SavedSearch => {}
+ at SearchFields => qw(Query Format OrderBy Order RowsPerPage)
+</%ARGS>
+<%INIT>
+
+return unless $ARGS{'SavedSearchSave'} || $ARGS{'SavedSearchCopy'};
+
+my @results;
+my $obj = $SavedSearch->{'Object'};
+my $id = $SavedSearch->{'Id'};
+my $desc = $SavedSearch->{'Description'};
+my $privacy = $SavedSearch->{'Privacy'};
+
+my %params = map { $_ => $Query->{$_} } @SearchFields;
+my ($new_obj_type, $new_obj_id) = split(/\-/, ($privacy || ''));
+
+if ( $obj && $obj->id ) {
+ # permission check
+ if ($obj->Object->isa('RT::System')) {
+ unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'SuperUser')) {
+ push @results, loc("No permission to save system-wide searches");
+ return @results;
+ }
+ }
+
+ $obj->SetSubValues( %params );
+ $obj->SetDescription( $desc );
+
+ my $obj_type = ref($obj->Object);
+ # We need to get current obj_id now, because when we change obj_type to
+ # RT::System, $obj->Object->Id returns 1, not the old one :(
+ my $obj_id = $obj->Object->Id;
+
+ if ( $new_obj_type && $new_obj_id ) {
+ my ($val, $msg);
+
+ # we need to check right before we change any of ObjectType and ObjectId,
+ # or it will fail the 2nd change if we use SetObjectType and
+ # SetObjectId sequentially
+
+ if ( $obj->CurrentUserHasRight('update') ) {
+ if ( $new_obj_type ne $obj_type ) {
+ ( $val, $msg ) = $obj->__Set(
+ Field => 'ObjectType',
+ Value => $new_obj_type,
+ );
+ push @results, loc( 'Unable to set privacy object: [_1]', $msg )
+ unless ($val);
+ }
+ if ( $new_obj_id != $obj_id ) {
+ ( $val, $msg ) = $obj->__Set(
+ Field => 'ObjectId',
+ Value => $new_obj_id,
+ );
+ push @results, loc( 'Unable to set privacy id: [_1]', $msg )
+ unless ($val);
+ }
+ }
+ else {
+ # two loc are just for convenience so we don't need to
+ # write an extra i18n translation item
+ push @results,
+ loc( 'Unable to set privacy object or id: [_1]',
+ loc('Permission Denied') )
+ }
+ } else {
+ push @results, loc('Unable to determine object type or id');
+ }
+ push @results, loc('Updated saved search "[_1]"', $desc);
+}
+elsif ( $id eq 'new' and defined $desc and length $desc ) {
+ my $saved_search = RT::SavedSearch->new( $session{'CurrentUser'} );
+ my ($status, $msg) = $saved_search->Save(
+ Privacy => $privacy,
+ Name => $desc,
+ Type => $SavedSearch->{'Type'},
+ SearchParams => \%params,
+ );
+
+ if ( $status ) {
+ $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} );
+ $SavedSearch->{'Object'}->Load( $saved_search->Id );
+ # Build new SearchId
+ $SavedSearch->{'Id'} =
+ ref( $session{'CurrentUser'}->UserObj ) . '-'
+ . $session{'CurrentUser'}->UserObj->Id
+ . '-SavedSearch-'
+ . $SavedSearch->{'Object'}->Id;
+ }
+ else {
+ push @results, loc("Can't find a saved search to work with").': '.loc($msg);
+ }
+}
+elsif ( $id eq 'new' ) {
+ push @results, loc("Can't save a search without a Description");
+}
+else {
+ push @results, loc("Can't save this search");
+}
+
+return @results;
+
+</%INIT>
+</%METHOD>
diff --git a/html/Asset/Search/Elements/EditSort b/html/Asset/Search/Elements/EditSort
new file mode 100644
index 0000000..63f370e
--- /dev/null
+++ b/html/Asset/Search/Elements/EditSort
@@ -0,0 +1,140 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<table valign="top">
+
+% for my $o (0..3) {
+% $Order[$o] ||= ''; $OrderBy[$o] ||= '';
+<tr>
+<td class="label">
+% if ($o == 0) {
+<&|/l&>Order by</&>:
+% }
+</td>
+<td class="value">
+<select name="OrderBy">
+% if ($o > 0) {
+<option value=""><&|/l&>~[none~]</&></option>
+% }
+% # %fields maps display name to SQL column/function
+% foreach my $field (sort keys %fields) {
+% next unless $field;
+% my $fieldval = $fields{$field};
+<option value="<%$fieldval%>"
+% if (defined $OrderBy[$o] and $fieldval eq $OrderBy[$o]) {
+selected="selected"
+% }
+><% $field =~ /^(?:CustomField|CF)\./ ? $field : loc($field) %></option>
+% }
+</select>
+<select name="Order">
+<option value="ASC"
+% unless ( ($Order[$o]||'') eq "DESC" ) {
+selected="selected"
+% }
+><&|/l&>Asc</&></option>
+<option value="DESC"
+% if ( ($Order[$o]||'') eq "DESC" ) {
+selected="selected"
+% }
+><&|/l&>Desc</&></option>
+</select>
+</td>
+</tr>
+% }
+<tr>
+<td class="label">
+<&|/l&>Rows per page</&>:
+</td><td class="value">
+<& /Elements/SelectResultsPerPage,
+ Name => "RowsPerPage",
+ Default => $RowsPerPage &>
+</td>
+</tr>
+</table>
+
+<%INIT>
+my $assets = RT::Assets->new($session{'CurrentUser'});
+my %FieldDescriptions = %{$assets->FIELDS};
+my %fields;
+
+for my $field (keys %FieldDescriptions) {
+ next if $field eq 'EffectiveId';
+ next unless $FieldDescriptions{$field}->[0] =~ /^(?:ENUM|INT|DATE|STRING|ID)$/;
+ $fields{$field} = $field;
+}
+
+$fields{'Owner'} = 'Owner';
+$fields{'HeldBy'} = 'HeldBy';
+$fields{'Contact'} = 'Contact';
+
+# Add all available CustomFields to the list of sortable columns.
+my @cfs = grep /^CustomField/, @{$ARGS{AvailableColumns}};
+$fields{$_} = $_ for @cfs;
+
+# Add PAW sort
+$fields{'Custom.Ownership'} = 'Custom.Ownership';
+
+$m->callback(CallbackName => 'MassageSortFields', Fields => \%fields );
+
+my @Order = split /\|/, $Order;
+my @OrderBy = split /\|/, $OrderBy;
+if ($Order =~ /\|/) {
+ @Order = split /\|/, $Order;
+} else {
+ @Order = ( $Order );
+}
+
+</%INIT>
+
+<%ARGS>
+$Order => ''
+$OrderBy => ''
+$RowsPerPage => undef
+$Format => undef
+$GroupBy => 'id'
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/NewListActions
similarity index 76%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/NewListActions
index 48e9df0..43c3daf 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/NewListActions
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,22 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
+<b><%loc('Results')%></b><br />
+% foreach my $action (@actions) {
+% my @item = @$action;
+% if ($item[1] < 0) {
+<font color="red">
+% }
+ <%$item[0]%><br />
+% if ($item[1] < 0) {
+</font>
+% }
+% }
+<br />
<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
+ at actions = map ref $_? $_: [$_, 0], grep defined && length, @actions;
+return unless @actions;
</%init>
+<%ARGS>
+ at actions => undef
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/PickAssetCFs
similarity index 74%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/PickAssetCFs
index 48e9df0..3505a0f 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/PickAssetCFs
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,20 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
+<%ARGS>
+%catalogs => ()
+</%ARGS>
<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
+my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
+foreach my $id (keys %catalogs) {
+ # Gotta load up the $catalog object, since catalogs get stored by name now.
+ my $catalog = RT::Catalog->new($session{'CurrentUser'});
+ $catalog->Load($id);
+ next unless $catalog->Id;
+ $CustomFields->LimitToCatalog($catalog->Id);
+ $CustomFields->SetContextObject( $catalog ) if keys %catalogs == 1;
}
+$CustomFields->LimitToCatalog(0);
+$CustomFields->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
</%init>
+<& /Search/Elements/PickCFs, %ARGS, CustomFields => $CustomFields &>
diff --git a/html/Asset/Search/Elements/PickBasics b/html/Asset/Search/Elements/PickBasics
new file mode 100644
index 0000000..95a07b9
--- /dev/null
+++ b/html/Asset/Search/Elements/PickBasics
@@ -0,0 +1,155 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+% foreach( @lines ) {
+<& ConditionRow, Condition => $_ &>
+% }
+<%INIT>
+
+my @lines = (
+ {
+ Name => 'id',
+ Field => loc('id'),
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectEqualityOperator',
+ },
+ Value => { Type => 'text', Size => 5 }
+ },
+ {
+ Name => 'Attachment',
+ Field => {
+ Type => 'component',
+ Path => '/Asset/Elements/SelectAttachmentField',
+ },
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectBoolean',
+ Arguments => {
+ True => loc("matches"),
+ False => loc("doesn't match"),
+ TrueVal => 'LIKE',
+ FalseVal => 'NOT LIKE',
+ },
+ },
+ Value => { Type => 'text', Size => 20 },
+ },
+ {
+ Name => 'Catalog',
+ Field => loc('Catalog'),
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectBoolean',
+ Arguments => { TrueVal=> '=', FalseVal => '!=' },
+ },
+ Value => {
+ Type => 'component',
+ Path => '/Asset/Elements/SelectCatalog',
+ Arguments => { NamedValues => 1, ShowNullOption => 1, UpdateSession => 0, CheckRight => 'ShowAsset' },
+ },
+ },
+ {
+ Name => 'Status',
+ Field => loc('Status'),
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectBoolean',
+ Arguments => { TrueVal=> '=', FalseVal => '!=' },
+ },
+ Value => {
+ Type => 'component',
+ Path => '/Asset/Elements/SelectStatus',
+ Arguments => { Catalogs => \%catalogs },
+ },
+ },
+ {
+ Name => 'Watcher',
+ Field => {
+ Type => 'component',
+ Path => 'SelectPersonType',
+ Arguments => { Default => 'Owner' },
+ },
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectMatch',
+ },
+ Value => { Type => 'text', Size => 20 }
+ },
+ {
+ Name => 'Date',
+ Field => {
+ Type => 'component',
+ Path => '/Asset/Elements/SelectDateType',
+ },
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectDateRelation',
+ },
+ Value => {
+ Type => 'component',
+ Path => '/Elements/SelectDate',
+ Arguments => { ShowTime => 0, Default => '' },
+ },
+ },
+ {
+ Name => 'Links',
+ Field => { Type => 'component', Path => 'SelectLinks' },
+ Op => {
+ Type => 'component',
+ Path => '/Elements/SelectBoolean',
+ Arguments => { TrueVal=> '=', FalseVal => '!=' },
+ },
+ Value => { Type => 'text', Size => 5 }
+ },
+);
+
+$m->callback( Conditions => \@lines );
+
+</%INIT>
+<%ARGS>
+%catalogs => ()
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/PickCriteria
similarity index 75%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/PickCriteria
index 48e9df0..d79ec82 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/PickCriteria
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,29 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<&| /Widgets/TitleBox, title => loc('Add Criteria')&>
+
+<table width="100%" cellspacing="0" cellpadding="0" border="0">
+
+
+% $m->callback( %ARGS, CallbackName => "BeforeBasics" );
+<& PickBasics, catalogs => \%catalogs &>
+<& PickAssetCFs, catalogs => \%catalogs &>
+% $m->callback( %ARGS, CallbackName => "AfterCFs" );
+
+<tr class="separator"><td colspan="3"><hr /></td></tr>
+<tr>
+<td class="label"><&|/l&>Aggregator</&></td>
+<td class="operator" colspan="2"><& SelectAndOr, Name => "AndOr" &></td>
+
+</tr>
+
+</table>
+
+</&>
+
+<%ARGS>
+$addquery => 0
+$query => undef
+%catalogs => ()
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/SearchPrivacy
similarity index 77%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/SearchPrivacy
index 48e9df0..1e43dfd 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/SearchPrivacy
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,17 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
+$Object => undef
</%args>
<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
+my $label;
+if (ref($Object) eq 'RT::User') {
+ $label = $Object->id == $session{'CurrentUser'}->Id
+ ? loc("My saved searches")
+ : loc("[_1]'s saved searches", $Object->Format);
+} else {
+ $label = loc("[_1]'s saved searches", $Object->Name);
}
</%init>
+<% $label %>\
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/SelectAndOr
similarity index 75%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/SelectAndOr
index 48e9df0..d506ef7 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/SelectAndOr
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,9 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<label><input type="radio" class="radio" name="<%$Name%>" checked="checked" value="AND" /><&|/l&>AND</&></label>
+<label><input type="radio" class="radio" name="<%$Name%>" value="OR" /><&|/l&>OR</&></label>
+
+<%ARGS>
+$Name => "Operator"
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/SelectLinks
similarity index 75%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/SelectLinks
index 48e9df0..1b85094 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/SelectLinks
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,23 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<select name="<%$Name%>">
+% foreach (@fields) {
+<option value="<%$_->[0]%>"><% $_->[1] %></option>
+% }
+</select>
+<%ARGS>
+$Name => 'LinksField'
+</%ARGS>
+
+<%INIT>
+my @fields = (
+ [ HasMember => loc("Child") ],
+ [ MemberOf => loc("Parent") ],
+ [ DependsOn => loc("Depends on") ],
+ [ DependedOnBy => loc("Depended on by") ],
+ [ RefersTo => loc("Refers to") ],
+ [ ReferredToBy => loc("Referred to by") ],
+ [ LinkedTo => loc("Links to") ],
+);
+</%INIT>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/SelectPersonType
similarity index 70%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/SelectPersonType
index 48e9df0..5e31c02 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/SelectPersonType
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,30 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
-<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
-</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<select id="<%$Name%>" name="<%$Name%>">
+% if ($AllowNull) {
+<option value="">-</option>
+% }
+% for my $option (@types) {
+% if ($Suffix) {
+<option value="<% $option %><% $Suffix %>"<%$option eq $Default && qq[ selected="selected"] |n %> ><% loc($option) %> <% loc('Group') %></option>
+% next;
+% }
+% foreach my $subtype (@subtypes) {
+<option value="<%"$option.$subtype"%>"<%$option eq $Default && $subtype eq 'EmailAddress' && qq[ selected="selected"] |n %> ><% loc($option) %> <% loc($subtype) %></option>
+% }
+% }
+</select>
+
+<%INIT>
+my @types = qw(Owner HeldBy Contact);
+my @subtypes = @{ $RT::Assets::SEARCHABLE_SUBFIELDS{'User'} };
+
+</%INIT>
+<%ARGS>
+$AllowNull => 1
+$Suffix => ''
+$Default=>undef
+$Scope => 'asset'
+$Name => 'WatcherType'
+</%ARGS>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/SelectSearchObject
similarity index 77%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/SelectSearchObject
index 48e9df0..a6af1fa 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/SelectSearchObject
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,20 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
+ at Objects => undef
+$Name => undef
+$Object => undef
</%args>
<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
+my $default_privacy = '';
+if ( $Object && $Object->Id ) {
+ $default_privacy = ref($Object).'-'.$Object->Id;
}
</%init>
+<select id="<%$Name%>" name="<%$Name%>">
+% foreach my $object (@Objects) {
+% my $privacy = ref($object).'-'.$object->id;
+<option value="<%$privacy%>" <% ( $privacy eq $default_privacy ) ? "selected='selected'" : '' |n %>><& SearchPrivacy, Object => $object &></option>
+% }
+</select>
diff --git a/html/Asset/Elements/SelectCatalog b/html/Asset/Search/Elements/SelectSearchesForObjects
similarity index 73%
copy from html/Asset/Elements/SelectCatalog
copy to html/Asset/Search/Elements/SelectSearchesForObjects
index 48e9df0..0e47a4f 100644
--- a/html/Asset/Elements/SelectCatalog
+++ b/html/Asset/Search/Elements/SelectSearchesForObjects
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,25 +45,24 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<& /Elements/SelectObject,
- Name => "Catalog",
- ShowAll => $ShowAll,
- ShowNullOption => 0,
- CheckRight => "CreateAsset",
- %ARGS,
- ObjectType => "Catalog",
- CacheNeedsUpdate => RT::Catalog->CacheNeedsUpdate,
- Default => $Default,
- &>
<%args>
-$ShowAll => 0
-$Default => undef
-$UpdateSession => 1
+ at Objects => undef
+$Name => undef
+$SearchType => 'Asset',
</%args>
-<%init>
-my $catalog_obj = LoadDefaultCatalog($Default || '');
-if ( $UpdateSession && $catalog_obj->Id ){
- $session{'DefaultCatalog'} = $catalog_obj->Id;
- $Default = $catalog_obj->Id;
-}
-</%init>
+<select id="<%$Name%>" name="<%$Name%>">
+<option value="">-</option>
+% foreach my $object (@Objects) {
+% my @searches = $object->Attributes->Named('SavedSearch');
+% if ( @searches ) {
+<optgroup label="<& SearchPrivacy, Object => $object &>">
+% foreach my $search (@searches) {
+% # Skip it if it is not of search type we want.
+% next if ($search->SubValue('SearchType')
+% && $search->SubValue('SearchType') ne $SearchType);
+<option value="<%ref($object)%>-<%$object->id%>-SavedSearch-<%$search->Id%>"><%$search->Description||loc('Unnamed search')%></option>
+% }
+</optgroup>
+% }
+% }
+</select>
diff --git a/html/Asset/Search/Results.html b/html/Asset/Search/Results.html
new file mode 100644
index 0000000..4a8eb74
--- /dev/null
+++ b/html/Asset/Search/Results.html
@@ -0,0 +1,220 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+%# <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => $title,
+ Refresh => $refresh,
+ LinkRel => \%link_rel &>
+<& /Elements/Tabs &>
+
+% my $DisplayFormat;
+% $m->callback( ARGSRef => \%ARGS, Format => \$Format, DisplayFormat => \$DisplayFormat, CallbackName => 'BeforeResults' );
+
+% unless ($ok) {
+% $msg =~ s{ at .*? line .*}{}s;
+<&| /Widgets/TitleBox, title => loc("Error"), class => "error-titlebox" &>
+<&|/l_unsafe, "<i>".$m->interp->apply_escapes($msg, "h")."</i>" &>There was an error parsing your search query: [_1]. Your RT admin can find more information in the error logs.</&>
+</&>
+% } else {
+% my $Collection = RT::Assets->new($session{CurrentUser});
+% $Collection->FromSQL($Query);
+<& /Elements/CollectionList,
+ Collection => $Collection,
+ Query => $Query,
+ TotalFound => $assetcount,
+ AllowSorting => 1,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ Rows => $Rows,
+ Page => $Page,
+ Format => $Format,
+ DisplayFormat => $DisplayFormat, # in case we set it in callbacks
+ Class => 'RT::Assets',
+ BaseURL => $BaseURL,
+ SavedSearchId => $ARGS{'SavedSearchId'},
+ SavedChartSearchId => $ARGS{'SavedChartSearchId'},
+ PassArguments => [qw(Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId)],
+&>
+% }
+% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' );
+
+% my %hiddens = (Query => $Query, Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page );
+<div align="right" class="refresh">
+<form method="get" action="<%RT->Config->Get('WebPath')%>/Search/Results.html">
+% foreach my $key (keys(%hiddens)) {
+<input type="hidden" class="hidden" name="<%$key%>" value="<% defined($hiddens{$key})?$hiddens{$key}:'' %>" />
+% }
+<& /Elements/Refresh, Name => 'AssetsRefreshInterval', Default => $session{'assets_refresh_interval'}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &>
+<input type="submit" class="button" value="<&|/l&>Change</&>" />
+</form>
+</div>
+<%INIT>
+$m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' );
+
+# Read from user preferences
+# TODO AssetSearchDisplay is not defined yet
+my $prefs = $session{'CurrentUser'}->UserObj->Preferences("AssetSearchDisplay") || {};
+
+# These variables are what define a search_hash; this is also
+# where we give sane defaults.
+$Format ||= $prefs->{'Format'} || RT->Config->Get('AssetSearchFormat');
+$Order ||= $prefs->{'Order'} || RT->Config->Get('AssetSearchOrder'); # TODO
+$OrderBy ||= $prefs->{'OrderBy'} || RT->Config->Get('AssetSearchResultOrderBy'); # TODO
+
+# Some forms pass in "RowsPerPage" rather than "Rows"
+# We call it RowsPerPage everywhere else.
+
+if ( !defined($Rows) ) {
+ if (defined $ARGS{'RowsPerPage'} ) {
+ $Rows = $ARGS{'RowsPerPage'};
+ } elsif ( defined $prefs->{'RowsPerPage'} ) {
+ $Rows = $prefs->{'RowsPerPage'};
+ } else {
+ $Rows = 50;
+ }
+}
+$Page = 1 unless $Page && $Page > 0;
+
+$session{'i'}++;
+$session{'assets'} = RT::Assets->new($session{'CurrentUser'}) ;
+my ($ok, $msg) = $Query ? $session{'assets'}->FromSQL($Query) : (1, "Vacuously OK");
+# Provide an empty search if parsing failed
+$session{'assets'}->FromSQL("id < 0") unless ($ok);
+
+if ($OrderBy =~ /\|/) {
+ # Multiple Sorts
+ my @OrderBy = split /\|/,$OrderBy;
+ my @Order = split /\|/,$Order;
+ $session{'assets'}->OrderByCols(
+ map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } } ( 0
+ .. $#OrderBy ) );;
+} else {
+ $session{'assets'}->OrderBy(FIELD => $OrderBy, ORDER => $Order);
+}
+$session{'assets'}->RowsPerPage( $Rows ) if $Rows;
+$session{'assets'}->GotoPage( $Page - 1 );
+
+$session{'CurrentAssetSearchHash'} = {
+ Format => $Format,
+ Query => $Query,
+ Page => $Page,
+ Order => $Order,
+ OrderBy => $OrderBy,
+ RowsPerPage => $Rows
+};
+
+
+my ($title, $assetcount) = (loc("Find assets"), 0);
+if ( $session{'assets'}->Query()) {
+ $assetcount = $session{assets}->CountAll();
+ $title = loc('Found [quant,_1,asset,assets]', $assetcount);
+}
+
+my $QueryString = "?".$m->comp('/Elements/QueryString',
+ Query => $Query,
+ Format => $Format,
+ Rows => $Rows,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ Page => $Page);
+my $ShortQueryString = "?".$m->comp('/Elements/QueryString', Query => $Query);
+
+if ($ARGS{'AssetsRefreshInterval'}) {
+ $session{'assets_refresh_interval'} = $ARGS{'AssetsRefreshInterval'};
+}
+
+my $refresh = $session{'assets_refresh_interval'}
+ || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
+
+# Check $m->request_args, not $DECODED_ARGS, to avoid creating a new CSRF token on each refresh
+if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
+ my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentAssetSearchHash'} );
+ $m->notes->{RefreshURL} = RT->Config->Get('WebURL')
+ . "Search/Results.html?CSRF_Token="
+ . $token;
+}
+
+my %link_rel;
+my $genpage = sub {
+ return $m->comp(
+ '/Elements/QueryString',
+ Query => $Query,
+ Format => $Format,
+ Rows => $Rows,
+ OrderBy => $OrderBy,
+ Order => $Order,
+ Page => shift(@_),
+ );
+};
+
+if ( RT->Config->Get('SearchResultsAutoRedirect') && $assetcount == 1 &&
+ $session{assets}->First ) {
+# $assetcount is not always precise unless $UseSQLForACLChecks is set to true,
+# check $session{assets}->First here is to make sure the asset is there.
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL')
+ ."Asset/Display.html?id=". $session{assets}->First->id );
+}
+
+my $BaseURL = RT->Config->Get('WebPath')."/Search/Results.html?";
+$link_rel{first} = $BaseURL . $genpage->(1) if $Page > 1;
+$link_rel{prev} = $BaseURL . $genpage->($Page - 1) if $Page > 1;
+$link_rel{next} = $BaseURL . $genpage->($Page + 1) if ($Page * $Rows) < $assetcount;
+$link_rel{last} = $BaseURL . $genpage->(POSIX::ceil($assetcount/$Rows)) if $Rows and ($Page * $Rows) < $assetcount;
+</%INIT>
+<%CLEANUP>
+$session{'assets'}->PrepForSerialization();
+</%CLEANUP>
+<%ARGS>
+$Query => undef
+$Format => undef
+$HideResults => 0
+$Rows => undef
+$Page => 1
+$OrderBy => undef
+$Order => undef
+$SavedSearchId => undef
+$SavedChartSearchId => undef
+</%ARGS>
diff --git a/html/Asset/Search/Results.tsv b/html/Asset/Search/Results.tsv
index 41248b6..03820f1 100644
--- a/html/Asset/Search/Results.tsv
+++ b/html/Asset/Search/Results.tsv
@@ -2,7 +2,7 @@
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
%# <sales at bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
@@ -45,29 +45,53 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<%init>
+<%ARGS>
+$Format => undef
+$Query => ''
+$OrderBy => 'id'
+$Order => 'ASC'
+$PreserveNewLines => 0
+</%ARGS>
+<%INIT>
+my $Assets = RT::Assets->new( $session{'CurrentUser'} );
+
my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
$ARGS{'Catalog'} = $catalog_obj->Id;
-my $assets = RT::Assets->new($session{CurrentUser});
-ProcessAssetsSearchArguments(
- Assets => $assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
-);
-
-my $Format = q|id, Name, Description, Status, Catalog, |;
-
-$Format .= "$_, " for RT::Asset->Roles;
+if ( $ARGS{Query} ) {
+ $Assets->FromSQL( $Query );
+}
+else {
+ ProcessAssetsSearchArguments(
+ Assets => $Assets, Catalog => $catalog_obj, ARGSRef => \%ARGS,
+ );
+}
+if ( $OrderBy =~ /\|/ ) {
+ # Multiple Sorts
+ my @OrderBy = split /\|/, $OrderBy;
+ my @Order = split /\|/, $Order;
+ $Assets->OrderByCols(
+ map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } }
+ ( 0 .. $#OrderBy )
+ );
+}
+else {
+ $Assets->OrderBy( FIELD => $OrderBy, ORDER => $Order );
+}
-my $CFs = RT::CustomFields->new( $session{CurrentUser} );
-$CFs->LimitToCatalog( $catalog_obj->Id );
-$CFs->LimitToObjectId( 0 ); # LimitToGlobal but no LookupType restriction
-$Format .= "'__CF.{$_}__/TITLE:$_', " for map {$_ = $_->Name; s/['\\]/\\$1/g; $_} @{$CFs->ItemsArrayRef};
-
-$m->callback(CallbackName => "ModifyFormat", Format => \$Format );
+if ( !$Format ) {
+ $Format = q|id, Name, Description, Status, Catalog, |;
+ $Format .= "$_, " for RT::Asset->Roles;
+ my $catalog_obj = LoadDefaultCatalog($ARGS{'Catalog'} || '');
+ my $CFs = RT::CustomFields->new( $session{CurrentUser} );
+ $CFs->LimitToCatalog( $catalog_obj->Id );
+ $CFs->LimitToObjectId( 0 ); # LimitToGlobal but no LookupType restriction
+ $Format .= "'__CF.{$_}__/TITLE:$_', " for map {$_ = $_->Name; s/['\\]/\\$1/g; $_} @{$CFs->ItemsArrayRef};
+ $m->callback(CallbackName => "ModifyFormat", Format => \$Format );
+}
my $comp = "/Asset/Elements/TSVExport";
$comp = "/Elements/TSVExport" if $m->comp_exists("/Elements/TSVExport");
+$m->comp( $comp, Collection => $Assets, Format => $Format );
-$m->comp($comp, Collection => $assets, Format => $Format );
-
-</%init>
+</%INIT>
diff --git a/html/Callbacks/RT-Extension-Assets/Elements/Tabs/Privileged b/html/Callbacks/RT-Extension-Assets/Elements/Tabs/Privileged
index cc0c912..3ff082c 100644
--- a/html/Callbacks/RT-Extension-Assets/Elements/Tabs/Privileged
+++ b/html/Callbacks/RT-Extension-Assets/Elements/Tabs/Privileged
@@ -61,7 +61,8 @@ my $query_string = sub {
my $assets = Menu->child("tools")->add_before(
"assets", title => loc("Assets"), path => "/Asset/Search/");
$assets->child("create", title => loc("Create"), path => "/Asset/CreateInCatalog.html");
-$assets->child("search", title => loc("Search"), path => "/Asset/Search/");
+$assets->child("search", title => loc("Query Builder"), path => "/Asset/Search/Build.html");
+$assets->child("new_search", title => loc("New Search"), path => "/Asset/Search/Build.html?NewQuery=1");
Menu->child("search")->child("assets", title => loc("Assets"), path => "/Asset/Search/");
@@ -153,7 +154,7 @@ if ($Path =~ m{^/Asset/} and $DECODED_ARGS->{id} and $DECODED_ARGS->{id} !~ /\D/
}
}
}
-elsif ($Path =~ m{^/Asset/Search/}) {
+elsif ($Path =~ m{^/Asset/Search/(index.html)?$}) {
my %search = map @{$_},
grep defined $_->[1] && length $_->[1],
map {ref $DECODED_ARGS->{$_} ? [$_, $DECODED_ARGS->{$_}[0]] : [$_, $DECODED_ARGS->{$_}] }
@@ -175,6 +176,81 @@ elsif ($Path =~ m{^/Asset/Search/}) {
path => '/Asset/Search/Results.tsv?' . $query_string->(%search),
);
}
+elsif ($Path =~ m{^/Asset/Search/}) {
+ my %search = map @{$_},
+ grep defined $_->[1] && length $_->[1],
+ map {ref $DECODED_ARGS->{$_} ? [$_, $DECODED_ARGS->{$_}[0]] : [$_, $DECODED_ARGS->{$_}] }
+ grep /^(?:q|SearchAssets|!?(Name|Description|Catalog|Status|Role\..+|CF\..+)|Order(?:By)?|Page)$/,
+ keys %$DECODED_ARGS;
+
+ my $current_search = $session{"CurrentAssetSearchHash"} || {};
+ my $search_id = $DECODED_ARGS->{'SavedSearchLoad'} || $DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
+ my $args = '';
+ my $has_query;
+ $has_query = 1 if ( $DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
+
+ my %query_args;
+ my %fallback_query_args = (
+ SavedSearchId => ( $search_id eq 'new' ) ? undef : $search_id,
+ (
+ map {
+ my $p = $_;
+ $p => $DECODED_ARGS->{$p} || $current_search->{$p}
+ } qw(Query Format OrderBy Order Page)
+ ),
+ RowsPerPage => (
+ defined $DECODED_ARGS->{'RowsPerPage'}
+ ? $DECODED_ARGS->{'RowsPerPage'}
+ : $current_search->{'RowsPerPage'}
+ ),
+ );
+
+ my $QueryString = $ARGSRef->{QueryString};
+ my $QueryArgs = $ARGSRef->{QueryArgs};
+ if ($QueryString) {
+ $args = '?' . $QueryString;
+ }
+ else {
+ my %final_query_args = ();
+ # key => callback to avoid unnecessary work
+
+ for my $param (keys %fallback_query_args) {
+ $final_query_args{$param} = defined($QueryArgs->{$param})
+ ? $QueryArgs->{$param}
+ : $fallback_query_args{$param};
+ }
+
+ for my $field (qw(Order OrderBy)) {
+ if ( ref( $final_query_args{$field} ) eq 'ARRAY' ) {
+ $final_query_args{$field} = join( "|", @{ $final_query_args{$field} } );
+ } elsif (not defined $final_query_args{$field}) {
+ delete $final_query_args{$field};
+ }
+ else {
+ $final_query_args{$field} ||= '';
+ }
+ }
+
+ $args = '?' . $query_string->(%final_query_args);
+ }
+
+ $page->child('edit_search',
+ title => loc('Edit Search'),
+ path => '/Asset/Search/Build.html' . $args,
+ );
+ $page->child( advanced => title => loc('Advanced'), path => '/Asset/Search/Edit.html' . $args );
+ if ($has_query) {
+ $page->child( results => title => loc('Show Results'), path => '/Asset/Search/Results.html' . $args );
+ $page->child('bulk',
+ title => loc('Bulk Update'),
+ path => '/Asset/Search/Bulk.html' . $args,
+ );
+ $page->child('csv',
+ title => loc('Download Spreadsheet'),
+ path => '/Asset/Search/Results.tsv' . $args,
+ );
+ }
+}
elsif ($Path =~ m{^/Admin/Global/CustomFields/Catalog-Assets\.html$}) {
$page->child("create", title => loc("Create New"), path => "/Admin/CustomFields/Modify.html?Create=1;LookupType=" . RT::Asset->CustomFieldLookupType);
}
@@ -205,3 +281,7 @@ elsif ($Path =~ m{^/Admin/Assets/Catalogs/}) {
}
}
</%init>
+
+<%args>
+$ARGSRef => {}
+</%args>
diff --git a/lib/RT/Assets.pm b/lib/RT/Assets.pm
index 787b5f3..f7959ff 100644
--- a/lib/RT/Assets.pm
+++ b/lib/RT/Assets.pm
@@ -97,21 +97,6 @@ sub LimitCatalog {
$self->SUPER::Limit(%args);
}
-=head2 Limit
-
-Defaults CASESENSITIVE to 0
-
-=cut
-
-sub Limit {
- my $self = shift;
- my %args = (
- CASESENSITIVE => 0,
- @_
- );
- $self->SUPER::Limit(%args);
-}
-
=head2 RoleLimit
Re-uses the underlying JOIN, if possible.
@@ -175,9 +160,370 @@ Sets default ordering by Name ascending.
sub _Init {
my $self = shift;
-
+ $self->{'table'} = "RTxAssets";
+ $self->{'RecalcAssetLimits'} = 1;
+ $self->{'restriction_index'} = 1;
+ $self->{'primary_key'} = "id";
+ delete $self->{'items_array'};
+ delete $self->{'item_map'};
+ delete $self->{'columns_to_display'};
$self->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
- return $self->SUPER::_Init( @_ );
+
+ $self->SUPER::_Init(@_);
+
+ $self->_InitSQL();
+}
+
+sub _InitSQL {
+ my $self = shift;
+ # Private Member Variables (which should get cleaned)
+ $self->{'_sql_cf_alias'} = undef;
+ $self->{'_sql_object_cfv_alias'} = undef;
+ $self->{'_sql_watcher_join_users_alias'} = undef;
+ $self->{'_sql_query'} = '';
+ $self->{'_sql_looking_at'} = {};
+}
+
+sub Count {
+ my $self = shift;
+ $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );
+ return ( $self->SUPER::Count() );
+}
+
+sub CountAll {
+ my $self = shift;
+ $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );
+ return ( $self->SUPER::CountAll() );
+}
+
+=head2 ItemsArrayRef
+
+Returns a reference to the set of all items found in this search
+
+=cut
+
+sub ItemsArrayRef {
+ my $self = shift;
+
+ return $self->{'items_array'} if $self->{'items_array'};
+
+ my $placeholder = $self->_ItemsCounter;
+ $self->GotoFirstItem();
+ while ( my $item = $self->Next ) {
+ push( @{ $self->{'items_array'} }, $item );
+ }
+ $self->GotoItem($placeholder);
+ $self->{'items_array'} ||= [];
+ $self->{'items_array'}
+ = $self->ItemsOrderBy( $self->{'items_array'} );
+
+ return $self->{'items_array'};
+}
+
+sub ItemsArrayRefWindow {
+ my $self = shift;
+ my $window = shift;
+
+ my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
+
+ $self->RowsPerPage( $window );
+ $self->FirstRow(1);
+ $self->GotoFirstItem;
+
+ my @res;
+ while ( my $item = $self->Next ) {
+ push @res, $item;
+ }
+
+ $self->RowsPerPage( $old[1] );
+ $self->FirstRow( $old[2] );
+ $self->GotoItem( $old[0] );
+
+ return \@res;
+}
+
+
+sub Next {
+ my $self = shift;
+
+ $self->_ProcessRestrictions() if ( $self->{'RecalcAssetLimits'} == 1 );
+
+ my $Asset = $self->SUPER::Next;
+ return $Asset unless $Asset;
+
+ if ( $Asset->__Value('Status') eq 'deleted'
+ && !$self->{'allow_deleted_search'} )
+ {
+ return $self->Next;
+ }
+ elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
+ # if we found an asset with this option enabled then
+ # all assets we found are ACLed, cache this fact
+ my $key = join ";:;", $self->CurrentUser->id, 'ShowAsset', 'RT::Asset-'. $Asset->id;
+ $RT::Principal::_ACL_CACHE->{ $key } = 1;
+ return $Asset;
+ }
+ elsif ( $Asset->CurrentUserHasRight('ShowAsset') ) {
+ # has rights
+ return $Asset;
+ }
+ else {
+ # If the user doesn't have the right to show this asset
+ return $self->Next;
+ }
+}
+
+sub _DoSearch {
+ my $self = shift;
+ $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
+ unless $self->{ 'allow_deleted_search' };
+ $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+ return $self->SUPER::_DoSearch( @_ );
+}
+
+sub _DoCount {
+ my $self = shift;
+ $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
+ unless $self->{ 'allow_deleted_search' };
+ $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+ return $self->SUPER::_DoCount( @_ );
+}
+
+sub _RolesCanSee {
+ my $self = shift;
+
+ my $cache_key = 'RolesHasRight;:;ShowAsset';
+
+ if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
+ return %$cached;
+ }
+
+ my $ACL = RT::ACL->new( RT->SystemUser );
+ $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowAsset' );
+ $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
+ my $principal_alias = $ACL->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ TABLE2 => 'Principals',
+ FIELD2 => 'id',
+ );
+ $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+
+ my %res = ();
+ foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+ my $role = $ACE->__Value('PrincipalType');
+ my $type = $ACE->__Value('ObjectType');
+ if ( $type eq 'RT::System' ) {
+ $res{ $role } = 1;
+ }
+ elsif ( $type eq 'RT::Catalog' ) {
+ next if $res{ $role } && !ref $res{ $role };
+ push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
+ }
+ else {
+ $RT::Logger->error('ShowAsset right is granted on unsupported object');
+ }
+ }
+ $RT::Principal::_ACL_CACHE->{ $cache_key } = \%res;
+ return %res;
+}
+
+sub _DirectlyCanSeeIn {
+ my $self = shift;
+ my $id = $self->CurrentUser->id;
+
+ my $cache_key = 'User-'. $id .';:;ShowAsset;:;DirectlyCanSeeIn';
+ if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
+ return @$cached;
+ }
+
+ my $ACL = RT::ACL->new( RT->SystemUser );
+ $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowAsset' );
+ my $principal_alias = $ACL->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ TABLE2 => 'Principals',
+ FIELD2 => 'id',
+ );
+ $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+ my $cgm_alias = $ACL->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ );
+ $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+ $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+
+ my @res = ();
+ foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
+ my $type = $ACE->__Value('ObjectType');
+ if ( $type eq 'RT::System' ) {
+ # If user is direct member of a group that has the right
+ # on the system then he can see any asset
+ $RT::Principal::_ACL_CACHE->{ $cache_key } = [-1];
+ return (-1);
+ }
+ elsif ( $type eq 'RT::Catalog' ) {
+ push @res, $ACE->__Value('ObjectId');
+ }
+ else {
+ $RT::Logger->error('ShowAsset right is granted on unsupported object');
+ }
+ }
+ $RT::Principal::_ACL_CACHE->{ $cache_key } = \@res;
+ return @res;
+}
+
+sub CurrentUserCanSee {
+ my $self = shift;
+ return if $self->{'_sql_current_user_can_see_applied'};
+
+ return $self->{'_sql_current_user_can_see_applied'} = 1
+ if $self->CurrentUser->UserObj->HasRight(
+ Right => 'SuperUser', Object => $RT::System
+ );
+
+ local $self->{using_restrictions};
+
+ my $id = $self->CurrentUser->id;
+
+ # directly can see in all catalogs then we have nothing to do
+ my @direct_catalogs = $self->_DirectlyCanSeeIn;
+ return $self->{'_sql_current_user_can_see_applied'} = 1
+ if @direct_catalogs && $direct_catalogs[0] == -1;
+
+ my %roles = $self->_RolesCanSee;
+ {
+ my %skip = map { $_ => 1 } @direct_catalogs;
+ foreach my $role ( keys %roles ) {
+ next unless ref $roles{ $role };
+
+ my @catalogs = grep !$skip{$_}, @{ $roles{ $role } };
+ if ( @catalogs ) {
+ $roles{ $role } = \@catalogs;
+ } else {
+ delete $roles{ $role };
+ }
+ }
+ }
+
+# there is no global watchers, only catalogs and tickes, if at
+# some point we will add global roles then it's gonna blow
+# the idea here is that if the right is set globaly for a role
+# and user plays this role for a catalog directly not a ticket
+# then we have to check in advance
+ if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
+
+ my $groups = RT::Groups->new( RT->SystemUser );
+ $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Catalog-Role', CASESENSITIVE => 0 );
+ $groups->Limit(
+ FIELD => 'Name',
+ FUNCTION => 'LOWER(?)',
+ OPERATOR => 'IN',
+ VALUE => [ map {lc $_} @tmp ],
+ CASESENSITIVE => 1,
+ );
+ my $principal_alias = $groups->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Principals',
+ FIELD2 => 'id',
+ );
+ $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
+ my $cgm_alias = $groups->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ );
+ $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
+ $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
+ while ( my $group = $groups->Next ) {
+ push @direct_catalogs, $group->Instance;
+ }
+ }
+
+ unless ( @direct_catalogs || keys %roles ) {
+ $self->Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => 'main',
+ FIELD => 'id',
+ VALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+ );
+ return $self->{'_sql_current_user_can_see_applied'} = 1;
+ }
+
+ {
+ my $join_roles = keys %roles;
+ $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
+ my ($role_group_alias, $cgm_alias);
+ if ( $join_roles ) {
+ $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
+ $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
+ $self->Limit(
+ LEFTJOIN => $cgm_alias,
+ FIELD => 'MemberId',
+ OPERATOR => '=',
+ VALUE => $id,
+ );
+ }
+ my $limit_catalogs = sub {
+ my $ea = shift;
+ my @catalogs = @_;
+
+ return unless @catalogs;
+ $self->Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => 'main',
+ FIELD => 'Catalog',
+ OPERATOR => 'IN',
+ VALUE => [ @catalogs ],
+ ENTRYAGGREGATOR => $ea,
+ );
+ return 1;
+ };
+
+ $self->SUPER::_OpenParen('ACL');
+ my $ea = 'AND';
+ $ea = 'OR' if $limit_catalogs->( $ea, @direct_catalogs );
+ while ( my ($role, $catalogs) = each %roles ) {
+ $self->SUPER::_OpenParen('ACL');
+ if ( $role eq 'Owner' ) {
+ $self->Limit(
+ SUBCLAUSE => 'ACL',
+ FIELD => 'Owner',
+ VALUE => $id,
+ ENTRYAGGREGATOR => $ea,
+ );
+ }
+ else {
+ $self->Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => $cgm_alias,
+ FIELD => 'MemberId',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => $ea,
+ );
+ $self->Limit(
+ SUBCLAUSE => 'ACL',
+ ALIAS => $role_group_alias,
+ FIELD => 'Name',
+ VALUE => $role,
+ ENTRYAGGREGATOR => 'AND',
+ CASESENSITIVE => 0,
+ );
+ }
+ $limit_catalogs->( 'AND', @$catalogs ) if ref $catalogs;
+ $ea = 'OR' if $ea eq 'AND';
+ $self->SUPER::_CloseParen('ACL');
+ }
+ $self->SUPER::_CloseParen('ACL');
+ }
+ return $self->{'_sql_current_user_can_see_applied'} = 1;
}
sub SimpleSearch {
@@ -282,30 +628,1154 @@ sub OrderByCols {
return $self->SUPER::OrderByCols( @res );
}
-=head2 _DoSearch
+sub _OpenParen {
+ $_[0]->SUPER::_OpenParen( $_[1] || 'assetsql' );
+}
+sub _CloseParen {
+ $_[0]->SUPER::_CloseParen( $_[1] || 'assetsql' );
+}
-=head2 _DoCount
+=head2 Limit
-Limits to non-deleted assets unless the C<allow_deleted_search> flag is set.
+Defaults CASESENSITIVE to 0
=cut
-sub _DoSearch {
+sub Limit {
my $self = shift;
- $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
- unless $self->{'allow_deleted_search'};
- $self->SUPER::_DoSearch(@_);
+ my %args = (
+ CASESENSITIVE => 0,
+ @_
+ );
+ $self->{'must_redo_search'} = 1;
+ delete $self->{'raw_rows'};
+ delete $self->{'count_all'};
+
+ if ($self->{'using_restrictions'}) {
+ RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
+ $self->LimitField(@_);
+ }
+
+ $args{SUBCLAUSE} ||= "assetsql"
+ if $self->{parsing_assetsql} and not $args{LEFTJOIN};
+
+ $self->{_sql_looking_at}{ lc $args{FIELD} } = 1
+ if $args{FIELD} and (not $args{ALIAS} or $args{ALIAS} eq "main");
+
+ $self->SUPER::Limit(%args);
}
-sub _DoCount {
+
+=head2 LimitField
+
+Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
+Generally best called from LimitFoo methods
+
+=cut
+
+sub LimitField {
my $self = shift;
- $self->Limit( FIELD => 'Status', OPERATOR => '!=', VALUE => 'deleted', SUBCLAUSE => "not_deleted" )
- unless $self->{'allow_deleted_search'};
- $self->SUPER::_DoCount(@_);
+ my %args = (
+ FIELD => undef,
+ OPERATOR => '=',
+ VALUE => undef,
+ DESCRIPTION => undef,
+ @_
+ );
+ $args{'DESCRIPTION'} = $self->loc(
+ "[_1] [_2] [_3]", $args{'FIELD'},
+ $args{'OPERATOR'}, $args{'VALUE'}
+ )
+ if ( !defined $args{'DESCRIPTION'} );
+
+
+ if ($self->_isLimited > 1) {
+ RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
+ }
+ $self->{using_restrictions} = 1;
+
+ my $index = $self->_NextIndex;
+
+# make the TicketRestrictions hash the equivalent of whatever we just passed in;
+
+ %{ $self->{'TicketRestrictions'}{$index} } = %args;
+
+ $self->{'RecalcTicketLimits'} = 1;
+
+ return ($index);
}
sub Table { "RTxAssets" }
+# Configuration Tables:
+
+# FIELD_METADATA is a mapping of searchable Field name, to Type, and other
+# metadata.
+
+our %FIELD_METADATA = (
+ id => [ 'ID', ], #loc_left_pair
+ Name => [ 'STRING', ], #loc_left_pair
+ Description => [ 'STRING', ], #loc_left_pair
+ Status => [ 'STRING', ], #loc_left_pair
+ Catalog => [ 'ENUM' => 'Catalog', ], #loc_left_pair
+ LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
+ Created => [ 'DATE' => 'Created', ], #loc_left_pair
+
+ Linked => [ 'LINK' ], #loc_left_pair
+ LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
+ LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
+ MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
+ DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
+ RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
+ HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
+ DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+ DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
+ ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
+
+ Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
+ OwnerGroup => [ 'MEMBERSHIPFIELD' => 'Owner', ], #loc_left_pair
+ HeldBy => [ 'WATCHERFIELD' => 'HeldBy', ], #loc_left_pair
+ HeldByGroup => [ 'MEMBERSHIPFIELD' => 'HeldBy', ], #loc_left_pair
+ Contact => [ 'WATCHERFIELD' => 'Contact', ], #loc_left_pair
+ ContactGroup => [ 'MEMBERSHIPFIELD' => 'Contact', ], #loc_left_pair
+
+ CustomFieldValue => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
+ CustomField => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
+ CF => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
+);
+
+# Lower Case version of FIELDS, for case insensitivity
+our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
+
+our %SEARCHABLE_SUBFIELDS = (
+ User => [qw(
+ EmailAddress Name RealName Nickname Organization Address1 Address2
+ City State Zip Country WorkPhone HomePhone MobilePhone PagerPhone id
+ )],
+);
+
+# Mapping of Field Type to Function
+our %dispatch = (
+ ENUM => \&_EnumLimit,
+ INT => \&_IntLimit,
+ ID => \&_IdLimit,
+ LINK => \&_LinkLimit,
+ DATE => \&_DateLimit,
+ STRING => \&_StringLimit,
+ WATCHERFIELD => \&_WatcherLimit,
+ MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
+ CUSTOMFIELD => \&_CustomFieldLimit,
+# HASATTRIBUTE => \&_HasAttributeLimit,
+# LIFECYCLE => \&_LifecycleLimit,
+);
+
+# Default EntryAggregator per type
+# if you specify OP, you must specify all valid OPs
+my %DefaultEA = (
+ INT => 'AND',
+ ENUM => {
+ '=' => 'OR',
+ '!=' => 'AND'
+ },
+ DATE => {
+ 'IS' => 'OR',
+ 'IS NOT' => 'OR',
+ '=' => 'OR',
+ '>=' => 'AND',
+ '<=' => 'AND',
+ '>' => 'AND',
+ '<' => 'AND'
+ },
+ STRING => {
+ '=' => 'OR',
+ '!=' => 'AND',
+ 'LIKE' => 'AND',
+ 'NOT LIKE' => 'AND'
+ },
+ LINK => 'OR',
+ LINKFIELD => 'AND',
+ TARGET => 'AND',
+ BASE => 'AND',
+ WATCHERFIELD => {
+ '=' => 'OR',
+ '!=' => 'AND',
+ 'LIKE' => 'OR',
+ 'NOT LIKE' => 'AND'
+ },
+
+ HASATTRIBUTE => {
+ '=' => 'AND',
+ '!=' => 'AND',
+ },
+
+ CUSTOMFIELD => 'OR',
+);
+
+sub FIELDS { return \%FIELD_METADATA }
+
+our @SORTFIELDS = qw(id Name Status Catalog Owner Created LastUpdated );
+
+=head2 SortFields
+
+Returns the list of fields that lists of assets can easily be sorted by
+
+=cut
+
+sub SortFields {
+ my $self = shift;
+ return (@SORTFIELDS);
+}
+
+
+# BEGIN SQL STUFF *********************************
+
+
+sub CleanSlate {
+ my $self = shift;
+ $self->SUPER::CleanSlate( @_ );
+ delete $self->{$_} foreach qw(
+ _sql_cf_alias
+ _sql_group_members_aliases
+ _sql_object_cfv_alias
+ _sql_role_group_aliases
+ _sql_u_watchers_alias_for_sort
+ _sql_u_watchers_aliases
+ _sql_current_user_can_see_applied
+ );
+}
+
+=head1 Limit Helper Routines
+
+These routines are the targets of a dispatch table depending on the
+type of field. They all share the same signature:
+
+ my ($self,$field,$op,$value, at rest) = @_;
+
+The values in @rest should be suitable for passing directly to
+DBIx::SearchBuilder::Limit.
+
+Essentially they are an expanded/broken out (and much simplified)
+version of what ProcessRestrictions used to do. They're also much
+more clearly delineated by the TYPE of field being processed.
+
+=head2 _IdLimit
+
+Handle ID field.
+
+=cut
+
+sub _IdLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+ return $sb->_IntLimit( $field, $op, $value, @rest );
+}
+
+=head2 _EnumLimit
+
+Handle Fields which are limited to certain values, and potentially
+need to be looked up from another class.
+
+This subroutine actually handles two different kinds of fields. For
+some the user is responsible for limiting the values. (i.e. Status,
+Type).
+
+For others, the value specified by the user will be looked by via
+specified class.
+
+Meta Data:
+ name of class to lookup in (Optional)
+
+=cut
+
+sub _EnumLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ # SQL::Statement changes != to <>. (Can we remove this now?)
+ $op = "!=" if $op eq "<>";
+
+ die "Invalid Operation: $op for $field"
+ unless $op eq "="
+ or $op eq "!=";
+
+ my $meta = $FIELD_METADATA{$field};
+ if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
+ my $class = "RT::" . $meta->[1];
+ my $o = $class->new( $sb->CurrentUser );
+ $o->Load($value);
+ $value = $o->Id || 0;
+ }
+ $sb->Limit(
+ FIELD => $field,
+ VALUE => $value,
+ OPERATOR => $op,
+ @rest,
+ );
+}
+
+=head2 _IntLimit
+
+Handle fields where the values are limited to integers. (For example,
+Priority, TimeWorked.)
+
+Meta Data:
+ None
+
+=cut
+
+sub _IntLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
+
+ # We want to support <id LIKE '1%'> for asset autocomplete,
+ # but we need to explicitly typecast on Postgres
+ if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
+ return $sb->Limit(
+ FUNCTION => "CAST(main.$field AS TEXT)",
+ OPERATOR => $op,
+ VALUE => $value,
+ @rest,
+ );
+ }
+
+ $sb->Limit(
+ FIELD => $field,
+ VALUE => $value,
+ OPERATOR => $op,
+ @rest,
+ );
+}
+
+=head2 _LinkLimit
+
+Handle fields which deal with links between assets. (MemberOf, DependsOn)
+
+Meta Data:
+ 1: Direction (From, To)
+ 2: Link Type (MemberOf, DependsOn, RefersTo)
+
+=cut
+
+sub _LinkLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ my $meta = $FIELD_METADATA{$field};
+ die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
+
+ my $is_negative = 0;
+ if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
+ $is_negative = 1;
+ }
+ my $is_null = 0;
+ $is_null = 1 if !$value || $value =~ /^null$/io;
+
+ my $direction = $meta->[1] || '';
+ my ($matchfield, $linkfield) = ('', '');
+ if ( $direction eq 'To' ) {
+ ($matchfield, $linkfield) = ("Target", "Base");
+ }
+ elsif ( $direction eq 'From' ) {
+ ($matchfield, $linkfield) = ("Base", "Target");
+ }
+ elsif ( $direction ) {
+ die "Invalid link direction '$direction' for $field\n";
+ } else {
+ $sb->_OpenParen;
+ $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
+ $sb->_LinkLimit(
+ 'LinkedFrom', $op, $value, @rest,
+ ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
+ );
+ $sb->_CloseParen;
+ return;
+ }
+
+ my $is_local = 1;
+ if ( $is_null ) {
+ $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
+ }
+ elsif ( $value =~ /\D/ ) {
+ $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
+ $is_local = 0;
+ }
+ $matchfield = "Local$matchfield" if $is_local;
+
+#For doing a left join to find "unlinked assets" we want to generate a query that looks like this
+# SELECT main.* FROM Assets main
+# LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
+# AND(main.id = Links_1.LocalTarget))
+# WHERE Links_1.LocalBase IS NULL;
+
+ my $join_expression;
+ if ( RT->Config->Get('DatabaseType') eq 'SQLite' ) {
+ $join_expression = q{'} . RT::URI::asset->new( RT->SystemUser )->LocalURIPrefix . q{' || main.id};
+ }
+ else {
+ $join_expression = q{CONCAT( '} . RT::URI::asset->new( RT->SystemUser )->LocalURIPrefix . q{', main.id )};
+ }
+ if ( $is_null ) {
+ my $linkalias = $sb->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Links',
+ FIELD2 => $linkfield,
+ EXPRESSION => $join_expression,
+ );
+ $sb->Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $meta->[2],
+ ) if $meta->[2];
+ $sb->Limit(
+ @rest,
+ ALIAS => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => $op,
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+ }
+ else {
+ my $linkalias = $sb->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Links',
+ FIELD2 => $linkfield,
+ EXPRESSION => $join_expression,
+ );
+ $sb->Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => $meta->[2],
+ ) if $meta->[2];
+ $sb->Limit(
+ LEFTJOIN => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => '=',
+ VALUE => $value,
+ );
+ $sb->Limit(
+ @rest,
+ ALIAS => $linkalias,
+ FIELD => $matchfield,
+ OPERATOR => $is_negative? 'IS': 'IS NOT',
+ VALUE => 'NULL',
+ QUOTEVALUE => 0,
+ );
+ }
+}
+
+=head2 _DateLimit
+
+Handle date fields. (Created, LastTold..)
+
+Meta Data:
+ 1: type of link. (Probably not necessary.)
+
+=cut
+
+sub _DateLimit {
+ my ( $sb, $field, $op, $value, %rest ) = @_;
+
+ die "Invalid Date Op: $op"
+ unless $op =~ /^(=|>|<|>=|<=|IS(\s+NOT)?)$/i;
+
+ my $meta = $FIELD_METADATA{$field};
+ die "Incorrect Meta Data for $field"
+ unless ( defined $meta->[1] );
+
+ if ( $op =~ /^(IS(\s+NOT)?)$/i) {
+ return $sb->Limit(
+ FUNCTION => $sb->NotSetDateToNullFunction,
+ FIELD => $meta->[1],
+ OPERATOR => $op,
+ VALUE => "NULL",
+ %rest,
+ );
+ }
+
+ if ( my $subkey = $rest{SUBKEY} ) {
+ if ( $subkey eq 'DayOfWeek' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+ for ( my $i = 0; $i < @RT::Date::DAYS_OF_WEEK; $i++ ) {
+ # Use a case-insensitive regex for better matching across
+ # locales since we don't have fc() and lc() is worse. Really
+ # we should be doing Unicode normalization too, but we don't do
+ # that elsewhere in RT.
+ #
+ # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
+ next unless lc $RT::Date::DAYS_OF_WEEK[ $i ] eq lc $value
+ or $sb->CurrentUser->loc($RT::Date::DAYS_OF_WEEK[ $i ]) =~ /^\Q$value\E$/i;
+
+ $value = $i; last;
+ }
+ return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
+ if $value =~ /[^0-9]/;
+ }
+ elsif ( $subkey eq 'Month' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+ for ( my $i = 0; $i < @RT::Date::MONTHS; $i++ ) {
+ # Use a case-insensitive regex for better matching across
+ # locales since we don't have fc() and lc() is worse. Really
+ # we should be doing Unicode normalization too, but we don't do
+ # that elsewhere in RT.
+ #
+ # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
+ next unless lc $RT::Date::MONTHS[ $i ] eq lc $value
+ or $sb->CurrentUser->loc($RT::Date::MONTHS[ $i ]) =~ /^\Q$value\E$/i;
+
+ $value = $i + 1; last;
+ }
+ return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
+ if $value =~ /[^0-9]/;
+ }
+
+ my $tz;
+ if ( RT->Config->Get('ChartsTimezonesInDB') ) {
+ my $to = $sb->CurrentUser->UserObj->Timezone
+ || RT->Config->Get('Timezone');
+ $tz = { From => 'UTC', To => $to }
+ if $to && lc $to ne 'utc';
+ }
+
+ # $subkey is validated by DateTimeFunction
+ my $function = $RT::Handle->DateTimeFunction(
+ Type => $subkey,
+ Field => $sb->NotSetDateToNullFunction,
+ Timezone => $tz,
+ );
+
+ return $sb->Limit(
+ FUNCTION => $function,
+ FIELD => $meta->[1],
+ OPERATOR => $op,
+ VALUE => $value,
+ %rest,
+ );
+ }
+
+ my $date = RT::Date->new( $sb->CurrentUser );
+ $date->Set( Format => 'unknown', Value => $value );
+
+ if ( $op eq "=" ) {
+
+ # if we're specifying =, that means we want everything on a
+ # particular single day. in the database, we need to check for >
+ # and < the edges of that day.
+
+ $date->SetToMidnight( Timezone => 'server' );
+ my $daystart = $date->ISO;
+ $date->AddDay;
+ my $dayend = $date->ISO;
+
+ $sb->_OpenParen;
+
+ $sb->Limit(
+ FIELD => $meta->[1],
+ OPERATOR => ">=",
+ VALUE => $daystart,
+ %rest,
+ );
+
+ $sb->Limit(
+ FIELD => $meta->[1],
+ OPERATOR => "<",
+ VALUE => $dayend,
+ %rest,
+ ENTRYAGGREGATOR => 'AND',
+ );
+
+ $sb->_CloseParen;
+
+ }
+ else {
+ $sb->Limit(
+ FUNCTION => $sb->NotSetDateToNullFunction,
+ FIELD => $meta->[1],
+ OPERATOR => $op,
+ VALUE => $date->ISO,
+ %rest,
+ );
+ }
+}
+
+=head2 _StringLimit
+
+Handle simple fields which are just strings. (Subject,Type)
+
+Meta Data:
+ None
+
+=cut
+
+sub _StringLimit {
+ my ( $sb, $field, $op, $value, @rest ) = @_;
+
+ # FIXME:
+ # Valid Operators:
+ # =, !=, LIKE, NOT LIKE
+ if ( RT->Config->Get('DatabaseType') eq 'Oracle'
+ && (!defined $value || !length $value)
+ && lc($op) ne 'is' && lc($op) ne 'is not'
+ ) {
+ if ($op eq '!=' || $op =~ /^NOT\s/i) {
+ $op = 'IS NOT';
+ } else {
+ $op = 'IS';
+ }
+ $value = 'NULL';
+ }
+
+ if ($field eq "Status") {
+ $value = lc $value;
+ }
+
+ $sb->Limit(
+ FIELD => $field,
+ OPERATOR => $op,
+ VALUE => $value,
+ CASESENSITIVE => 0,
+ @rest,
+ );
+}
+
+=head2 _WatcherLimit
+
+Handle watcher limits. (Requestor, CC, etc..)
+
+Meta Data:
+ 1: Field to query on
+
+
+
+=cut
+
+sub _WatcherLimit {
+ my $self = shift;
+ my $field = shift;
+ my $op = shift;
+ my $value = shift;
+ my %rest = (@_);
+
+ my $meta = $FIELD_METADATA{ $field };
+ my $type = $meta->[1] || '';
+ my $class = $meta->[2] || 'Asset';
+
+ # Bail if the subfield is not allowed
+ if ( $rest{SUBKEY}
+ and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
+ {
+ die "Invalid watcher subfield: '$rest{SUBKEY}'";
+ }
+
+ $self->RoleLimit(
+ TYPE => $type,
+ CLASS => "RT::$class",
+ FIELD => $rest{SUBKEY},
+ OPERATOR => $op,
+ VALUE => $value,
+ SUBCLAUSE => "assetsql",
+ %rest,
+ );
+}
+
+=head2 _WatcherMembershipLimit
+
+Handle watcher membership limits, i.e. whether the watcher belongs to a
+specific group or not.
+
+Meta Data:
+ 1: Role to query on
+
+=cut
+
+sub _WatcherMembershipLimit {
+ my ( $self, $field, $op, $value, %rest ) = @_;
+
+ # we don't support anything but '='
+ die "Invalid $field Op: $op"
+ unless $op =~ /^=$/;
+
+ unless ( $value =~ /^\d+$/ ) {
+ my $group = RT::Group->new( $self->CurrentUser );
+ $group->LoadUserDefinedGroup( $value );
+ $value = $group->id || 0;
+ }
+
+ my $meta = $FIELD_METADATA{$field};
+ my $type = $meta->[1] || '';
+
+ my ($members_alias, $members_column);
+ if ( $type eq 'Owner' ) {
+ ($members_alias, $members_column) = ('main', 'Owner');
+ } else {
+ (undef, undef, $members_alias) = $self->_WatcherJoin( New => 1, Name => $type );
+ $members_column = 'id';
+ }
+
+ my $cgm_alias = $self->Join(
+ ALIAS1 => $members_alias,
+ FIELD1 => $members_column,
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'MemberId',
+ );
+ $self->Limit(
+ LEFTJOIN => $cgm_alias,
+ ALIAS => $cgm_alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+ $self->Limit(
+ ALIAS => $cgm_alias,
+ FIELD => 'GroupId',
+ VALUE => $value,
+ OPERATOR => $op,
+ %rest,
+ );
+}
+
+=head2 _CustomFieldDecipher
+
+Try and turn a CF descriptor into (cfid, cfname) object pair.
+
+Takes an optional second parameter of the CF LookupType, defaults to Asset CFs.
+
+=cut
+
+sub _CustomFieldDecipher {
+ my ($self, $string, $lookuptype) = @_;
+ $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
+
+ my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
+ $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
+
+ my ($cf, $applied_to);
+
+ if ( $object ) {
+ my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
+ $applied_to = $record_class->new( $self->CurrentUser );
+ $applied_to->Load( $object );
+
+ if ( $applied_to->id ) {
+ RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
+ }
+ else {
+ RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
+ $object = 0;
+ undef $applied_to;
+ }
+ }
+
+ if ( $field =~ /\D/ ) {
+ $object ||= '';
+ my $cfs = RT::CustomFields->new( $self->CurrentUser );
+ $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
+ $cfs->LimitToLookupType($lookuptype);
+
+ if ($applied_to) {
+ $cfs->SetContextObject($applied_to);
+ $cfs->LimitToObjectId($applied_to->id);
+ }
+
+ # if there is more then one field the current user can
+ # see with the same name then we shouldn't return cf object
+ # as we don't know which one to use
+ $cf = $cfs->First;
+ if ( $cf ) {
+ $cf = undef if $cfs->Next;
+ }
+ }
+ else {
+ $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->Load( $field );
+ $cf->SetContextObject($applied_to)
+ if $cf->id and $applied_to;
+ }
+
+ return ($object, $field, $cf, $column);
+}
+
+=head2 _CustomFieldLimit
+
+Limit based on CustomFields
+
+Meta Data:
+ none
+
+=cut
+
+sub _CustomFieldLimit {
+ my ( $self, $_field, $op, $value, %rest ) = @_;
+
+ my $meta = $FIELD_METADATA{ $_field };
+ my $class = $meta->[1] || 'Asset';
+ my $type = "RT::$class"->CustomFieldLookupType;
+
+ my $field = $rest{'SUBKEY'} || die "No field specified";
+
+ # For our sanity, we can only limit on one object at a time
+
+ my ($object, $cfid, $cf, $column);
+ ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
+
+
+ $self->_LimitCustomField(
+ %rest,
+ LOOKUPTYPE => $type,
+ CUSTOMFIELD => $cf || $field,
+ KEY => $cf ? $cf->id : "$type-$object.$field",
+ OPERATOR => $op,
+ VALUE => $value,
+ COLUMN => $column,
+ SUBCLAUSE => "assetsql",
+ );
+}
+
+=head2 PrepForSerialization
+
+You don't want to serialize a big assets object, as
+the {items} hash will be instantly invalid _and_ eat
+lots of space
+
+=cut
+
+sub PrepForSerialization {
+ my $self = shift;
+ delete $self->{'items'};
+ delete $self->{'items_array'};
+ $self->RedoSearch();
+}
+
+=head2 FromSQL
+
+Convert a RT-SQL string into a set of SearchBuilder restrictions.
+
+Returns (1, 'Status message') on success and (0, 'Error Message') on
+failure.
+
+=cut
+
+sub _parser {
+ my ($self,$string) = @_;
+ my $ea = '';
+
+ my %sub_tree;
+ my $depth = 0;
+
+ my %callback;
+ $callback{'OpenParen'} = sub {
+ $self->_OpenParen;
+ $depth++;
+ push @$_, '(' foreach values %sub_tree;
+ };
+ $callback{'CloseParen'} = sub {
+ $self->_CloseParen;
+ $depth--;
+ foreach my $list ( values %sub_tree ) {
+ if ( $list->[-1] eq '(' ) {
+ pop @$list;
+ pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
+ }
+ else {
+ pop @$list while $list->[-2] ne '(';
+ $list->[-1] = pop @$list;
+ }
+ }
+ };
+ $callback{'EntryAggregator'} = sub {
+ $ea = $_[0] || '';
+ push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
+ };
+ $callback{'Condition'} = sub {
+ my ($key, $op, $value) = @_;
+
+ my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
+ my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
+ # key has dot then it's compound variant and we have subkey
+ my $subkey = '';
+ ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
+
+ # normalize key and get class (type)
+ my $class;
+ if (exists $LOWER_CASE_FIELDS{lc $key}) {
+ $key = $LOWER_CASE_FIELDS{lc $key};
+ $class = $FIELD_METADATA{$key}->[0];
+ }
+ die "Unknown field '$key' in '$string'" unless $class;
+
+ # replace __CurrentUser__ with id
+ $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
+
+
+ unless( $dispatch{ $class } ) {
+ die "No dispatch method for class '$class'"
+ }
+ my $sub = $dispatch{ $class };
+
+ my @res; my $bundle_with;
+ if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
+ if ( !$sub_tree{$key} ) {
+ $sub_tree{$key} = [ ('(')x$depth, \@res ];
+ } else {
+ $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
+ if ( $sub_tree{$key}[-1] eq '(' ) {
+ push @{ $sub_tree{$key} }, \@res;
+ }
+ }
+ }
+
+ # Remove our aggregator from subtrees where our condition didn't get added
+ pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
+
+ # A reference to @res may be pushed onto $sub_tree{$key} from
+ # above, and we fill it here.
+ @res = $sub->( $self, $key, $op, $value,
+ SUBCLAUSE => '', # don't need anymore
+ ENTRYAGGREGATOR => $ea,
+ SUBKEY => $subkey,
+ BUNDLE => $bundle_with,
+ );
+ $ea = '';
+ };
+ RT::SQL::Parse($string, \%callback);
+}
+
+sub FromSQL {
+ my ($self,$query) = @_;
+
+ {
+ # preserve first_row and show_rows across the CleanSlate
+ local ($self->{'first_row'}, $self->{'show_rows'}, $self->{_sql_looking_at});
+ $self->CleanSlate;
+ $self->_InitSQL();
+ }
+
+ return (1, $self->loc("No Query")) unless $query;
+
+ $self->{_sql_query} = $query;
+ eval {
+ local $self->{parsing_assetsql} = 1;
+ $self->_parser( $query );
+ };
+ if ( $@ ) {
+ my $error = "$@";
+ $RT::Logger->error("Couldn't parse query: $error");
+ return (0, $error);
+ }
+
+ # We don't want deleted tickets unless 'allow_deleted_search' is set
+ unless( $self->{'allow_deleted_search'} ) {
+ $self->Limit(
+ FIELD => 'Status',
+ OPERATOR => '!=',
+ VALUE => 'deleted',
+ );
+ }
+
+ # set SB's dirty flag
+ $self->{'must_redo_search'} = 1;
+ $self->{'RecalcAssetLimits'} = 0;
+ return (1, $self->loc("Valid Query"));
+}
+
+=head2 Query
+
+Returns the last string passed to L</FromSQL>.
+
+=cut
+
+sub Query {
+ my $self = shift;
+ return $self->{_sql_query};
+}
+
+=head2 ClearRestrictions
+
+Removes all restrictions irretrievably
+
+=cut
+
+sub ClearRestrictions {
+ my $self = shift;
+ delete $self->{'AssetRestrictions'};
+ $self->{_sql_looking_at} = {};
+ $self->{'RecalcAssetLimits'} = 1;
+}
+
+# Convert a set of oldstyle SB Restrictions to Clauses for RQL
+
+sub _RestrictionsToClauses {
+ my $self = shift;
+
+ my %clause;
+ foreach my $row ( keys %{ $self->{'AssetRestrictions'} } ) {
+ my $restriction = $self->{'AssetRestrictions'}{$row};
+
+ # We need to reimplement the subclause aggregation that SearchBuilder does.
+ # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
+ # Then SB AND's the different Subclauses together.
+
+ # So, we want to group things into Subclauses, convert them to
+ # SQL, and then join them with the appropriate DefaultEA.
+ # Then join each subclause group with AND.
+
+ my $field = $restriction->{'FIELD'};
+ my $realfield = $field; # CustomFields fake up a fieldname, so
+ # we need to figure that out
+
+ # One special case
+ # Rewrite LinkedTo meta field to the real field
+ if ( $field =~ /LinkedTo/ ) {
+ $realfield = $field = $restriction->{'TYPE'};
+ }
+
+ # Two special case
+ # Handle subkey fields with a different real field
+ if ( $field =~ /^(\w+)\./ ) {
+ $realfield = $1;
+ }
+
+ die "I don't know about $field yet"
+ unless ( exists $FIELD_METADATA{$realfield}
+ or $restriction->{CUSTOMFIELD} );
+
+ my $type = $FIELD_METADATA{$realfield}->[0];
+ my $op = $restriction->{'OPERATOR'};
+
+ my $value = (
+ grep {defined}
+ map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
+ )[0];
+
+ # this performs the moral equivalent of defined or/dor/C<//>,
+ # without the short circuiting.You need to use a 'defined or'
+ # type thing instead of just checking for truth values, because
+ # VALUE could be 0.(i.e. "false")
+
+ # You could also use this, but I find it less aesthetic:
+ # (although it does short circuit)
+ #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
+ # defined $restriction->{'TICKET'} ?
+ # $restriction->{TICKET} :
+ # defined $restriction->{'BASE'} ?
+ # $restriction->{BASE} :
+ # defined $restriction->{'TARGET'} ?
+ # $restriction->{TARGET} )
+
+ my $ea = $restriction->{ENTRYAGGREGATOR}
+ || $DefaultEA{$type}
+ || "AND";
+ if ( ref $ea ) {
+ die "Invalid operator $op for $field ($type)"
+ unless exists $ea->{$op};
+ $ea = $ea->{$op};
+ }
+
+ # Each CustomField should be put into a different Clause so they
+ # are ANDed together.
+ if ( $restriction->{CUSTOMFIELD} ) {
+ $realfield = $field;
+ }
+
+ exists $clause{$realfield} or $clause{$realfield} = [];
+
+ # Escape Quotes
+ $field =~ s!(['\\])!\\$1!g;
+ $value =~ s!(['\\])!\\$1!g;
+ my $data = [ $ea, $type, $field, $op, $value ];
+
+ # here is where we store extra data, say if it's a keyword or
+ # something. (I.e. "TYPE SPECIFIC STUFF")
+
+ if (lc $ea eq 'none') {
+ $clause{$realfield} = [ $data ];
+ } else {
+ push @{ $clause{$realfield} }, $data;
+ }
+ }
+ return \%clause;
+}
+
+=head2 ClausesToSQL
+
+=cut
+
+sub ClausesToSQL {
+ my $self = shift;
+ my $clauses = shift;
+ my @sql;
+
+ for my $f (keys %{$clauses}) {
+ my $sql;
+ my $first = 1;
+
+ # Build SQL from the data hash
+ for my $data ( @{ $clauses->{$f} } ) {
+ $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
+ $sql .= " '". $data->[2] . "' "; # FIELD
+ $sql .= $data->[3] . " "; # OPERATOR
+ $sql .= "'". $data->[4] . "' "; # VALUE
+ }
+
+ push @sql, " ( " . $sql . " ) ";
+ }
+
+ return join("AND", at sql);
+}
+
+sub _ProcessRestrictions {
+ my $self = shift;
+
+ delete $self->{'items_array'};
+ delete $self->{'item_map'};
+ delete $self->{'raw_rows'};
+ delete $self->{'count_all'};
+
+ my $sql = $self->Query;
+ if ( !$sql || $self->{'RecalcAssetLimits'} ) {
+
+ local $self->{using_restrictions};
+ # "Restrictions to Clauses Branch\n";
+ my $clauseRef = eval { $self->_RestrictionsToClauses; };
+ if ($@) {
+ $RT::Logger->error( "RestrictionsToClauses: " . $@ );
+ $self->FromSQL("");
+ }
+ else {
+ $sql = $self->ClausesToSQL($clauseRef);
+ $self->FromSQL($sql) if $sql;
+ }
+ }
+
+ $self->{'RecalcAssetLimits'} = 0;
+
+}
+
+sub _check_bundling_possibility {
+ my $self = shift;
+ my $string = shift;
+ my @list = reverse @_;
+ while (my $e = shift @list) {
+ next if $e eq '(';
+ if ( lc($e) eq 'and' ) {
+ return undef;
+ }
+ elsif ( lc($e) eq 'or' ) {
+ return shift @list;
+ }
+ else {
+ # should not happen
+ $RT::Logger->error(
+ "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
+ );
+ die "Internal error. Contact your system administrator.";
+ }
+ }
+ return undef;
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/lib/RT/Extension/Assets.pm b/lib/RT/Extension/Assets.pm
index 7536534..be34992 100644
--- a/lib/RT/Extension/Assets.pm
+++ b/lib/RT/Extension/Assets.pm
@@ -412,6 +412,93 @@ RT->AddJavaScript("RTx-Assets.js");
}
}
+{
+
+ package RT::Interface::Web::QueryBuilder::Tree;
+ sub GetReferencedCatalogs {
+ my $self = shift;
+
+ my $catalogs = {};
+
+ $self->traverse(
+ sub {
+ my $node = shift;
+
+ return if $node->isRoot;
+ return unless $node->isLeaf;
+
+ my $clause = $node->getNodeValue();
+ return unless $clause->{ Key } eq 'Catalog';
+ return unless $clause->{ Op } eq '=';
+
+ $catalogs->{ $clause->{ RawValue } } = 1;
+ }
+ );
+
+ return $catalogs;
+ }
+
+ sub ParseAssetSQL {
+ my $self = shift;
+ my %args = (
+ Query => '',
+ CurrentUser => '', #XXX: Hack
+ @_
+ );
+ my $string = $args{ 'Query' };
+
+ my @results;
+
+ my %field = %{ RT::Assets->new( $args{ 'CurrentUser' } )->FIELDS };
+ my %lcfield = map { ( lc( $_ ) => $_ ) } keys %field;
+
+ my $node = $self;
+
+ my %callback;
+ $callback{ 'OpenParen' } = sub {
+ $node = __PACKAGE__->new( 'AND', $node );
+ };
+ $callback{ 'CloseParen' } = sub { $node = $node->getParent };
+ $callback{ 'EntryAggregator' } = sub { $node->setNodeValue( $_[ 0 ] ) };
+ $callback{ 'Condition' } = sub {
+ my ( $key, $op, $value ) = @_;
+ my $rawvalue = $value;
+
+ my ( $main_key ) = split /[.]/, $key;
+
+ my $class;
+ if ( exists $lcfield{ lc $main_key } ) {
+ $key =~ s/^[^.]+/ $lcfield{ lc $main_key } /e;
+ ( $main_key ) = split /[.]/, $key; # make the case right
+ $class = $field{ $main_key }->[ 0 ];
+ }
+ unless ( $class ) {
+ push @results, [ $args{ 'CurrentUser' }->loc( "Unknown field: [_1]", $key ), -1 ];
+ }
+
+ if ( lc $op eq 'is' || lc $op eq 'is not' ) {
+ $value = 'NULL'; # just fix possible mistakes here
+ }
+ elsif ( $value !~ /^[+-]?[0-9]+$/ ) {
+ $value =~ s/(['\\])/\\$1/g;
+ $value = "'$value'";
+ }
+
+ if ( $key =~ s/(['\\])/\\$1/g or $key =~ /[^{}\w\.]/ ) {
+ $key = "'$key'";
+ }
+
+ my $clause = { Key => $key, Op => $op, Value => $value, RawValue => $rawvalue };
+ $node->addChild( __PACKAGE__->new( $clause ) );
+ };
+ $callback{ 'Error' } = sub { push @results, @_ };
+
+ require RT::SQL;
+ RT::SQL::Parse( $string, \%callback );
+ return @results;
+ }
+}
+
=head1 INSTALLATION
Assets requires version 4.2.1 or higher of RT.
@@ -428,6 +515,8 @@ This step may require root permissions.
=item Patch your RT
+ patch -d /opt/rt4 -p1 < patches/assets-query-builder.patch
+
Assets requires a small patch to work on versions of RT prior to 4.2.3.
To patch RT, run:
diff --git a/patches/assets-query-builder.patch b/patches/assets-query-builder.patch
new file mode 100644
index 0000000..f2d6a43
--- /dev/null
+++ b/patches/assets-query-builder.patch
@@ -0,0 +1,35 @@
+diff --git a/share/html/Elements/SelectStatus b/share/html/Elements/SelectStatus
+index e29e7cf..3820033 100644
+--- a/share/html/Elements/SelectStatus
++++ b/share/html/Elements/SelectStatus
+@@ -94,7 +94,7 @@ if ( @Statuses ) {
+ }
+
+ if (not keys %statuses_by_lifecycle) {
+- for my $lifecycle (map { RT::Lifecycle->Load($_) } RT::Lifecycle->List($Type)) {
++ for my $lifecycle (map { RT::Lifecycle->Load(Type => $Type, Name => $_) } RT::Lifecycle->List($Type)) {
+ $statuses_by_lifecycle{$lifecycle->Name} = [ $lifecycle->Valid ];
+ }
+ }
+diff --git a/share/html/Elements/Tabs b/share/html/Elements/Tabs
+index c6c6505..24617b8 100644
+--- a/share/html/Elements/Tabs
++++ b/share/html/Elements/Tabs
+@@ -936,7 +936,7 @@ my $build_main_nav = sub {
+ PageMenu()->child( edit => title => loc('Edit'), path => '/Prefs/MyRT.html' );
+ }
+
+- $m->callback( CallbackName => 'Privileged', Path => $request_path );
++ $m->callback( CallbackName => 'Privileged', Path => $request_path, ARGSRef => \%ARGS );
+ };
+
+ my $build_selfservice_nav = sub {
+@@ -989,7 +989,7 @@ my $build_selfservice_nav = sub {
+
+ PageWidgets->child( goto => raw_html => $m->scomp('/SelfService/Elements/GotoTicket') );
+
+- $m->callback( CallbackName => 'SelfService', Path => $request_path );
++ $m->callback( CallbackName => 'SelfService', Path => $request_path, ARGSRef => \%ARGS );
+ };
+
+
diff --git a/static/css/RTx-Assets.css b/static/css/RTx-Assets.css
index 3526fa7..5504928 100644
--- a/static/css/RTx-Assets.css
+++ b/static/css/RTx-Assets.css
@@ -227,3 +227,12 @@ body#comp-Asset-Search .collection-as-table td {
width: 10em;
}
}
+
+/* search */
+#comp-Asset-Search-Build #body {
+ position: relative;
+}
+
+#comp-Asset-Search-Build #pick-criteria {
+ min-height: 400px;
+}
-----------------------------------------------------------------------
More information about the Bps-public-commit
mailing list