1776 lines
62 KiB
Perl
1776 lines
62 KiB
Perl
# --
|
|
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
|
|
# --
|
|
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
|
|
# the enclosed file COPYING for license information (GPL). If you
|
|
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
|
|
# --
|
|
|
|
package Kernel::Modules::AgentFAQSearch;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Kernel::Language qw(Translatable);
|
|
use Kernel::System::VariableCheck qw(:all);
|
|
|
|
our $ObjectManagerDisabled = 1;
|
|
|
|
sub new {
|
|
my ( $Type, %Param ) = @_;
|
|
|
|
# Allocate new hash for object.
|
|
my $Self = {%Param};
|
|
bless( $Self, $Type );
|
|
|
|
# Get config for frontend.
|
|
$Self->{Config} = $Kernel::OM->Get('Kernel::Config')->Get("FAQ::Frontend::$Self->{Action}");
|
|
|
|
# Get the dynamic fields for FAQ object.
|
|
$Self->{DynamicField} = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
|
|
Valid => 1,
|
|
ObjectType => 'FAQ',
|
|
FieldFilter => $Self->{Config}->{DynamicField} || {},
|
|
);
|
|
|
|
return $Self;
|
|
}
|
|
|
|
sub Run {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my $Output;
|
|
|
|
my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
|
|
|
|
# Get config from constructor.
|
|
my $Config = $Self->{Config};
|
|
|
|
# Get config data.
|
|
my $StartHit = int( $ParamObject->GetParam( Param => 'StartHit' ) || 1 );
|
|
my $SearchLimit = $Config->{SearchLimit} || 500;
|
|
my $SortBy = $ParamObject->GetParam( Param => 'SortBy' )
|
|
|| $Config->{'SortBy::Default'}
|
|
|| 'FAQID';
|
|
my $OrderBy = $ParamObject->GetParam( Param => 'OrderBy' )
|
|
|| $Config->{'Order::Default'}
|
|
|| 'Down';
|
|
my $Profile = $ParamObject->GetParam( Param => 'Profile' ) || '';
|
|
my $SaveProfile = $ParamObject->GetParam( Param => 'SaveProfile' ) || '';
|
|
my $TakeLastSearch = $ParamObject->GetParam( Param => 'TakeLastSearch' ) || '';
|
|
my $EraseTemplate = $ParamObject->GetParam( Param => 'EraseTemplate' ) || '';
|
|
|
|
my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
|
|
|
|
# Build output for open search description by FAQ number.
|
|
if ( $Self->{Subaction} eq 'OpenSearchDescriptionFAQNumber' ) {
|
|
my $Output = $LayoutObject->Output(
|
|
TemplateFile => 'AgentFAQSearchOpenSearchDescriptionFAQNumber',
|
|
Data => \%Param,
|
|
);
|
|
return $LayoutObject->Attachment(
|
|
Filename => 'OpenSearchDescriptionFAQNumber.xml',
|
|
ContentType => 'application/opensearchdescription+xml',
|
|
Content => $Output,
|
|
Type => 'inline',
|
|
);
|
|
}
|
|
|
|
# Build output for open search description by full-text.
|
|
if ( $Self->{Subaction} eq 'OpenSearchDescriptionFulltext' ) {
|
|
my $Output = $LayoutObject->Output(
|
|
TemplateFile => 'AgentFAQSearchOpenSearchDescriptionFulltext',
|
|
Data => \%Param,
|
|
);
|
|
return $LayoutObject->Attachment(
|
|
Filename => 'OpenSearchDescriptionFulltext.xml',
|
|
ContentType => 'application/opensearchdescription+xml',
|
|
Content => $Output,
|
|
Type => 'inline',
|
|
);
|
|
}
|
|
|
|
# Search with a saved template.
|
|
if ( $ParamObject->GetParam( Param => 'SearchTemplate' ) && $Profile ) {
|
|
return $LayoutObject->Redirect(
|
|
OP =>
|
|
"Action=AgentFAQSearch;Subaction=Search;TakeLastSearch=1;SaveProfile=1;Profile=$Profile",
|
|
);
|
|
}
|
|
|
|
my %GetParam;
|
|
|
|
my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
|
|
my $SearchProfileObject = $Kernel::OM->Get('Kernel::System::SearchProfile');
|
|
|
|
# Load profiles string params (press load profile).
|
|
if ( ( $Self->{Subaction} eq 'LoadProfile' && $Profile ) || $TakeLastSearch ) {
|
|
%GetParam = $SearchProfileObject->SearchProfileGet(
|
|
Base => 'FAQSearch',
|
|
Name => $Profile,
|
|
UserLogin => $Self->{UserLogin},
|
|
);
|
|
}
|
|
|
|
# Get search string params (get submitted params).
|
|
else {
|
|
|
|
# Get scalar search parameters from web request.
|
|
for my $ParamName (
|
|
qw(Number Title Keyword Fulltext ResultForm VoteSearch VoteSearchType RateSearch
|
|
RateSearchType ApprovedSearch
|
|
TimeSearchType ChangeTimeSearchType
|
|
ItemCreateTimePointFormat ItemCreateTimePoint
|
|
ItemCreateTimePointStart
|
|
ItemCreateTimeStart ItemCreateTimeStartDay ItemCreateTimeStartMonth
|
|
ItemCreateTimeStartYear
|
|
ItemCreateTimeStop ItemCreateTimeStopDay ItemCreateTimeStopMonth
|
|
ItemCreateTimeStopYear
|
|
ItemChangeTimePointFormat ItemChangeTimePoint
|
|
ItemChangeTimePointStart
|
|
ItemChangeTimeStart ItemChangeTimeStartDay ItemChangeTimeStartMonth
|
|
ItemChangeTimeStartYear
|
|
ItemChangeTimeStop ItemChangeTimeStopDay ItemChangeTimeStopMonth
|
|
ItemChangeTimeStopYear
|
|
)
|
|
)
|
|
{
|
|
$GetParam{$ParamName} = $ParamObject->GetParam( Param => $ParamName );
|
|
|
|
# Remove whitespace on the start and end.
|
|
if ( $GetParam{$ParamName} ) {
|
|
$GetParam{$ParamName} =~ s{ \A \s+ }{}xms;
|
|
$GetParam{$ParamName} =~ s{ \s+ \z }{}xms;
|
|
}
|
|
}
|
|
|
|
# Get array search parameters from web request.
|
|
for my $SearchParam (
|
|
qw(CategoryIDs LanguageIDs ValidIDs StateIDs CreatedUserIDs LastChangedUserIDs)
|
|
)
|
|
{
|
|
my @Array = $ParamObject->GetArray( Param => $SearchParam );
|
|
if (@Array) {
|
|
$GetParam{$SearchParam} = \@Array;
|
|
}
|
|
}
|
|
|
|
# Get Dynamic fields from param object.
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
|
|
# Get search field preferences.
|
|
my $SearchFieldPreferences = $DynamicFieldBackendObject->SearchFieldPreferences(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
);
|
|
|
|
next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences);
|
|
|
|
PREFERENCE:
|
|
for my $Preference ( @{$SearchFieldPreferences} ) {
|
|
|
|
# Extract the dynamic field value from the web request.
|
|
my $DynamicFieldValue = $DynamicFieldBackendObject->SearchFieldValueGet(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
ParamObject => $ParamObject,
|
|
ReturnProfileStructure => 1,
|
|
LayoutObject => $LayoutObject,
|
|
Type => $Preference->{Type},
|
|
);
|
|
|
|
# Set the complete value structure in GetParam to store it later in the search profile.
|
|
if ( IsHashRefWithData($DynamicFieldValue) ) {
|
|
%GetParam = ( %GetParam, %{$DynamicFieldValue} );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Get approved option.
|
|
if ( $GetParam{ApprovedSearch} && $GetParam{ApprovedSearch} eq 'Yes' ) {
|
|
$GetParam{Approved} = 1;
|
|
}
|
|
elsif ( $GetParam{ApprovedSearch} && $GetParam{ApprovedSearch} eq 'No' ) {
|
|
$GetParam{Approved} = 0;
|
|
}
|
|
|
|
# Get create time option.
|
|
if ( !$GetParam{TimeSearchType} ) {
|
|
$GetParam{'TimeSearchType::None'} = 1;
|
|
}
|
|
elsif ( $GetParam{TimeSearchType} eq 'TimePoint' ) {
|
|
$GetParam{'TimeSearchType::TimePoint'} = 1;
|
|
}
|
|
elsif ( $GetParam{TimeSearchType} eq 'TimeSlot' ) {
|
|
$GetParam{'TimeSearchType::TimeSlot'} = 1;
|
|
}
|
|
|
|
# Get change time option.
|
|
if ( !$GetParam{ChangeTimeSearchType} ) {
|
|
$GetParam{'ChangeTimeSearchType::None'} = 1;
|
|
}
|
|
elsif ( $GetParam{ChangeTimeSearchType} eq 'TimePoint' ) {
|
|
$GetParam{'ChangeTimeSearchType::TimePoint'} = 1;
|
|
}
|
|
elsif ( $GetParam{ChangeTimeSearchType} eq 'TimeSlot' ) {
|
|
$GetParam{'ChangeTimeSearchType::TimeSlot'} = 1;
|
|
}
|
|
|
|
# Set result form ENV.
|
|
if ( !$GetParam{ResultForm} ) {
|
|
$GetParam{ResultForm} = '';
|
|
}
|
|
|
|
my $FAQObject = $Kernel::OM->Get('Kernel::System::FAQ');
|
|
|
|
# Show result site.
|
|
if ( $Self->{Subaction} eq 'Search' && !$EraseTemplate ) {
|
|
|
|
# Fill up profile name (e.g. with last-search).
|
|
if ( !$Profile || !$SaveProfile ) {
|
|
$Profile = 'last-search';
|
|
}
|
|
|
|
my $SessionObject = $Kernel::OM->Get('Kernel::System::AuthSession');
|
|
|
|
# Store last overview screen.
|
|
my $URL = "Action=AgentFAQSearch;Subaction=Search"
|
|
. ";Profile=" . $LayoutObject->LinkEncode($Profile)
|
|
. ";SortBy=" . $LayoutObject->LinkEncode($SortBy)
|
|
. ";OrderBy=" . $LayoutObject->LinkEncode($OrderBy)
|
|
. ";TakeLastSearch=1"
|
|
. ";StartHit=" . $LayoutObject->LinkEncode($StartHit);
|
|
|
|
$SessionObject->UpdateSessionID(
|
|
SessionID => $Self->{SessionID},
|
|
Key => 'LastScreenOverview',
|
|
Value => $URL,
|
|
);
|
|
$SessionObject->UpdateSessionID(
|
|
SessionID => $Self->{SessionID},
|
|
Key => 'LastScreenView',
|
|
Value => $URL,
|
|
);
|
|
|
|
# Save search profile (under last-search or real profile name).
|
|
$SaveProfile = 1;
|
|
|
|
# Remember last search values.
|
|
if ( $SaveProfile && $Profile ) {
|
|
|
|
# Remove old profile stuff.
|
|
$SearchProfileObject->SearchProfileDelete(
|
|
Base => 'FAQSearch',
|
|
Name => $Profile,
|
|
UserLogin => $Self->{UserLogin},
|
|
);
|
|
|
|
# Insert new profile parameters.
|
|
for my $Key ( sort keys %GetParam ) {
|
|
if ( $GetParam{$Key} ) {
|
|
$SearchProfileObject->SearchProfileAdd(
|
|
Base => 'FAQSearch',
|
|
Name => $Profile,
|
|
Key => $Key,
|
|
Value => $GetParam{$Key},
|
|
UserLogin => $Self->{UserLogin},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
my %TimeMap = (
|
|
ItemCreate => 'Time',
|
|
ItemChange => 'ChangeTime',
|
|
);
|
|
|
|
for my $TimeType ( sort keys %TimeMap ) {
|
|
|
|
if ( !$GetParam{ $TimeMap{$TimeType} . 'SearchType' } ) {
|
|
|
|
# Do nothing with time stuff
|
|
}
|
|
elsif ( $GetParam{ $TimeMap{$TimeType} . 'SearchType' } eq 'TimeSlot' ) {
|
|
for my $Key (qw(Month Day)) {
|
|
$GetParam{ $TimeType . 'TimeStart' . $Key }
|
|
= sprintf( "%02d", $GetParam{ $TimeType . 'TimeStart' . $Key } );
|
|
$GetParam{ $TimeType . 'TimeStop' . $Key }
|
|
= sprintf( "%02d", $GetParam{ $TimeType . 'TimeStop' . $Key } );
|
|
}
|
|
if (
|
|
$GetParam{ $TimeType . 'TimeStartDay' }
|
|
&& $GetParam{ $TimeType . 'TimeStartMonth' }
|
|
&& $GetParam{ $TimeType . 'TimeStartYear' }
|
|
)
|
|
{
|
|
$GetParam{ $TimeType . 'TimeNewerDate' } = $GetParam{ $TimeType . 'TimeStartYear' } . '-'
|
|
. $GetParam{ $TimeType . 'TimeStartMonth' } . '-'
|
|
. $GetParam{ $TimeType . 'TimeStartDay' }
|
|
. ' 00:00:00';
|
|
}
|
|
if (
|
|
$GetParam{ $TimeType . 'TimeStopDay' }
|
|
&& $GetParam{ $TimeType . 'TimeStopMonth' }
|
|
&& $GetParam{ $TimeType . 'TimeStopYear' }
|
|
)
|
|
{
|
|
$GetParam{ $TimeType . 'TimeOlderDate' } = $GetParam{ $TimeType . 'TimeStopYear' } . '-'
|
|
. $GetParam{ $TimeType . 'TimeStopMonth' } . '-'
|
|
. $GetParam{ $TimeType . 'TimeStopDay' }
|
|
. ' 23:59:59';
|
|
}
|
|
}
|
|
elsif ( $GetParam{ $TimeMap{$TimeType} . 'SearchType' } eq 'TimePoint' ) {
|
|
if (
|
|
$GetParam{ $TimeType . 'TimePoint' }
|
|
&& $GetParam{ $TimeType . 'TimePointStart' }
|
|
&& $GetParam{ $TimeType . 'TimePointFormat' }
|
|
)
|
|
{
|
|
my $Time = 0;
|
|
if ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'minute' ) {
|
|
$Time = $GetParam{ $TimeType . 'TimePoint' };
|
|
}
|
|
elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'hour' ) {
|
|
$Time = $GetParam{ $TimeType . 'TimePoint' } * 60;
|
|
}
|
|
elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'day' ) {
|
|
$Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24;
|
|
}
|
|
elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'week' ) {
|
|
$Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24 * 7;
|
|
}
|
|
elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'month' ) {
|
|
$Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24 * 30;
|
|
}
|
|
elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'year' ) {
|
|
$Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24 * 365;
|
|
}
|
|
if ( $GetParam{ $TimeType . 'TimePointStart' } eq 'Before' ) {
|
|
|
|
# More than ... ago.
|
|
$GetParam{ $TimeType . 'TimeOlderMinutes' } = $Time;
|
|
}
|
|
elsif ( $GetParam{ $TimeType . 'TimePointStart' } eq 'Next' ) {
|
|
|
|
# Within next.
|
|
$GetParam{ $TimeType . 'TimeNewerMinutes' } = 0;
|
|
$GetParam{ $TimeType . 'TimeOlderMinutes' } = -$Time;
|
|
}
|
|
else {
|
|
# Within last ...
|
|
$GetParam{ $TimeType . 'TimeOlderMinutes' } = 0;
|
|
$GetParam{ $TimeType . 'TimeNewerMinutes' } = $Time;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Dynamic fields search parameters for FAQ search.
|
|
my %DynamicFieldSearchParameters;
|
|
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
|
|
# Get search field preferences.
|
|
my $SearchFieldPreferences = $DynamicFieldBackendObject->SearchFieldPreferences(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
);
|
|
|
|
next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences);
|
|
|
|
PREFERENCE:
|
|
for my $Preference ( @{$SearchFieldPreferences} ) {
|
|
|
|
my $DynamicFieldValue = $DynamicFieldBackendObject->SearchFieldValueGet(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
ParamObject => $ParamObject,
|
|
Type => $Preference->{Type},
|
|
ReturnProfileStructure => 1,
|
|
);
|
|
|
|
# Set the complete value structure in %DynamicFieldValues to discard those where the
|
|
# value will not be possible to get.
|
|
next PREFERENCE if !IsHashRefWithData($DynamicFieldValue);
|
|
|
|
# Extract the dynamic field value from the profile.
|
|
my $SearchParameter = $DynamicFieldBackendObject->SearchFieldParameterBuild(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
Profile => \%GetParam,
|
|
LayoutObject => $LayoutObject,
|
|
Type => $Preference->{Type},
|
|
);
|
|
|
|
# Set search parameter.
|
|
if ( defined $SearchParameter ) {
|
|
$DynamicFieldSearchParameters{ 'DynamicField_' . $DynamicFieldConfig->{Name} }
|
|
= $SearchParameter->{Parameter};
|
|
}
|
|
}
|
|
}
|
|
|
|
# Prepare full-text search.
|
|
if ( $GetParam{Fulltext} ) {
|
|
$GetParam{ContentSearch} = 'OR';
|
|
$GetParam{What} = $GetParam{Fulltext};
|
|
}
|
|
|
|
my %ValidList = $Kernel::OM->Get('Kernel::System::Valid')->ValidList();
|
|
my @AllValidIDs = keys %ValidList;
|
|
|
|
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
|
|
|
|
# Set default interface settings.
|
|
my $Interface = $FAQObject->StateTypeGet(
|
|
Name => 'internal',
|
|
UserID => $Self->{UserID},
|
|
);
|
|
my $InterfaceStates = $FAQObject->StateTypeList(
|
|
Types => $ConfigObject->Get('FAQ::Agent::StateTypes'),
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
# Prepare search states.
|
|
my $SearchStates;
|
|
if ( !IsArrayRefWithData( $GetParam{StateIDs} ) ) {
|
|
$SearchStates = $InterfaceStates;
|
|
}
|
|
else {
|
|
STATETYPEID:
|
|
for my $StateTypeID ( @{ $GetParam{StateIDs} } ) {
|
|
next STATETYPEID if !$StateTypeID;
|
|
next STATETYPEID if !$InterfaceStates->{$StateTypeID};
|
|
$SearchStates->{$StateTypeID} = $InterfaceStates->{$StateTypeID};
|
|
}
|
|
}
|
|
|
|
if ( IsNumber( $GetParam{VoteSearch} ) ) {
|
|
$GetParam{Votes} = {
|
|
$GetParam{VoteSearchType} => $GetParam{VoteSearch}
|
|
};
|
|
}
|
|
|
|
if ( IsNumber( $GetParam{RateSearch} ) ) {
|
|
$GetParam{Rate} = {
|
|
$GetParam{RateSearchType} => $GetParam{RateSearch}
|
|
};
|
|
}
|
|
|
|
# Get UserCategoryGroup Hash.
|
|
# This returns a Hash of the following sample data structure:
|
|
#
|
|
# $UserCatGroup = {
|
|
# '1' => {
|
|
# '3' => 'MiscSub'
|
|
# },
|
|
# '3' => {},
|
|
# '0' => {
|
|
# '1' => 'Misc',
|
|
# '2' => 'secret'
|
|
# },
|
|
# '2' => {}
|
|
# };
|
|
#
|
|
# Keys of the outer hash inform about subcategories.
|
|
# 0 Shows top level CategoryIDs.
|
|
# 1 Shows the SubcategoryIDs of Category 1.
|
|
# 2 and 3 are empty hashes because these categories don't have subcategories.
|
|
#
|
|
# Keys of the inner hashes are CategoryIDs a user is allowed to have ro access to.
|
|
# Values are the Category names.
|
|
|
|
my $UserCatGroup = $FAQObject->GetUserCategories(
|
|
Type => 'ro',
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
# Find CategoryIDs the current User is allowed to view.
|
|
my %AllowedCategoryIDs = ();
|
|
|
|
if ( $UserCatGroup && ref $UserCatGroup eq 'HASH' ) {
|
|
|
|
# So now we have to extract all Category ID's of the "inner hashes"
|
|
# -> Loop through the outer category ID's.
|
|
for my $Level ( sort keys %{$UserCatGroup} ) {
|
|
|
|
# Check if the Value of the current hash key is a hash ref.
|
|
if ( $UserCatGroup->{$Level} && ref $UserCatGroup->{$Level} eq 'HASH' ) {
|
|
|
|
# Map the keys of the inner hash to a TempIDs hash.
|
|
# Original Data structure:
|
|
# {
|
|
# '1' => 'Misc',
|
|
# '2' => 'secret'
|
|
# }
|
|
#
|
|
# after mapping:
|
|
#
|
|
# {
|
|
# '1' => 1,
|
|
# '2' => 1'
|
|
# }
|
|
|
|
my %TempIDs = map { $_ => 1 } keys %{ $UserCatGroup->{$Level} };
|
|
|
|
# Put the TempIDs over the formally found AllowedCategorys to produce a hash
|
|
# that holds all CategoryID as keys and the number 1 as values.
|
|
%AllowedCategoryIDs = (
|
|
%AllowedCategoryIDs,
|
|
%TempIDs
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
# For the database query it's necessary to have an array of CategoryIDs.
|
|
my @CategoryIDs = ();
|
|
|
|
if (%AllowedCategoryIDs) {
|
|
@CategoryIDs = sort keys %AllowedCategoryIDs;
|
|
}
|
|
|
|
# Categories got from the web request could include a not allowed category if the user
|
|
# temper with the categories drop-box, a check is needed.
|
|
#
|
|
# "Map" copy from one array to another, while "grep" will only let pass the categories
|
|
# that are defined in the %AllowedCategoryIDs hash.
|
|
if ( IsArrayRefWithData( $GetParam{CategoryIDs} ) ) {
|
|
@{ $GetParam{CategoryIDs} } = map {$_} grep { $AllowedCategoryIDs{$_} } @{ $GetParam{CategoryIDs} };
|
|
}
|
|
|
|
# Just search if we do have categories, we have access to.
|
|
# If we don't have access to any category, a search with no CategoryIDs
|
|
# would result in finding all categories.
|
|
#
|
|
# It is not possible to create FAQ's without categories
|
|
# so at least one category has to be present
|
|
|
|
my @ViewableItemIDs = ();
|
|
|
|
if (@CategoryIDs) {
|
|
|
|
# Perform FAQ search.
|
|
# Default search on all valid ids, this can be overwritten by %GetParam.
|
|
@ViewableItemIDs = $FAQObject->FAQSearch(
|
|
OrderBy => [$SortBy],
|
|
OrderByDirection => [$OrderBy],
|
|
Limit => $SearchLimit,
|
|
UserID => $Self->{UserID},
|
|
States => $SearchStates,
|
|
Interface => $Interface,
|
|
ContentSearchPrefix => '*',
|
|
ContentSearchSuffix => '*',
|
|
ValidIDs => \@AllValidIDs,
|
|
CategoryIDs => \@CategoryIDs,
|
|
%GetParam,
|
|
%DynamicFieldSearchParameters,
|
|
);
|
|
}
|
|
|
|
my $MultiLanguage = $ConfigObject->Get('FAQ::MultiLanguage');
|
|
|
|
# CSV and Excel output.
|
|
if (
|
|
$GetParam{ResultForm} eq 'CSV'
|
|
|| $GetParam{ResultForm} eq 'Excel'
|
|
)
|
|
{
|
|
my @TmpCSVHead;
|
|
my @CSVHead;
|
|
my @CSVData;
|
|
|
|
# Get the FAQ dynamic fields for CSV display.
|
|
my $CSVDynamicField = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
|
|
Valid => 1,
|
|
ObjectType => 'FAQ',
|
|
FieldFilter => $Config->{SearchCSVDynamicField} || {},
|
|
);
|
|
|
|
for my $ItemID (@ViewableItemIDs) {
|
|
|
|
# Get FAQ data details.
|
|
my %FAQData = $FAQObject->FAQGet(
|
|
ItemID => $ItemID,
|
|
ItemFields => 0,
|
|
DynamicFields => 1,
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
# Get info for CSV output.
|
|
my %CSVInfo = (%FAQData);
|
|
|
|
$CSVInfo{Changed} = $LayoutObject->{LanguageObject}->FormatTimeString(
|
|
$FAQData{Changed},
|
|
'DateFormat',
|
|
);
|
|
|
|
# CSV quote.
|
|
if ( !@CSVHead ) {
|
|
@TmpCSVHead = qw( FAQNumber Title Category);
|
|
@CSVHead = qw( FAQNumber Title Category);
|
|
|
|
# Insert language header.
|
|
if ($MultiLanguage) {
|
|
push @TmpCSVHead, 'Language';
|
|
push @CSVHead, 'Language';
|
|
}
|
|
|
|
push @TmpCSVHead, qw(State Changed);
|
|
push @CSVHead, qw(State Changed);
|
|
|
|
# Include the selected dynamic fields on CVS results.
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{$CSVDynamicField} ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
next DYNAMICFIELD if !$DynamicFieldConfig->{Name};
|
|
next DYNAMICFIELD if $DynamicFieldConfig->{Name} eq '';
|
|
|
|
push @TmpCSVHead, 'DynamicField_' . $DynamicFieldConfig->{Name};
|
|
push @CSVHead, $DynamicFieldConfig->{Label};
|
|
}
|
|
}
|
|
|
|
# Insert data.
|
|
my @Data;
|
|
for my $Header (@TmpCSVHead) {
|
|
|
|
# Check if header is a dynamic field and get the value from dynamic field backend
|
|
if ( $Header =~ m{\A DynamicField_ ( [a-zA-Z\d]+ ) \z}xms ) {
|
|
|
|
# Loop over the dynamic fields configured for CSV output.
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{$CSVDynamicField} ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
next DYNAMICFIELD if !$DynamicFieldConfig->{Name};
|
|
|
|
# Skip all fields that does not match with current field name ($1)
|
|
# with out the 'DynamicField_' prefix.
|
|
next DYNAMICFIELD if $DynamicFieldConfig->{Name} ne $1;
|
|
|
|
# Get the value as for print (to correctly display).
|
|
my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
Value => $CSVInfo{$Header},
|
|
HTMLOutput => 0,
|
|
LayoutObject => $LayoutObject,
|
|
);
|
|
push @Data, $ValueStrg->{Value};
|
|
|
|
last DYNAMICFIELD;
|
|
}
|
|
}
|
|
|
|
# Otherwise retrieve data from FAQ item.
|
|
else {
|
|
if ( $Header eq 'FAQNumber' ) {
|
|
push @Data, $CSVInfo{'Number'};
|
|
}
|
|
elsif ( $Header eq 'Category' ) {
|
|
push @Data, $CSVInfo{'CategoryName'};
|
|
}
|
|
else {
|
|
push @Data, $CSVInfo{$Header};
|
|
}
|
|
}
|
|
}
|
|
push @CSVData, \@Data;
|
|
}
|
|
|
|
# CSV quote.
|
|
# Translate non existing header may result in a garbage file.
|
|
if ( !@CSVHead ) {
|
|
@TmpCSVHead = qw(FAQNumber Title Category);
|
|
@CSVHead = qw(FAQNumber Title Category);
|
|
|
|
# Insert language header.
|
|
if ($MultiLanguage) {
|
|
push @TmpCSVHead, 'Language';
|
|
push @CSVHead, 'Language';
|
|
}
|
|
|
|
push @TmpCSVHead, qw(State Changed);
|
|
push @CSVHead, qw(State Changed);
|
|
|
|
# Include the selected dynamic fields on CVS results.
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{$CSVDynamicField} ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
next DYNAMICFIELD if !$DynamicFieldConfig->{Name};
|
|
next DYNAMICFIELD if $DynamicFieldConfig->{Name} eq '';
|
|
|
|
push @TmpCSVHead, 'DynamicField_' . $DynamicFieldConfig->{Name};
|
|
push @CSVHead, $DynamicFieldConfig->{Label};
|
|
}
|
|
}
|
|
|
|
# Get Separator from language file.
|
|
my $UserCSVSeparator = $LayoutObject->{LanguageObject}->{Separator};
|
|
|
|
if ( $ConfigObject->Get('PreferencesGroups')->{CSVSeparator}->{Active} ) {
|
|
my %UserData = $Kernel::OM->Get('Kernel::System::User')->GetUserData(
|
|
UserID => $Self->{UserID},
|
|
);
|
|
if ( $UserData{UserCSVSeparator} ) {
|
|
$UserCSVSeparator = $UserData{UserCSVSeparator};
|
|
}
|
|
}
|
|
|
|
# Translate headers.
|
|
for my $Header (@CSVHead) {
|
|
|
|
# Replace FAQNumber header with the current FAQHook from config.
|
|
if ( $Header eq 'FAQNumber' ) {
|
|
$Header = $ConfigObject->Get('FAQ::FAQHook');
|
|
}
|
|
else {
|
|
$Header = $LayoutObject->{LanguageObject}->Translate($Header);
|
|
}
|
|
}
|
|
|
|
# Return CSV to download.
|
|
my $FileName = 'FAQ_search';
|
|
|
|
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
|
|
my $DateTime = $DateTimeObject->Get();
|
|
my $Y = $DateTime->{Year};
|
|
my $M = sprintf( "%02d", $DateTime->{Month} );
|
|
my $D = sprintf( "%02d", $DateTime->{Day} );
|
|
my $h = sprintf( "%02d", $DateTime->{Hour} );
|
|
my $m = sprintf( "%02d", $DateTime->{Minute} );
|
|
|
|
my $CSVObject = $Kernel::OM->Get('Kernel::System::CSV');
|
|
|
|
# Generate CSV output.
|
|
if ( $GetParam{ResultForm} eq 'CSV' ) {
|
|
|
|
my $CSV = $CSVObject->Array2CSV(
|
|
Head => \@CSVHead,
|
|
Data => \@CSVData,
|
|
Separator => $UserCSVSeparator,
|
|
);
|
|
|
|
# Return CSV to download,
|
|
return $LayoutObject->Attachment(
|
|
Filename => $FileName . "_" . "$Y-$M-$D" . "_" . "$h-$m.csv",
|
|
ContentType => "text/csv; charset=" . $LayoutObject->{UserCharset},
|
|
Content => $CSV,
|
|
);
|
|
}
|
|
|
|
# Generate Excel output.
|
|
elsif ( $GetParam{ResultForm} eq 'Excel' ) {
|
|
my $Excel = $CSVObject->Array2CSV(
|
|
Head => \@CSVHead,
|
|
Data => \@CSVData,
|
|
Format => 'Excel',
|
|
);
|
|
|
|
# Return Excel to download.
|
|
return $LayoutObject->Attachment(
|
|
Filename => $FileName . "_" . "$Y-$M-$D" . "_" . "$h-$m.xlsx",
|
|
ContentType =>
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
Content => $Excel,
|
|
);
|
|
}
|
|
}
|
|
elsif ( $GetParam{ResultForm} eq 'Print' ) {
|
|
|
|
my $PDFObject = $Kernel::OM->Get('Kernel::System::PDF');
|
|
|
|
my @PDFData;
|
|
for my $ItemID (@ViewableItemIDs) {
|
|
|
|
my %FAQData = $FAQObject->FAQGet(
|
|
ItemID => $ItemID,
|
|
ItemFields => 0,
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
# Set change date to long format.
|
|
my $Changed = $LayoutObject->{LanguageObject}->FormatTimeString(
|
|
$FAQData{Changed},
|
|
'DateFormatLong',
|
|
);
|
|
|
|
# Create PDF Rows.
|
|
my @PDFRow;
|
|
push @PDFRow, $FAQData{Number};
|
|
push @PDFRow, $FAQData{Title};
|
|
push @PDFRow, $FAQData{CategoryName};
|
|
|
|
# Create language row.
|
|
if ($MultiLanguage) {
|
|
push @PDFRow, $FAQData{Language};
|
|
}
|
|
|
|
push @PDFRow, $FAQData{State};
|
|
push @PDFRow, $Changed;
|
|
push @PDFData, \@PDFRow;
|
|
}
|
|
|
|
# PDF Output.
|
|
my $Title = $LayoutObject->{LanguageObject}->Translate('FAQ') . ' '
|
|
. $LayoutObject->{LanguageObject}->Translate('Search');
|
|
my $PrintedBy = $LayoutObject->{LanguageObject}->Translate('printed by');
|
|
my $Page = $LayoutObject->{LanguageObject}->Translate('Page');
|
|
my $Time = $LayoutObject->{Time};
|
|
|
|
# Get maximum number of pages.
|
|
my $MaxPages = $ConfigObject->Get('PDF::MaxPages');
|
|
if ( !$MaxPages || $MaxPages < 1 || $MaxPages > 1000 ) {
|
|
$MaxPages = 100;
|
|
}
|
|
|
|
# Create the header.
|
|
my $CellData;
|
|
|
|
# Output 'No Result', if no content was given.
|
|
if (@PDFData) {
|
|
|
|
$CellData->[0]->[0]->{Content} = $ConfigObject->Get('FAQ::FAQHook');
|
|
$CellData->[0]->[0]->{Font} = 'ProportionalBold';
|
|
$CellData->[0]->[1]->{Content} = $LayoutObject->{LanguageObject}->Translate('Title');
|
|
$CellData->[0]->[1]->{Font} = 'ProportionalBold';
|
|
$CellData->[0]->[2]->{Content} = $LayoutObject->{LanguageObject}->Translate('Category');
|
|
$CellData->[0]->[2]->{Font} = 'ProportionalBold';
|
|
|
|
# Store the correct header index.
|
|
my $NextHeaderIndex = 3;
|
|
|
|
# Add language header.
|
|
if ($MultiLanguage) {
|
|
$CellData->[0]->[3]->{Content} = $LayoutObject->{LanguageObject}->Translate('Language');
|
|
$CellData->[0]->[3]->{Font} = 'ProportionalBold';
|
|
$NextHeaderIndex = 4;
|
|
}
|
|
|
|
$CellData->[0]->[$NextHeaderIndex]->{Content} = $LayoutObject->{LanguageObject}->Translate('State');
|
|
$CellData->[0]->[$NextHeaderIndex]->{Font} = 'ProportionalBold';
|
|
|
|
$CellData->[0]->[ $NextHeaderIndex + 1 ]->{Content}
|
|
= $LayoutObject->{LanguageObject}->Translate('Changed');
|
|
$CellData->[0]->[ $NextHeaderIndex + 1 ]->{Font} = 'ProportionalBold';
|
|
|
|
# Create the content array.
|
|
my $CounterRow = 1;
|
|
for my $Row (@PDFData) {
|
|
my $CounterColumn = 0;
|
|
for my $Content ( @{$Row} ) {
|
|
$CellData->[$CounterRow]->[$CounterColumn]->{Content} = $Content;
|
|
$CounterColumn++;
|
|
}
|
|
$CounterRow++;
|
|
}
|
|
}
|
|
else {
|
|
$CellData->[0]->[0]->{Content} = $LayoutObject->{LanguageObject}->Translate('No Result!');
|
|
|
|
}
|
|
|
|
# Page params.
|
|
my %PageParam;
|
|
$PageParam{PageOrientation} = 'landscape';
|
|
$PageParam{MarginTop} = 30;
|
|
$PageParam{MarginRight} = 40;
|
|
$PageParam{MarginBottom} = 40;
|
|
$PageParam{MarginLeft} = 40;
|
|
$PageParam{HeaderRight} = $Title;
|
|
$PageParam{HeadlineLeft} = $Title;
|
|
|
|
# Table params.
|
|
my %TableParam;
|
|
$TableParam{CellData} = $CellData;
|
|
$TableParam{Type} = 'Cut';
|
|
$TableParam{FontSize} = 6;
|
|
$TableParam{Border} = 0;
|
|
$TableParam{BackgroundColorEven} = '#DDDDDD';
|
|
$TableParam{Padding} = 1;
|
|
$TableParam{PaddingTop} = 3;
|
|
$TableParam{PaddingBottom} = 3;
|
|
|
|
# Create new PDF document.
|
|
$PDFObject->DocumentNew(
|
|
Title => $ConfigObject->Get('Product') . ': ' . $Title,
|
|
Encode => $LayoutObject->{UserCharset},
|
|
);
|
|
|
|
# Start table output.
|
|
$PDFObject->PageNew(
|
|
%PageParam,
|
|
FooterRight => $Page . ' 1',
|
|
);
|
|
|
|
$PDFObject->PositionSet(
|
|
Move => 'relativ',
|
|
Y => -6,
|
|
);
|
|
|
|
# Output title.
|
|
$PDFObject->Text(
|
|
Text => $Title,
|
|
FontSize => 13,
|
|
);
|
|
|
|
$PDFObject->PositionSet(
|
|
Move => 'relativ',
|
|
Y => -6,
|
|
);
|
|
|
|
# Output "printed by".
|
|
$PDFObject->Text(
|
|
Text => $PrintedBy . ' '
|
|
. $Self->{UserFullname} . ' ('
|
|
. $Self->{UserEmail} . ')'
|
|
. ', ' . $Time,
|
|
FontSize => 9,
|
|
);
|
|
|
|
$PDFObject->PositionSet(
|
|
Move => 'relativ',
|
|
Y => -14,
|
|
);
|
|
|
|
PAGE:
|
|
for ( 2 .. $MaxPages ) {
|
|
|
|
# Output table (or a fragment of it).
|
|
%TableParam = $PDFObject->Table( %TableParam, );
|
|
|
|
# Stop output or another page.
|
|
if ( $TableParam{State} ) {
|
|
last PAGE;
|
|
}
|
|
else {
|
|
$PDFObject->PageNew(
|
|
%PageParam,
|
|
FooterRight => $Page . ' ' . $_,
|
|
);
|
|
}
|
|
}
|
|
|
|
# Return the PDF document.
|
|
my $Filename = 'FAQ_search';
|
|
|
|
my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');
|
|
my $DateTime = $DateTimeObject->Get();
|
|
my $Y = $DateTime->{Year};
|
|
my $M = sprintf( "%02d", $DateTime->{Month} );
|
|
my $D = sprintf( "%02d", $DateTime->{Day} );
|
|
my $h = sprintf( "%02d", $DateTime->{Hour} );
|
|
my $m = sprintf( "%02d", $DateTime->{Minute} );
|
|
|
|
my $PDFString = $PDFObject->DocumentOutput();
|
|
return $LayoutObject->Attachment(
|
|
Filename => $Filename . "_" . "$Y-$M-$D" . "_" . "$h-$m.pdf",
|
|
ContentType => "application/pdf",
|
|
Content => $PDFString,
|
|
Type => 'inline',
|
|
);
|
|
}
|
|
else {
|
|
|
|
# Start HTML page.
|
|
my $Output = $LayoutObject->Header();
|
|
$Output .= $LayoutObject->NavigationBar();
|
|
$LayoutObject->Print(
|
|
Output => \$Output,
|
|
);
|
|
$Output = '';
|
|
|
|
my $Filter = $ParamObject->GetParam( Param => 'Filter' ) || '';
|
|
my $View = $ParamObject->GetParam( Param => 'View' ) || '';
|
|
|
|
# Show FAQ's.
|
|
my $LinkPage = 'Filter='
|
|
. $LayoutObject->LinkEncode($Filter)
|
|
. ';View=' . $LayoutObject->LinkEncode($View)
|
|
. ';SortBy=' . $LayoutObject->LinkEncode($SortBy)
|
|
. ';OrderBy=' . $LayoutObject->LinkEncode($OrderBy)
|
|
. ';Profile=' . $LayoutObject->LinkEncode($Profile) . ';TakeLastSearch=1;Subaction=Search'
|
|
. ';';
|
|
my $LinkSort = 'Filter='
|
|
. $LayoutObject->LinkEncode($Filter)
|
|
. ';View=' . $LayoutObject->LinkEncode($View)
|
|
. ';Profile=' . $LayoutObject->LinkEncode($Profile) . ';TakeLastSearch=1;Subaction=Search'
|
|
. ';';
|
|
my $LinkFilter = 'TakeLastSearch=1;Subaction=Search;Profile='
|
|
. $LayoutObject->LinkEncode($Profile)
|
|
. ';';
|
|
my $LinkBack = 'Subaction=LoadProfile;Profile='
|
|
. $LayoutObject->LinkEncode($Profile)
|
|
. ';TakeLastSearch=1;';
|
|
|
|
my $FilterLink = 'SortBy=' . $LayoutObject->LinkEncode($SortBy)
|
|
. ';OrderBy=' . $LayoutObject->LinkEncode($OrderBy)
|
|
. ';View=' . $LayoutObject->LinkEncode($View)
|
|
. ';Profile=' . $LayoutObject->LinkEncode($Profile) . ';TakeLastSearch=1;Subaction=Search'
|
|
. ';';
|
|
|
|
# Find out which columns should be shown.
|
|
my @ShowColumns;
|
|
if ( $Config->{ShowColumns} ) {
|
|
|
|
# Get all possible columns from config.
|
|
my %PossibleColumn = %{ $Config->{ShowColumns} };
|
|
|
|
# Get the column names that should be shown.
|
|
COLUMNNAME:
|
|
for my $Name ( sort keys %PossibleColumn ) {
|
|
next COLUMNNAME if !$PossibleColumn{$Name};
|
|
push @ShowColumns, $Name;
|
|
}
|
|
|
|
# Enforce FAQ number column since is the link MasterAction hook.
|
|
if ( !$PossibleColumn{'Number'} ) {
|
|
push @ShowColumns, 'Number';
|
|
}
|
|
}
|
|
|
|
$Output .= $LayoutObject->FAQListShow(
|
|
FAQIDs => \@ViewableItemIDs,
|
|
Total => scalar @ViewableItemIDs,
|
|
|
|
View => $View,
|
|
|
|
Env => $Self,
|
|
LinkPage => $LinkPage,
|
|
LinkSort => $LinkSort,
|
|
LinkFilter => $LinkFilter,
|
|
LinkBack => $LinkBack,
|
|
Profile => $Profile,
|
|
|
|
TitleName => Translatable('Search Result'),
|
|
Limit => $SearchLimit,
|
|
|
|
Filter => $Filter,
|
|
FilterLink => $FilterLink,
|
|
|
|
OrderBy => $OrderBy,
|
|
SortBy => $SortBy,
|
|
|
|
ShowColumns => \@ShowColumns,
|
|
FAQTitleSize => $Config->{TitleSize},
|
|
);
|
|
|
|
$Output .= $LayoutObject->Footer();
|
|
return $Output;
|
|
}
|
|
}
|
|
|
|
elsif ( $Self->{Subaction} eq 'AJAXProfileDelete' ) {
|
|
my $Profile = $ParamObject->GetParam( Param => 'Profile' );
|
|
|
|
# Remove old profile stuff.
|
|
$SearchProfileObject->SearchProfileDelete(
|
|
Base => 'FAQSearch',
|
|
Name => $Profile,
|
|
UserLogin => $Self->{UserLogin},
|
|
);
|
|
my $Output = $LayoutObject->JSONEncode(
|
|
Data => 1,
|
|
);
|
|
return $LayoutObject->Attachment(
|
|
NoCache => 1,
|
|
ContentType => 'text/html',
|
|
Content => $Output,
|
|
Type => 'inline',
|
|
);
|
|
}
|
|
|
|
elsif ( $Self->{Subaction} eq 'AJAX' ) {
|
|
|
|
my $Output = $Self->_MaskForm(
|
|
%GetParam,
|
|
);
|
|
|
|
return $LayoutObject->Attachment(
|
|
NoCache => 1,
|
|
ContentType => 'text/html',
|
|
Charset => $LayoutObject->{UserCharset},
|
|
Content => $Output,
|
|
Type => 'inline',
|
|
);
|
|
}
|
|
|
|
# Show default search screen.
|
|
$Output = $LayoutObject->Header();
|
|
$Output .= $LayoutObject->NavigationBar();
|
|
|
|
# Send data to JS.
|
|
$LayoutObject->AddJSData(
|
|
Key => 'AgentFAQSearch',
|
|
Value => 1,
|
|
);
|
|
|
|
$Output .= $LayoutObject->Output(
|
|
TemplateFile => 'AgentFAQSearch',
|
|
Data => \%Param,
|
|
);
|
|
$Output .= $LayoutObject->Footer();
|
|
return $Output;
|
|
|
|
}
|
|
|
|
sub _MaskForm {
|
|
my ( $Self, %Param ) = @_;
|
|
|
|
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
|
|
|
|
my $TreeView = 0;
|
|
if ( $ConfigObject->Get('Ticket::Frontend::ListType') eq 'tree' ) {
|
|
$TreeView = 1;
|
|
}
|
|
|
|
my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
|
|
|
|
my $Profile = $ParamObject->GetParam( Param => 'Profile' ) || '';
|
|
my $EmptySearch = $ParamObject->GetParam( Param => 'EmptySearch' );
|
|
if ( !$Profile ) {
|
|
$EmptySearch = 1;
|
|
}
|
|
|
|
my $SearchProfileObject = $Kernel::OM->Get('Kernel::System::SearchProfile');
|
|
|
|
my %GetParam = $SearchProfileObject->SearchProfileGet(
|
|
Base => 'FAQSearch',
|
|
Name => $Profile,
|
|
UserLogin => $Self->{UserLogin},
|
|
);
|
|
|
|
# Get config from constructor.
|
|
my $Config = $Self->{Config};
|
|
|
|
# If no profile is used, set default params of default attributes.
|
|
if ( !$Profile ) {
|
|
if ( $Config->{Defaults} ) {
|
|
ATTRIBUTE:
|
|
for my $Attribute ( sort keys %{ $Config->{Defaults} } ) {
|
|
next ATTRIBUTE if !$Config->{Defaults}->{$Attribute};
|
|
next ATTRIBUTE if $Attribute eq 'DynamicField';
|
|
$GetParam{$Attribute} = $Config->{Defaults}->{$Attribute};
|
|
}
|
|
}
|
|
}
|
|
|
|
# Set attributes string.
|
|
my @Attributes = (
|
|
{
|
|
Key => 'Number',
|
|
Value => Translatable('FAQ Number'),
|
|
},
|
|
{
|
|
Key => 'Fulltext',
|
|
Value => Translatable('Fulltext'),
|
|
},
|
|
{
|
|
Key => 'Title',
|
|
Value => Translatable('Title'),
|
|
},
|
|
{
|
|
Key => 'Keyword',
|
|
Value => Translatable('Keyword'),
|
|
},
|
|
);
|
|
|
|
# Show Languages attribute.
|
|
my $MultiLanguage = $ConfigObject->Get('FAQ::MultiLanguage');
|
|
if ($MultiLanguage) {
|
|
push @Attributes, (
|
|
{
|
|
Key => 'LanguageIDs',
|
|
Value => Translatable('Language'),
|
|
},
|
|
);
|
|
}
|
|
|
|
push @Attributes, (
|
|
{
|
|
Key => 'CategoryIDs',
|
|
Value => Translatable('Category'),
|
|
},
|
|
{
|
|
Key => 'ValidIDs',
|
|
Value => Translatable('Validity'),
|
|
},
|
|
{
|
|
Key => 'StateIDs',
|
|
Value => Translatable('State'),
|
|
},
|
|
{
|
|
Key => 'VoteSearchType',
|
|
Value => Translatable('Votes'),
|
|
},
|
|
{
|
|
Key => 'RateSearchType',
|
|
Value => Translatable('Rate'),
|
|
},
|
|
{
|
|
Key => 'ApprovedSearch',
|
|
Value => Translatable('Approved'),
|
|
},
|
|
{
|
|
Key => 'CreatedUserIDs',
|
|
Value => Translatable('Created by'),
|
|
},
|
|
{
|
|
Key => 'LastChangedUserIDs',
|
|
Value => Translatable('Last Changed by'),
|
|
},
|
|
{
|
|
Key => 'ItemCreateTimePoint',
|
|
Value => Translatable('FAQ Item Create Time (before/after)'),
|
|
},
|
|
{
|
|
Key => 'ItemCreateTimeSlot',
|
|
Value => Translatable('FAQ Item Create Time (between)'),
|
|
},
|
|
{
|
|
Key => 'ItemChangeTimePoint',
|
|
Value => Translatable('FAQ Item Change Time (before/after)'),
|
|
},
|
|
{
|
|
Key => 'ItemChangeTimeSlot',
|
|
Value => Translatable('FAQ Item Change Time (between)'),
|
|
},
|
|
);
|
|
|
|
my $DynamicFieldSeparator = 1;
|
|
|
|
my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
|
|
my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
|
|
|
|
# Create dynamic fields search options for attribute select.
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
next DYNAMICFIELD if !$DynamicFieldConfig->{Name};
|
|
next DYNAMICFIELD if $DynamicFieldConfig->{Name} eq '';
|
|
|
|
# Create a separator for dynamic fields attributes.
|
|
if ($DynamicFieldSeparator) {
|
|
push @Attributes, (
|
|
{
|
|
Key => '',
|
|
Value => '-',
|
|
Disabled => 1,
|
|
},
|
|
);
|
|
|
|
$DynamicFieldSeparator = 0;
|
|
}
|
|
|
|
my $SearchFieldPreferences = $DynamicFieldBackendObject->SearchFieldPreferences(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
);
|
|
|
|
next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences);
|
|
|
|
my $TranslatedDynamicFieldLabel = $LayoutObject->{LanguageObject}->Translate(
|
|
$DynamicFieldConfig->{Label},
|
|
);
|
|
|
|
PREFERENCE:
|
|
for my $Preference ( @{$SearchFieldPreferences} ) {
|
|
|
|
my $TranslatedSuffix = $LayoutObject->{LanguageObject}->Translate(
|
|
$Preference->{LabelSuffix},
|
|
) || '';
|
|
|
|
if ($TranslatedSuffix) {
|
|
$TranslatedSuffix = ' (' . $TranslatedSuffix . ')';
|
|
}
|
|
|
|
push @Attributes, (
|
|
{
|
|
Key => 'Search_DynamicField_'
|
|
. $DynamicFieldConfig->{Name}
|
|
. $Preference->{Type},
|
|
Value => $TranslatedDynamicFieldLabel . $TranslatedSuffix,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
# Create a separator if a dynamic field attribute was pushed.
|
|
if ( !$DynamicFieldSeparator ) {
|
|
push @Attributes, (
|
|
{
|
|
Key => '',
|
|
Value => '-',
|
|
Disabled => 1,
|
|
},
|
|
);
|
|
}
|
|
|
|
# Create HTML strings for all dynamic fields.
|
|
my %DynamicFieldHTML;
|
|
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
|
|
my $SearchFieldPreferences = $DynamicFieldBackendObject->SearchFieldPreferences(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
);
|
|
|
|
next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences);
|
|
|
|
PREFERENCE:
|
|
for my $Preference ( @{$SearchFieldPreferences} ) {
|
|
|
|
# Get field HTML.
|
|
$DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} }
|
|
= $DynamicFieldBackendObject->SearchFieldRender(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
Profile => \%GetParam,
|
|
DefaultValue =>
|
|
$Config->{Defaults}->{DynamicField}
|
|
->{ $DynamicFieldConfig->{Name} },
|
|
LayoutObject => $LayoutObject,
|
|
Type => $Preference->{Type},
|
|
);
|
|
}
|
|
}
|
|
|
|
# Drop-down menu for 'attributes'.
|
|
$Param{AttributesStrg} = $LayoutObject->BuildSelection(
|
|
PossibleNone => 1,
|
|
Data => \@Attributes,
|
|
Name => 'Attribute',
|
|
Multiple => 0,
|
|
Class => 'Modernize',
|
|
);
|
|
$Param{AttributesOrigStrg} = $LayoutObject->BuildSelection(
|
|
PossibleNone => 1,
|
|
Data => \@Attributes,
|
|
Name => 'AttributeOrig',
|
|
Multiple => 0,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
my $FAQObject = $Kernel::OM->Get('Kernel::System::FAQ');
|
|
|
|
my %Languages = $FAQObject->LanguageList(
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
# Drop-down menu for 'languages'.
|
|
$Param{LanguagesSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%Languages,
|
|
Name => 'LanguageIDs',
|
|
Size => 5,
|
|
Multiple => 1,
|
|
SelectedID => $GetParam{LanguageIDs} || [],
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
# Get categories (with category long names) where user has rights.
|
|
my $UserCategoriesLongNames = $FAQObject->GetUserCategoriesLongNames(
|
|
Type => 'ro',
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
# Build the category selection.
|
|
$Param{CategoriesSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => $UserCategoriesLongNames,
|
|
Name => 'CategoryIDs',
|
|
Size => 5,
|
|
SelectedID => $GetParam{CategoryIDs} || [],
|
|
Translation => 0,
|
|
Multiple => 1,
|
|
TreeView => $TreeView,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
my %ValidList = $Kernel::OM->Get('Kernel::System::Valid')->ValidList();
|
|
|
|
# Build the valid selection.
|
|
$Param{ValidSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%ValidList,
|
|
Name => 'ValidIDs',
|
|
Size => 5,
|
|
SelectedID => $GetParam{ValidIDs} || [],
|
|
Translation => 0,
|
|
Multiple => 1,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
# Create a mix of state and state types hash in order to have the state type IDs with state names.
|
|
my %StateList = $FAQObject->StateList(
|
|
UserID => $Self->{UserID},
|
|
);
|
|
|
|
my %States;
|
|
for my $StateID ( sort keys %StateList ) {
|
|
my %StateData = $FAQObject->StateGet(
|
|
StateID => $StateID,
|
|
UserID => $Self->{UserID},
|
|
);
|
|
$States{ $StateData{TypeID} } = $StateData{Name};
|
|
}
|
|
|
|
$Param{StateSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%States,
|
|
Name => 'StateIDs',
|
|
Size => 3,
|
|
SelectedID => $GetParam{StateIDs} || [],
|
|
Translation => 1,
|
|
Multiple => 1,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
my %VotingOperators = (
|
|
Equals => Translatable('Equals'),
|
|
GreaterThan => Translatable('Greater than'),
|
|
GreaterThanEquals => Translatable('Greater than equals'),
|
|
SmallerThan => Translatable('Smaller than'),
|
|
SmallerThanEquals => Translatable('Smaller than equals'),
|
|
);
|
|
|
|
$Param{VoteSearchTypeSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%VotingOperators,
|
|
Name => 'VoteSearchType',
|
|
Size => 1,
|
|
SelectedID => $GetParam{VoteSearchType} || '',
|
|
Translation => 1,
|
|
Multiple => 0,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
$Param{RateSearchTypeSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%VotingOperators,
|
|
Name => 'RateSearchType',
|
|
Size => 1,
|
|
SelectedID => $GetParam{RateSearchType} || '',
|
|
Translation => 1,
|
|
Multiple => 0,
|
|
Class => 'Modernize',
|
|
);
|
|
$Param{RateSearchSelectionStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
0 => '0%',
|
|
25 => '25%',
|
|
50 => '50%',
|
|
75 => '75%',
|
|
100 => '100%',
|
|
},
|
|
Sort => 'NumericKey',
|
|
Name => 'RateSearch',
|
|
Size => 1,
|
|
SelectedID => $GetParam{RateSearch} || '',
|
|
Translation => 0,
|
|
Multiple => 0,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
$Param{ApprovedStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
No => Translatable('No'),
|
|
Yes => Translatable('Yes'),
|
|
},
|
|
Name => 'ApprovedSearch',
|
|
SelectedID => $GetParam{ApprovedSearch} || 'Yes',
|
|
Multiple => 0,
|
|
Translation => 1,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
# Get a list of all users to display.
|
|
my %ShownUsers = $Kernel::OM->Get('Kernel::System::User')->UserList(
|
|
Type => 'Long',
|
|
Valid => 1,
|
|
);
|
|
|
|
my $FrontendConfig = $ConfigObject->Get('Frontend::Module');
|
|
my $FAQAddGroups = $FrontendConfig->{AgentFAQAdd}->{Group} || [];
|
|
|
|
my %FAQAddUsers = %ShownUsers;
|
|
if ( IsArrayRefWithData($FAQAddGroups) ) {
|
|
|
|
my %GroupUsers;
|
|
for my $Group ( @{$FAQAddGroups} ) {
|
|
|
|
my $GroupObject = $Kernel::OM->Get('Kernel::System::Group');
|
|
|
|
my $GroupID = $GroupObject->GroupLookup( Group => $Group );
|
|
my %Users = $GroupObject->GroupMemberList(
|
|
GroupID => $GroupID,
|
|
Type => 'rw',
|
|
Result => 'HASH',
|
|
);
|
|
%GroupUsers = ( %GroupUsers, %Users );
|
|
}
|
|
|
|
# Remove all users that are not in the FAQ or faq_admin groups.
|
|
for my $UserID ( sort keys %FAQAddUsers ) {
|
|
if ( !$GroupUsers{$UserID} ) {
|
|
delete $FAQAddUsers{$UserID};
|
|
}
|
|
}
|
|
}
|
|
$Param{CreatedUserStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%FAQAddUsers,
|
|
Name => 'CreatedUserIDs',
|
|
Size => 5,
|
|
Multiple => 1,
|
|
SelectedID => $GetParam{CreatedUserIDs},
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
my $FAQEditGroups = $FrontendConfig->{AgentFAQEdit}->{Group} || [];
|
|
|
|
my %FAQEditUsers = %ShownUsers;
|
|
if ( IsArrayRefWithData($FAQEditGroups) ) {
|
|
|
|
my %GroupUsers;
|
|
for my $Group ( @{$FAQEditGroups} ) {
|
|
|
|
my $GroupObject = $Kernel::OM->Get('Kernel::System::Group');
|
|
|
|
my $GroupID = $GroupObject->GroupLookup( Group => $Group );
|
|
my %Users = $GroupObject->GroupMemberList(
|
|
GroupID => $GroupID,
|
|
Type => 'rw',
|
|
Result => 'HASH',
|
|
);
|
|
%GroupUsers = ( %GroupUsers, %Users );
|
|
}
|
|
|
|
# Remove all users that are not in the FAQ or faq_admin groups.
|
|
for my $UserID ( sort keys %FAQEditUsers ) {
|
|
if ( !$GroupUsers{$UserID} ) {
|
|
delete $FAQEditUsers{$UserID};
|
|
}
|
|
}
|
|
}
|
|
|
|
$Param{LastChangedUserStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%FAQEditUsers,
|
|
Name => 'LastChangedUserIDs',
|
|
Size => 5,
|
|
Multiple => 1,
|
|
SelectedID => $GetParam{LastChangedUserIDs},
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
$Param{ItemCreateTimePointStrg} = $LayoutObject->BuildSelection(
|
|
Data => [ 1 .. 59 ],
|
|
Name => 'ItemCreateTimePoint',
|
|
SelectedID => $GetParam{ItemCreateTimePoint},
|
|
);
|
|
$Param{ItemCreateTimePointStartStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
'Last' => Translatable('within the last ...'),
|
|
'Before' => Translatable('more than ... ago'),
|
|
},
|
|
Name => 'ItemCreateTimePointStart',
|
|
SelectedID => $GetParam{ItemCreateTimePointStart} || 'Last',
|
|
);
|
|
$Param{ItemCreateTimePointFormatStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
minute => Translatable('minute(s)'),
|
|
hour => Translatable('hour(s)'),
|
|
day => Translatable('day(s)'),
|
|
week => Translatable('week(s)'),
|
|
month => Translatable('month(s)'),
|
|
year => Translatable('year(s)'),
|
|
},
|
|
Name => 'ItemCreateTimePointFormat',
|
|
SelectedID => $GetParam{ItemCreateTimePointFormat},
|
|
);
|
|
$Param{ItemCreateTimeStartStrg} = $LayoutObject->BuildDateSelection(
|
|
%GetParam,
|
|
Prefix => 'ItemCreateTimeStart',
|
|
Format => 'DateInputFormat',
|
|
DiffTime => -( ( 60 * 60 * 24 ) * 30 ),
|
|
);
|
|
$Param{ItemCreateTimeStopStrg} = $LayoutObject->BuildDateSelection(
|
|
%GetParam,
|
|
Prefix => 'ItemCreateTimeStop',
|
|
Format => 'DateInputFormat',
|
|
);
|
|
|
|
$Param{ItemChangeTimePointStrg} = $LayoutObject->BuildSelection(
|
|
Data => [ 1 .. 59 ],
|
|
Name => 'ItemChangeTimePoint',
|
|
SelectedID => $GetParam{ItemChangeTimePoint},
|
|
);
|
|
$Param{ItemChangeTimePointStartStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
'Last' => Translatable('within the last ...'),
|
|
'Before' => Translatable('more than ... ago'),
|
|
},
|
|
Name => 'ItemChangeTimePointStart',
|
|
SelectedID => $GetParam{ItemChangeTimePointStart} || 'Last',
|
|
);
|
|
$Param{ItemChangeTimePointFormatStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
minute => Translatable('minute(s)'),
|
|
hour => Translatable('hour(s)'),
|
|
day => Translatable('day(s)'),
|
|
week => Translatable('week(s)'),
|
|
month => Translatable('month(s)'),
|
|
year => Translatable('year(s)'),
|
|
},
|
|
Name => 'ItemChangeTimePointFormat',
|
|
SelectedID => $GetParam{ItemChangeTimePointFormat},
|
|
);
|
|
$Param{ItemChangeTimeStartStrg} = $LayoutObject->BuildDateSelection(
|
|
%GetParam,
|
|
Prefix => 'ItemChangeTimeStart',
|
|
Format => 'DateInputFormat',
|
|
DiffTime => -( ( 60 * 60 * 24 ) * 30 ),
|
|
);
|
|
$Param{ItemChangeTimeStopStrg} = $LayoutObject->BuildDateSelection(
|
|
%GetParam,
|
|
Prefix => 'ItemChangeTimeStop',
|
|
Format => 'DateInputFormat',
|
|
);
|
|
|
|
my %Profiles = $SearchProfileObject->SearchProfileList(
|
|
Base => 'FAQSearch',
|
|
UserLogin => $Self->{UserLogin},
|
|
);
|
|
delete $Profiles{''};
|
|
delete $Profiles{'last-search'};
|
|
if ($EmptySearch) {
|
|
$Profiles{''} = '-';
|
|
}
|
|
else {
|
|
$Profiles{'last-search'} = '-';
|
|
}
|
|
$Param{ProfilesStrg} = $LayoutObject->BuildSelection(
|
|
Data => \%Profiles,
|
|
Name => 'Profile',
|
|
ID => 'SearchProfile',
|
|
SelectedID => $Profile,
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
$Param{ResultFormStrg} = $LayoutObject->BuildSelection(
|
|
Data => {
|
|
Normal => Translatable('Normal'),
|
|
Print => Translatable('Print'),
|
|
CSV => Translatable('CSV'),
|
|
Excel => Translatable('Excel'),
|
|
},
|
|
Name => 'ResultForm',
|
|
SelectedID => $GetParam{ResultForm} || 'Normal',
|
|
Class => 'Modernize',
|
|
);
|
|
|
|
$LayoutObject->Block(
|
|
Name => 'SearchAJAX',
|
|
Data => {
|
|
%Param,
|
|
%GetParam,
|
|
EmptySearch => $EmptySearch,
|
|
},
|
|
);
|
|
|
|
# Output Dynamic fields blocks.
|
|
DYNAMICFIELD:
|
|
for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) {
|
|
next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
|
|
|
|
# Get search field preferences.
|
|
my $SearchFieldPreferences = $DynamicFieldBackendObject->SearchFieldPreferences(
|
|
DynamicFieldConfig => $DynamicFieldConfig,
|
|
);
|
|
|
|
next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences);
|
|
|
|
PREFERENCE:
|
|
for my $Preference ( @{$SearchFieldPreferences} ) {
|
|
|
|
# skip fields that HTML could not be retrieved
|
|
next PREFERENCE if !IsHashRefWithData(
|
|
$DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} }
|
|
);
|
|
|
|
$LayoutObject->Block(
|
|
Name => 'DynamicField',
|
|
Data => {
|
|
Label =>
|
|
$DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} }
|
|
->{Label},
|
|
Field =>
|
|
$DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} }
|
|
->{Field},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
# Show attributes.
|
|
my @SearchAttributes;
|
|
my %AlreadyShown;
|
|
if ($Profile) {
|
|
$LayoutObject->AddJSData(
|
|
Key => 'FAQSearchProfile',
|
|
Value => $Profile,
|
|
);
|
|
}
|
|
|
|
ITEM:
|
|
for my $Item (@Attributes) {
|
|
my $Key = $Item->{Key};
|
|
next ITEM if !$Key;
|
|
next ITEM if !defined $GetParam{$Key};
|
|
next ITEM if $GetParam{$Key} eq '';
|
|
|
|
next ITEM if $AlreadyShown{$Key};
|
|
$AlreadyShown{$Key} = 1;
|
|
push @SearchAttributes, $Key;
|
|
}
|
|
|
|
# if no attribute is shown, show full-text search.
|
|
if ( !$Profile ) {
|
|
|
|
# Merge regular show/hide settings and the settings for the dynamic fields.
|
|
my %Defaults = %{ $Config->{Defaults} || {} };
|
|
|
|
delete $Defaults{DynamicField};
|
|
|
|
for my $DynamicFieldItem ( sort keys %{ $Config->{DynamicField} || {} } ) {
|
|
if ( $Config->{DynamicField}->{$DynamicFieldItem} == 2 ) {
|
|
$Defaults{"Search_DynamicField_$DynamicFieldItem"} = 1;
|
|
}
|
|
}
|
|
|
|
if (%Defaults) {
|
|
DEFAULT:
|
|
for my $Key ( sort keys %Defaults ) {
|
|
next DEFAULT if $Key eq 'DynamicField'; # ignore entry for DF config
|
|
next DEFAULT if $AlreadyShown{$Key};
|
|
$AlreadyShown{$Key} = 1;
|
|
push @SearchAttributes, $Key;
|
|
}
|
|
}
|
|
else {
|
|
|
|
# If no attribute is shown, show full-text search.
|
|
if ( !keys %AlreadyShown ) {
|
|
push @SearchAttributes, 'Fulltext';
|
|
}
|
|
}
|
|
}
|
|
|
|
$LayoutObject->AddJSData(
|
|
Key => 'SearchAttributes',
|
|
Value => \@SearchAttributes,
|
|
);
|
|
|
|
my $Output = $LayoutObject->Output(
|
|
TemplateFile => 'AgentFAQSearch',
|
|
Data => \%Param,
|
|
AJAX => 1,
|
|
);
|
|
return $Output;
|
|
}
|
|
|
|
1;
|