Bug 20750: Allow logging of arbitrary actions
This patch allows logging of arbitrary actions on request objects. A chronological summary of these actions can then be displayed when viewing a request. This patch also adds logging of request status changes. Additional work has been done on the BLDSS backend to log requests to the BLDSS request status check API. To test: - Apply patch - Ensure the Illlog logging preference is turned on - Create an ILL request and perform actions on it that change it's status. - Navigate to the "Manage ILL request" screen - Click the "Display request log" button - Observe that a modal opens displaying a summary of the status changes. Signed-off-by: Niamh Walker <Niamh.Walker-Headon@it-tallaght.ie> Signed-off-by: Josef Moravec <josef.moravec@gmail.com> Since this bug now is dependent on Bug 20581, it should be aware of the custom statuses provided in 20581. This patch enables logging of request statuses being changed to custom ones. As such the test plan is modified: To test: - Apply patch - Ensure the Illlog logging preference is turned on - Ensure you have some custom request statuses defined in the Authorised Values ILLSTATUS category - Create an ILL request and perform actions on it that change it's status. - Edit the request and change status to a custom one - Navigate to the "Manage ILL request" screen - Click the "Display request log" button - Observe that a modal opens displaying a summary of the status changes. Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com> Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
This commit is contained in:
parent
a13dff4b88
commit
79d64a37a0
|
|
@ -32,6 +32,7 @@ use Koha::Exceptions::Ill;
|
|||
use Koha::Illcomments;
|
||||
use Koha::Illrequestattributes;
|
||||
use Koha::AuthorisedValue;
|
||||
use Koha::Illrequest::Logger;
|
||||
use Koha::Patron;
|
||||
use Koha::AuthorisedValues;
|
||||
|
||||
|
|
@ -157,6 +158,16 @@ sub illcomments {
|
|||
);
|
||||
}
|
||||
|
||||
=head3 logs
|
||||
|
||||
=cut
|
||||
|
||||
sub logs {
|
||||
my ( $self ) = @_;
|
||||
my $logger = Koha::Illrequest::Logger->new;
|
||||
return $logger->get_request_logs($self);
|
||||
}
|
||||
|
||||
=head3 patron
|
||||
|
||||
=cut
|
||||
|
|
@ -169,15 +180,26 @@ sub patron {
|
|||
}
|
||||
|
||||
=head3 status_alias
|
||||
|
||||
$Illrequest->status_alias(143);
|
||||
|
||||
Overloaded getter/setter for status_alias,
|
||||
that only returns authorised values from the
|
||||
correct category
|
||||
correct category and records the fact that the status has changed
|
||||
|
||||
=cut
|
||||
|
||||
sub status_alias {
|
||||
my ($self, $newval) = @_;
|
||||
if ($newval) {
|
||||
my ($self, $new_status_alias) = @_;
|
||||
|
||||
my $current_status_alias = $self->SUPER::status_alias;
|
||||
|
||||
if ($new_status_alias) {
|
||||
# Keep a record of the previous status before we change it,
|
||||
# we might need it
|
||||
$self->{previous_status} = $current_status_alias ?
|
||||
$current_status_alias :
|
||||
scalar $self->status;
|
||||
# This is hackery to enable us to undefine
|
||||
# status_alias, since we need to have an overloaded
|
||||
# status_alias method to get us around the problem described
|
||||
|
|
@ -185,13 +207,19 @@ sub status_alias {
|
|||
# https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
|
||||
# We need a way of accepting implied undef, so we can nullify
|
||||
# the status_alias column, when called from $self->status
|
||||
my $val = $newval eq "-1" ? undef : $newval;
|
||||
my $newval = $self->SUPER::status_alias($val);
|
||||
if ($newval) {
|
||||
return $newval;
|
||||
my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
|
||||
my $ret = $self->SUPER::status_alias($val);
|
||||
my $val_to_log = $val ? $new_status_alias : scalar $self->status;
|
||||
if ($ret) {
|
||||
my $logger = Koha::Illrequest::Logger->new;
|
||||
$logger->log_status_change({
|
||||
request => $self,
|
||||
value => $val_to_log
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
delete $self->{previous_status};
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
# We can't know which result is the right one if there are multiple
|
||||
# ILLSTATUS authorised values with the same authorised_value column value
|
||||
|
|
@ -210,25 +238,47 @@ sub status_alias {
|
|||
|
||||
=head3 status
|
||||
|
||||
$Illrequest->status('CANREQ');
|
||||
|
||||
Overloaded getter/setter for request status,
|
||||
also nullifies status_alias
|
||||
also nullifies status_alias and records the fact that the status has changed
|
||||
|
||||
=cut
|
||||
|
||||
sub status {
|
||||
my ( $self, $newval) = @_;
|
||||
if ($newval) {
|
||||
# This is hackery to enable us to undefine
|
||||
# status_alias, since we need to have an overloaded
|
||||
# status_alias method to get us around the problem described
|
||||
# here:
|
||||
# https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
|
||||
# We need a way of passing implied undef to nullify status_alias
|
||||
# so we pass -1, which is special cased in the overloaded setter
|
||||
$self->status_alias("-1");
|
||||
return $self->SUPER::status($newval);
|
||||
my ( $self, $new_status) = @_;
|
||||
|
||||
my $current_status = $self->SUPER::status;
|
||||
my $current_status_alias = $self->SUPER::status_alias;
|
||||
|
||||
if ($new_status) {
|
||||
# Keep a record of the previous status before we change it,
|
||||
# we might need it
|
||||
$self->{previous_status} = $current_status_alias ?
|
||||
$current_status_alias :
|
||||
$current_status;
|
||||
my $ret = $self->SUPER::status($new_status)->store;
|
||||
if ($current_status_alias) {
|
||||
# This is hackery to enable us to undefine
|
||||
# status_alias, since we need to have an overloaded
|
||||
# status_alias method to get us around the problem described
|
||||
# here:
|
||||
# https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
|
||||
# We need a way of passing implied undef to nullify status_alias
|
||||
# so we pass -1, which is special cased in the overloaded setter
|
||||
$self->status_alias("-1");
|
||||
} else {
|
||||
my $logger = Koha::Illrequest::Logger->new;
|
||||
$logger->log_status_change({
|
||||
request => $self,
|
||||
value => $new_status
|
||||
});
|
||||
}
|
||||
delete $self->{previous_status};
|
||||
return $ret;
|
||||
} else {
|
||||
return $current_status;
|
||||
}
|
||||
return $self->SUPER::status;
|
||||
}
|
||||
|
||||
=head3 load_backend
|
||||
|
|
@ -252,7 +302,10 @@ sub load_backend {
|
|||
my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
|
||||
my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
|
||||
require $location;
|
||||
$self->{_my_backend} = $backend_class->new({ config => $self->_config });
|
||||
$self->{_my_backend} = $backend_class->new({
|
||||
config => $self->_config,
|
||||
logger => Koha::Illrequest::Logger->new
|
||||
});
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
|
@ -1107,6 +1160,33 @@ sub _censor {
|
|||
return $params;
|
||||
}
|
||||
|
||||
=head3 store
|
||||
|
||||
$Illrequest->store;
|
||||
|
||||
Overloaded I<store> method that, in addition to performing the 'store',
|
||||
possibly records the fact that something happened
|
||||
|
||||
=cut
|
||||
|
||||
sub store {
|
||||
my ( $self, $attrs ) = @_;
|
||||
|
||||
my $ret = $self->SUPER::store;
|
||||
|
||||
$attrs->{log_origin} = 'core';
|
||||
|
||||
if ($ret && defined $attrs) {
|
||||
my $logger = Koha::Illrequest::Logger->new;
|
||||
$logger->log_maybe({
|
||||
request => $self,
|
||||
attrs => $attrs
|
||||
});
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
=head3 TO_JSON
|
||||
|
||||
$json = $illrequest->TO_JSON
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
package Koha::Illrequest::Logger;
|
||||
|
||||
# Copyright 2018 PTFS Europe Ltd
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Koha 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 Koha; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
use Modern::Perl;
|
||||
use JSON qw( to_json from_json );
|
||||
use Time::Local;
|
||||
|
||||
use C4::Koha;
|
||||
use C4::Context;
|
||||
use C4::Templates;
|
||||
use C4::Log qw( logaction );
|
||||
use Koha::ActionLogs;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Koha::Illrequest::Logger - Koha ILL Action / Event logger
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Object-oriented class that provides event logging functionality for
|
||||
ILL requests
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This class provides the ability to log arbitrary actions or events
|
||||
relating to Illrequest to the action log.
|
||||
|
||||
=head1 API
|
||||
|
||||
=head2 Class Methods
|
||||
|
||||
=head3 new
|
||||
|
||||
my $config = Koha::Illrequest::Logger->new();
|
||||
|
||||
Create a new Koha::Illrequest::Logger object.
|
||||
We also set up what can be logged, how to do it and how to display
|
||||
log entries we get back out
|
||||
|
||||
=cut
|
||||
|
||||
sub new {
|
||||
my ( $class ) = @_;
|
||||
my $self = {};
|
||||
|
||||
$self->{loggers} = {
|
||||
status => sub {
|
||||
$self->log_status_change(@_);
|
||||
}
|
||||
};
|
||||
|
||||
my ( $htdocs, $theme, $lang, $base ) =
|
||||
C4::Templates::_get_template_file('ill/log/', 'intranet');
|
||||
|
||||
$self->{templates} = {
|
||||
STATUS_CHANGE => $base . 'status_change.tt'
|
||||
};
|
||||
|
||||
bless $self, $class;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
=head3 log_maybe
|
||||
|
||||
Koha::IllRequest::Logger->log_maybe($params);
|
||||
|
||||
Receive params hashref, containing a request object and an attrs
|
||||
hashref (which may or may not be defined) If the attrs hashref contains
|
||||
a key matching our "loggers" hashref then we want to log it
|
||||
|
||||
=cut
|
||||
|
||||
sub log_maybe {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
if (defined $params->{request} && defined $params->{attrs}) {
|
||||
foreach my $key (keys %{ $params->{attrs} }) {
|
||||
if (defined($self->{loggers}->{$key})) {
|
||||
$self->{loggers}->{$key}(
|
||||
$params->{request},
|
||||
$params->{attrs}->{$key}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
=head3 log_status_change
|
||||
|
||||
Koha::IllRequest::Logger->log_status_change($params);
|
||||
|
||||
Receive a hashref containing a request object and a status to log,
|
||||
and log it
|
||||
|
||||
=cut
|
||||
|
||||
sub log_status_change {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
if (defined $params->{request} && defined $params->{value}) {
|
||||
$self->log_something({
|
||||
modulename => 'ILL',
|
||||
actionname => 'STATUS_CHANGE',
|
||||
objectnumber => $params->{request}->id,
|
||||
infos => to_json({
|
||||
log_origin => 'core',
|
||||
status_before => $params->{request}->{previous_status},
|
||||
status_after => $params->{value}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
=head3 log_something
|
||||
|
||||
Koha::IllRequest::Logger->log_something({
|
||||
modulename => 'ILL',
|
||||
actionname => 'STATUS_CHANGE',
|
||||
objectnumber => $req->id,
|
||||
infos => to_json({
|
||||
log_origin => 'core',
|
||||
status_before => $req->{previous_status},
|
||||
status_after => $new_status
|
||||
})
|
||||
});
|
||||
|
||||
If we have the required data passed, log an action
|
||||
|
||||
=cut
|
||||
|
||||
sub log_something {
|
||||
my ( $self, $to_log ) = @_;
|
||||
|
||||
if (
|
||||
defined $to_log->{modulename} &&
|
||||
defined $to_log->{actionname} &&
|
||||
defined $to_log->{objectnumber} &&
|
||||
defined $to_log->{infos} &&
|
||||
C4::Context->preference("IllLog")
|
||||
) {
|
||||
logaction(
|
||||
$to_log->{modulename},
|
||||
$to_log->{actionname},
|
||||
$to_log->{objectnumber},
|
||||
$to_log->{infos}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
=head3 get_log_template
|
||||
|
||||
$template_path = get_log_template($params);
|
||||
|
||||
Given a log's origin and action, get the appropriate display template
|
||||
|
||||
=cut
|
||||
|
||||
sub get_log_template {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
my $origin = $params->{origin};
|
||||
my $action = $params->{action};
|
||||
|
||||
if ($origin eq 'core') {
|
||||
# It's a core log, so we can just get the template path from
|
||||
# the hashref above
|
||||
return $self->{templates}->{$action};
|
||||
} else {
|
||||
# It's probably a backend log, so we need to get the path to the
|
||||
# template from the backend
|
||||
my $backend =$params->{request}->{_my_backend};
|
||||
return $backend->get_log_template_path($action);
|
||||
}
|
||||
}
|
||||
|
||||
=head3 get_request_logs
|
||||
|
||||
$requestlogs = Koha::IllRequest::Logger->get_request_logs($request_id);
|
||||
|
||||
Get all logged actions for a given request
|
||||
|
||||
=cut
|
||||
|
||||
sub get_request_logs {
|
||||
my ( $self, $request ) = @_;
|
||||
|
||||
my $logs = Koha::ActionLogs->search(
|
||||
{
|
||||
module => 'ILL',
|
||||
object => $request->id
|
||||
},
|
||||
{ order_by => { -desc => "timestamp" } }
|
||||
)->unblessed;
|
||||
|
||||
# Populate a lookup table for status aliases
|
||||
my $aliases = GetAuthorisedValues('ILLSTATUS');
|
||||
my $alias_hash;
|
||||
foreach my $alias(@{$aliases}) {
|
||||
$alias_hash->{$alias->{authorised_value}} = $alias;
|
||||
}
|
||||
foreach my $log(@{$logs}) {
|
||||
$log->{aliases} = $alias_hash;
|
||||
$log->{info} = from_json($log->{info});
|
||||
$log->{template} = $self->get_log_template({
|
||||
request => $request,
|
||||
origin => $log->{info}->{log_origin},
|
||||
action => $log->{action}
|
||||
});
|
||||
}
|
||||
|
||||
return $logs;
|
||||
}
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
$DBversion = 'XXX'; # will be replaced by the RM
|
||||
if( CheckVersion( $DBversion ) ) {
|
||||
$dbh->do( "INSERT IGNORE INTO systempreferences (variable, value, explanation, type) VALUES ('IllLog', 0, 'If ON, log information about ILL requests', 'YesNo')" );
|
||||
|
||||
SetVersion( $DBversion );
|
||||
print "Upgrade to $DBversion done (Bug 20750 - Allow timestamped auditing of ILL request events)\n";
|
||||
}
|
||||
|
|
@ -216,6 +216,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
|
|||
('IDreamBooksResults','0','','Display IDreamBooks.com rating in search results','YesNo'),
|
||||
('IDreamBooksReviews','0','','Display book review snippets from IDreamBooks.com','YesNo'),
|
||||
('IdRef','0','','Disable/enable the IdRef webservice from the OPAC detail page.','YesNo'),
|
||||
('IllLog', 0, '', 'If ON, log information about ILL requests', 'YesNo'),
|
||||
('ILLModule','0','If ON, enables the interlibrary loans module.','','YesNo'),
|
||||
('ILLModuleCopyrightClearance','','70|10','Enter text to enable the copyright clearance stage of request creation. Text will be displayed','Textarea'),
|
||||
('ILLOpacbackends',NULL,NULL,'ILL backends to enabled for OPAC initiated requests','multiple'),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ Logging:
|
|||
on: Log
|
||||
off: "Don't log"
|
||||
- any actions on holds (create, cancel, suspend, resume, etc).
|
||||
-
|
||||
- pref: IllLog
|
||||
choices:
|
||||
on: Log
|
||||
off: "Don't log"
|
||||
- when changes to ILL requests take place
|
||||
-
|
||||
- pref: IssueLog
|
||||
choices:
|
||||
|
|
|
|||
|
|
@ -319,6 +319,10 @@
|
|||
<span class="fa fa-eye"></span>
|
||||
Display supplier metadata
|
||||
</a>
|
||||
<a title="ILL request log" id="ill-request-display-log" class="btn btn-sm btn-default pull-right" href="#">
|
||||
<span class="fa fa-calendar"></span>
|
||||
ILL request log
|
||||
</a>
|
||||
</div>
|
||||
<div class="ill-view-panel panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
|
@ -429,6 +433,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="requestLog" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="dataPreviewLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3 id="requestLogLabel"> Request log</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
[% IF request.logs.size > 0 %]
|
||||
[% FOREACH log IN request.logs %]
|
||||
[% tpl = log.template %]
|
||||
[% INCLUDE $tpl log=log %]
|
||||
[% END %]
|
||||
[% ELSE %]
|
||||
There are no recorded logs for this request
|
||||
[% END %]
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ill-view-panel panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3>[% request.illcomments.count | html %] comments</h3>
|
||||
|
|
@ -467,7 +495,7 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[% ELSIF query_type == 'illlist' %]
|
||||
|
|
@ -835,6 +863,12 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Display the modal containing request supplier metadata
|
||||
$('#ill-request-display-log').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#requestLog').modal({show:true});
|
||||
});
|
||||
|
||||
// Toggle request attributes in Illview
|
||||
$('#toggle_requestattributes').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<p>
|
||||
[% log.timestamp | $KohaDates with_hours => 1 %] : <b>Status changed</b>
|
||||
[% IF log.info.status_before %]
|
||||
[% before = log.info.status_before %]
|
||||
[% display_before = log.aliases.$before ? log.aliases.$before.lib : request.capabilities.$before.name %]
|
||||
from "[% display_before | html %]"
|
||||
[% END %]
|
||||
[% after = log.info.status_after %]
|
||||
[% display_after = log.aliases.$after ? log.aliases.$after.lib : request.capabilities.$after.name %]
|
||||
to "[% display_after | html %]"
|
||||
</p>
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
[% CASE 'ACQUISITIONS' %]Acquisitions
|
||||
[% CASE 'SERIAL' %]Serials
|
||||
[% CASE 'HOLDS' %]Holds
|
||||
[% CASE 'ILL' %]Interlibrary loans
|
||||
[% CASE 'CIRCULATION' %]Circulation
|
||||
[% CASE 'LETTER' %]Letter
|
||||
[% CASE 'FINES' %]Fines
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
[% CASE 'CHANGE PASS' %]Change password
|
||||
[% CASE 'ADDCIRCMESSAGE' %]Add circulation message
|
||||
[% CASE 'DELCIRCMESSAGE' %]Delete circulation message
|
||||
[% CASE 'STATUS_CHANGE' %]Change ILL request status
|
||||
[% CASE 'Run' %]Run
|
||||
[% CASE %][% action | html %]
|
||||
[% END %]
|
||||
|
|
@ -103,7 +105,7 @@
|
|||
[% ELSE %]
|
||||
<option value="">All</option>
|
||||
[% END %]
|
||||
[% FOREACH modx IN [ 'CATALOGUING' 'AUTHORITIES' 'MEMBERS' 'ACQUISITIONS' 'SERIAL' 'HOLDS' 'CIRCULATION' 'LETTER' 'FINES' 'SYSTEMPREFERENCE' 'CRONJOBS', 'REPORTS' ] %]
|
||||
[% FOREACH modx IN [ 'CATALOGUING' 'AUTHORITIES' 'MEMBERS' 'ACQUISITIONS' 'SERIAL' 'HOLDS' 'ILL' 'CIRCULATION' 'LETTER' 'FINES' 'SYSTEMPREFERENCE' 'CRONJOBS', 'REPORTS' ] %]
|
||||
[% IF modules.grep(modx).size %]
|
||||
<option value="[% modx | html %]" selected="selected">[% PROCESS translate_log_module module=modx %]</option>
|
||||
[% ELSE %]
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ use_ok('Koha::Illrequests');
|
|||
|
||||
subtest 'Basic object tests' => sub {
|
||||
|
||||
plan tests => 24;
|
||||
plan tests => 25;
|
||||
|
||||
$schema->storage->txn_begin;
|
||||
|
||||
|
|
@ -62,6 +62,8 @@ subtest 'Basic object tests' => sub {
|
|||
"Branchcode getter works.");
|
||||
is($illrq_obj->status, $illrq->{status},
|
||||
"Status getter works.");
|
||||
is($illrq_obj->status_alias, $illrq->{status_alias},
|
||||
"Status_alias getter works.");
|
||||
is($illrq_obj->placed, $illrq->{placed},
|
||||
"Placed getter works.");
|
||||
is($illrq_obj->replied, $illrq->{replied},
|
||||
|
|
|
|||
Loading…
Reference in New Issue