Bug 30982: [22.05.x] Use the REST API for background job list view

Bug 30982: REST API specs

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: (QA follow-up) Double quoting and console.log

This patch fixes the issues highlighted by the QA script; We use double
quotes for translatable strings in JS and remove an errant console.log
call.

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: Add tests and implement GET /background_jobs/$id

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: Make code more re-usable

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: Add 'context' to the REST API specs

context has been added by bug 30889

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: Add Koha::BackgroundJobs->filter_by_current

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: API tweaks

This patch makes the following changes to the 'background_jobs' API:

* We now call them 'jobs'
* Removed deprecated query parameter definitions
* Added only_current query parameter
* Controller gets adapted to use $rs->filter_by_current when
  only_current is passed

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: Adapt table to new API spec

Disclaimer: this patch is highly opinionated :-D

When I started looking at this patch I felt like the two tables
(current/past jobs) implemented on bug 30462 was the way to go.

In order to make this patches apply after it I had to redo all the
things. Or most of them.

But I decided to keep the idea of filtering out completed tasks, not
just having the option to display 'the last hour' tasks. For the task I
added some required helper methods and the relevant tests as well. So a
behavior change.

Hope you all agree with it.

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: Rename 'Background Jobs' => 'Jobs'

Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: (QA follow-up) Resolve pod warning

Empty section in previous paragraph at line 32 in file Koha/BackgroundJobs.pm

Test plan:
Run podchecker again on this module.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: (QA follow-up) Spelling

[1] Correct: BackgrounJob
[2] If should filter out not current jobs
=> Had a hard time reading that one until I replaced if by it.
=> Decided to rephrase it in a more positive way.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: (QA follow-up) Remove redundancy from template

The template now contains two lists for both status and type:
a TT list and a JS list. The type list already proves that
redundancy leads to bugs. We miss three types at one side:
    Unknown job type 'stage_marc_for_import'
    Unknown job type 'marc_import_commit_batch'
    Unknown job type 'marc_import_revert_batch'

This patch removes the TT list. And gets the status and type
via an additional js call. For that reason I hide the fieldset
until document ready. This can be improved later when needed.

Test plan:
Look at status and type on both job list and detail view.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Bug 30982: (QA follow-up) No userenv, no jobs

+    # Assume permission if context has no user
+    my $can_manage_background_jobs = 1;
=> This felt a bit unsafe.

Test plan:
Try interface for jobs. Call API with cookie. Call API with OAuth.
Run t/db_dependent/Koha/BackgroundJobs.t

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>

Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Jonathan Druart 2022-06-17 09:21:39 +02:00 committed by Lucas Gass
parent ae9d3b0477
commit eb2dfad0d1
13 changed files with 740 additions and 209 deletions

View file

@ -47,7 +47,7 @@ my $job_id = Koha::BackgroundJob->enqueue(
);
Consumer:
Koha::BackgrounJobs->find($job_id)->process;
Koha::BackgroundJobs->find($job_id)->process;
See also C<misc/background_jobs_worker.pl> for a full example
=head1 API
@ -386,7 +386,7 @@ sub _derived_class {
=head3 type_to_class_mapping
my $mapping = Koha::BackgrounJob->new->type_to_class_mapping;
my $mapping = Koha::BackgroundJob->new->type_to_class_mapping;
Returns the available types to class mappings.
@ -404,7 +404,7 @@ sub type_to_class_mapping {
=head3 core_types_to_classes
my $mappings = Koha::BackgrounJob->new->core_types_to_classes
my $mappings = Koha::BackgroundJob->new->core_types_to_classes
Returns the core background jobs types to class mappings.
@ -469,6 +469,44 @@ sub plugin_types_to_classes {
return $self->{_plugin_mapping};
}
=head3 to_api
my $json = $job->to_api;
Overloaded method that returns a JSON representation of the Koha::BackgroundJob object,
suitable for API output.
=cut
sub to_api {
my ( $self, $params ) = @_;
my $json = $self->SUPER::to_api( $params );
$json->{context} = $self->json->decode($self->context)
if defined $self->context;
$json->{data} = $self->decoded_data;
return $json;
}
=head3 to_api_mapping
This method returns the mapping for representing a Koha::BackgroundJob object
on the API.
=cut
sub to_api_mapping {
return {
id => 'job_id',
borrowernumber => 'patron_id',
ended_on => 'ended_date',
enqueued_on => 'enqueued_date',
started_on => 'started_date',
};
}
=head3 _type
=cut

View file

@ -16,19 +16,64 @@ package Koha::BackgroundJobs;
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use base qw(Koha::Objects);
use Koha::BackgroundJob;
use base qw(Koha::Objects);
=head1 NAME
Koha::BackgroundJobs - Koha BackgroundJob Object set class
=head1 API
=head2 Class Methods
=head2 Class methods
=head3 search_limited
my $background_jobs = Koha::BackgroundJobs->search_limited( $params, $attributes );
Returns all background jobs the logged in user should be allowed to see
=cut
sub search_limited {
my ( $self, $params, $attributes ) = @_;
my $can_manage_background_jobs;
my $logged_in_user;
my $userenv = C4::Context->userenv;
if ( $userenv and $userenv->{number} ) {
$logged_in_user = Koha::Patrons->find( $userenv->{number} );
$can_manage_background_jobs = $logged_in_user->has_permission(
{ parameters => 'manage_background_jobs' } );
}
return $self->search( $params, $attributes ) if $can_manage_background_jobs;
my $id = $logged_in_user ? $logged_in_user->borrowernumber : undef;
return $self->search({ borrowernumber => $id })->search( $params, $attributes );
}
=head3 filter_by_current
my $current_jobs = $jobs->filter_by_current;
Returns a new resultset, filtering out finished jobs.
=cut
sub filter_by_current {
my ($self) = @_;
return $self->search(
{
status => { not_in => [ 'cancelled', 'failed', 'finished' ] }
}
);
}
=head2 Internal methods
=head3 _type
=cut

View file

@ -0,0 +1,99 @@
package Koha::REST::V1::BackgroundJobs;
# 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, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Mojo::Base 'Mojolicious::Controller';
use Koha::BackgroundJobs;
use Try::Tiny;
=head1 API
=head2 Methods
=head3 list
Controller function that handles listing Koha::BackgroundJob objects
=cut
sub list {
my $c = shift->openapi->valid_input or return;
return try {
my $only_current = delete $c->validation->output->{only_current};
my $bj_rs = Koha::BackgroundJobs->new;
if ($only_current) {
$bj_rs = $bj_rs->filter_by_current;
}
return $c->render(
status => 200,
openapi => $c->objects->search($bj_rs)
);
} catch {
$c->unhandled_exception($_);
};
}
=head3 get
Controller function that handles retrieving a single Koha::BackgroundJob object
=cut
sub get {
my $c = shift->openapi->valid_input or return;
return try {
my $job_id = $c->validation->param('job_id');
my $patron = $c->stash('koha.user');
my $can_manage_background_jobs =
$patron->has_permission( { parameters => 'manage_background_jobs' } );
my $job = Koha::BackgroundJobs->find($job_id);
return $c->render(
status => 404,
openapi => { error => "Object not found" }
) unless $job;
return $c->render(
status => 403,
openapi => { error => "Cannot see background job info" }
)
if !$can_manage_background_jobs
&& $job->borrowernumber != $patron->borrowernumber;
return $c->render(
status => 200,
openapi => $job->to_api
);
}
catch {
$c->unhandled_exception($_);
};
}
1;

View file

@ -77,39 +77,6 @@ if ( $op eq 'cancel' ) {
$op = 'list';
}
if ( $op eq 'list' ) {
my $queued_jobs =
$can_manage_background_jobs
? Koha::BackgroundJobs->search( { ended_on => undef },
{ order_by => { -desc => 'enqueued_on' } } )
: Koha::BackgroundJobs->search(
{ borrowernumber => $logged_in_user->borrowernumber, ended_on => undef },
{ order_by => { -desc => 'enqueued_on' } }
);
$template->param( queued => $queued_jobs );
my $ended_since = dt_from_string->subtract( minutes => '60' );
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
my $complete_jobs =
$can_manage_background_jobs
? Koha::BackgroundJobs->search(
{
ended_on => { '>=' => $dtf->format_datetime($ended_since) }
},
{ order_by => { -desc => 'enqueued_on' } }
)
: Koha::BackgroundJobs->search(
{
borrowernumber => $logged_in_user->borrowernumber,
ended_on => { '>=' => $dtf->format_datetime($ended_since) }
},
{ order_by => { -desc => 'enqueued_on' } }
);
$template->param( complete => $complete_jobs );
}
$template->param(
messages => \@messages,
op => $op,

View file

@ -0,0 +1,54 @@
---
type: object
properties:
job_id:
type: integer
description: internally assigned job identifier
readOnly: true
status:
description: job status
type: string
progress:
description: job progress
type:
- string
- "null"
size:
description: job size
type:
- string
- "null"
patron_id:
description: job enqueuer
type:
- string
- "null"
type:
description: job type
type: string
queue:
description: job queue
type: string
data:
description: job data
type: object
context:
description: job context
type: object
enqueued_date:
description: job enqueue date
type: string
format: date-time
started_date:
description: job start date
type:
- string
- "null"
format: date-time
ended_date:
description: job end date
type:
- string
- "null"
format: date-time
additionalProperties: false

View file

@ -0,0 +1,87 @@
---
/jobs:
get:
x-mojo-to: BackgroundJobs#list
operationId: listJobs
tags:
- jobs
summary: List jobs
produces:
- application/json
parameters:
- name: only_current
in: query
required: false
type: boolean
description: Only include current jobs
- $ref: "../swagger.yaml#/parameters/match"
- $ref: "../swagger.yaml#/parameters/order_by"
- $ref: "../swagger.yaml#/parameters/page"
- $ref: "../swagger.yaml#/parameters/per_page"
- $ref: "../swagger.yaml#/parameters/q_param"
- $ref: "../swagger.yaml#/parameters/q_body"
- $ref: "../swagger.yaml#/parameters/q_header"
- $ref: "../swagger.yaml#/parameters/request_id_header"
responses:
"200":
description: A list of jobs
schema:
type: array
items:
$ref: "../swagger.yaml#/definitions/job"
"403":
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
"500":
description: |
Internal server error. Possible `error_code` attribute values:
* `internal_server_error`
schema:
$ref: "../swagger.yaml#/definitions/error"
"503":
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
catalogue: "1"
"/jobs/{job_id}":
get:
x-mojo-to: BackgroundJobs#get
operationId: getJob
tags:
- jobs
summary: Get a job
parameters:
- $ref: "../swagger.yaml#/parameters/job_id_pp"
produces:
- application/json
responses:
"200":
description: A job
schema:
$ref: "../swagger.yaml#/definitions/job"
"403":
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
"404":
description: Job not found
schema:
$ref: "../swagger.yaml#/definitions/error"
"500":
description: |
Internal server error. Possible `error_code` attribute values:
* `internal_server_error`
schema:
$ref: "../swagger.yaml#/definitions/error"
"503":
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
catalogue: "1"

View file

@ -42,6 +42,8 @@ definitions:
$ref: ./definitions/invoice.yaml
item:
$ref: ./definitions/item.yaml
job:
$ref: ./definitions/job.yaml
library:
$ref: ./definitions/library.yaml
order:
@ -155,6 +157,10 @@ paths:
$ref: "./paths/items.yaml#/~1items~1{item_id}"
"/items/{item_id}/pickup_locations":
$ref: "./paths/items.yaml#/~1items~1{item_id}~1pickup_locations"
/jobs:
$ref: ./paths/jobs.yaml#/~1jobs
"/jobs/{job_id}":
$ref: "./paths/jobs.yaml#/~1jobs~1{job_id}"
/libraries:
$ref: ./paths/libraries.yaml#/~1libraries
"/libraries/{library_id}":
@ -300,6 +306,12 @@ parameters:
name: item_id
required: true
type: integer
job_id_pp:
description: Job internal identifier
in: path
name: job_id
required: true
type: integer
library_id_pp:
description: Internal library identifier
in: path
@ -560,6 +572,9 @@ tags:
- description: "Manage items\n"
name: items
x-displayName: Items
- description: "Manage jobs\n"
name: jobs
x-displayName: Jobs
- description: "Manage libraries\n"
name: libraries
x-displayName: Libraries

View file

@ -75,9 +75,9 @@
[% END %]
[% IF CAN_user_parameters_manage_background_jobs %]
<h5>Background jobs</h5>
<h5>Jobs</h5>
<ul>
<li><a href="/cgi-bin/koha/admin/background_jobs.pl">Background jobs</a></li>
<li><a href="/cgi-bin/koha/admin/background_jobs.pl">Jobs</a></li>
</ul>
[% END %]

View file

@ -147,10 +147,10 @@
[% END %]
[% IF CAN_user_parameters_manage_background_jobs %]
<h3>Background jobs</h3>
<h3>Jobs</h3>
<dl>
<dt><a href="/cgi-bin/koha/admin/background_jobs.pl">Manage background jobs</a></dt>
<dd>View, manage and cancel background jobs.</dd>
<dt><a href="/cgi-bin/koha/admin/background_jobs.pl">Manage jobs</a></dt>
<dd>View, manage and cancel jobs.</dd>
</dl>
[% END %]

View file

@ -3,54 +3,12 @@
[% USE Asset %]
[% USE KohaDates %]
[% SET footerjs = 1 %]
[% BLOCK show_job_status %]
[% SWITCH job.status %]
[% CASE "new" %]
<span>New</span>
[% CASE "cancelled" %]
<span>Cancelled</span>
[% CASE "finished" %]
<span>Finished</span>
[% CASE "started" %]
<span>Started</span>
[% CASE "running" %]
<span>Running</span>
[% CASE "failed" %]
<span>Failed</span>
[% CASE # Default case %]
[% job.status | html %]
[% END -%]
[% END %]
[% BLOCK show_job_type %]
[% SWITCH job_type %]
[% CASE 'batch_biblio_record_modification' %]
<span>Batch bibliographic record modification</span>
[% CASE 'batch_biblio_record_deletion' %]
<span>Batch bibliographic record record deletion</span>
[% CASE 'batch_authority_record_modification' %]
<span>Batch authority record modification</span>
[% CASE 'batch_authority_record_deletion' %]
<span>Batch authority record deletion</span>
[% CASE 'batch_item_record_modification' %]
<span>Batch item record modification</span>
[% CASE 'batch_item_record_deletion' %]
<span>Batch item record deletion</span>
[% CASE "batch_hold_cancel" %]
<span>Batch hold cancellation</span>
[% CASE 'update_elastic_index' %]
<span>Update Elasticsearch index</span>
[% CASE 'update_holds_queue_for_biblios' %]
<span>Holds queue update</span>
[% CASE %]<span>Unknown job type '[% job_type | html %]'</span>
[% END %]
[% END %]
[% INCLUDE 'doc-head-open.inc' %]
<title>
[% IF op == 'view' %]
Details of job #[% job.id | html %] &rsaquo;
[% END %]
Background jobs &rsaquo;
Jobs &rsaquo;
Administration &rsaquo; Koha
</title>
@ -73,14 +31,14 @@
</li>
[% IF op == 'view' %]
<li>
<a href="/cgi-bin/koha/admin/background_jobs.pl">Background jobs</a>
<a href="/cgi-bin/koha/admin/background_jobs.pl">Jobs</a>
</li>
<li>
<a href="#" aria-current="page">Details of job #[% job.id | html %]</a>
</li>
[% ELSE %]
<li>
<a href="#" aria-current="page">Background jobs</a>
<a href="#" aria-current="page">Jobs</a>
</li>
[% END %]
[% ELSE %]
@ -112,17 +70,17 @@
[% PROCESS "background_jobs/${job.type}.inc" %]
<fieldset class="rows">
<fieldset class="rows" style="display:none;">
<ol>
<li><span class="label">Job ID: </span>[% job.id | html %]</li>
<li>
<label for="job_status">Status: </label>
[% PROCESS show_job_status %]
<span id="job_status_description"></span>
</li>
<li><label for="job_progress">Progress: </label>[% job.progress || 0 | html %] / [% job.size | html %]</li>
<li>
<label for="job_type">Type: </label>
[% PROCESS show_job_type job_type => job.type %]
<span id="job_type_description"></span>
</li>
<li>
<label for="job_enqueued_on">Queued: </label>
@ -154,108 +112,32 @@
[% IF op == 'list' %]
<h1>Background jobs</h1>
<h1>Jobs</h1>
<div id="taskstabs" class="toptabs">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#queued" aria-controls="queued" role="tab" data-toggle="tab">Queued jobs</a></li>
<li role="presentation"><a href="#complete" aria-controls="complete" role="tab" data-toggle="tab">Completed jobs</a></li>
</ul>
<div>
<input type="checkbox" id="only_current" checked />
<label for="only_current">Current jobs only</label>
</div>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="queued">
[% IF queued.count %]
<table id="table_queued_jobs">
<div>
<input type="checkbox" id="include_last_hour" checked />
<label for="include_last_hour">Only include jobs started in the last hour</label>
</div>
<table id="table_jobs">
<thead>
<tr>
<th>Job ID</th>
<th>Status</th>
<th data-filter="job_statuses">Status</th>
<th>Progress</th>
<th>Type</th>
<th>Queued</th>
<th>Started</th>
<th class="noExport">Actions</th>
</tr>
</thead>
<tbody>
[% FOREACH job IN queued %]
<tr>
<td>[% job.id | html %]</td>
<td>
[% PROCESS show_job_status %]
</td>
<td>[% job.progress || 0 | html %] / [% job.size | html %]</td>
<td>
[% PROCESS show_job_type job_type => job.type %]
</td>
<td>[% job.enqueued_on | $KohaDates with_hours = 1 %]</td>
<td>[% job.started_on| $KohaDates with_hours = 1 %]</td>
<td class="actions">
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/admin/background_jobs.pl?op=view&amp;id=[% job.id | html %]"><i class="fa fa-eye"></i> View</a>
[% IF job.status == 'new' || job.status == 'started' %]
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/admin/background_jobs.pl?op=cancel&amp;id=[% job.id | html %]"><i class="fa fa-trash"></i> Cancel</a>
[% END %]
</td>
</tr>
[% END %]
</tbody>
</table>
[% ELSE %]
<div class="dialog message">
There are no queued background jobs yet.
</div>
[% END %]
</div>
<div role="tabpanel" class="tab-pane" id="complete">
[% IF complete.count %]
<p>Jobs completed in the last 60 minutes.</p>
<table id="table_complete_jobs">
<thead>
<tr>
<th>Job ID</th>
<th>Status</th>
<th>Progress</th>
<th>Type</th>
<th data-filter="job_types">Type</th>
<th>Queued</th>
<th>Started</th>
<th>Ended</th>
<th class="noExport">Actions</th>
</tr>
</thead>
<tbody>
[% FOREACH job IN complete %]
<tr>
<td>[% job.id | html %]</td>
<td>
[% PROCESS show_job_status %]
</td>
<td>[% job.progress || 0 | html %] / [% job.size | html %]</td>
<td>
[% PROCESS show_job_type job_type => job.type %]
</td>
<td>[% job.enqueued_on | $KohaDates with_hours = 1 %]</td>
<td>[% job.started_on| $KohaDates with_hours = 1 %]</td>
<td>[% job.ended_on| $KohaDates with_hours = 1 %]</td>
<td class="actions">
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/admin/background_jobs.pl?op=view&amp;id=[% job.id | html %]"><i class="fa fa-eye"></i> View</a>
[% IF job.status == 'new' || job.status == 'started' %]
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/admin/background_jobs.pl?op=cancel&amp;id=[% job.id | html %]"><i class="fa fa-trash"></i> Cancel</a>
[% END %]
</td>
</tr>
[% END %]
</tbody>
</table>
[% ELSE %]
<div class="dialog message">
There were no completed background jobs completed in the last 60 minutes.
</div>
[% END %]
</div>
</div>
</div>
[% END %]
</main>
@ -270,26 +152,193 @@
[% MACRO jsinclude BLOCK %]
[% Asset.js("js/admin-menu.js") | $raw %]
[% INCLUDE 'js-date-format.inc' %]
[% INCLUDE 'datatables.inc' %]
<script>
$(document).ready(function() {
$("#table_queued_jobs").dataTable($.extend(true, {}, dataTablesDefaults, {
"aoColumnDefs": [
{ "aTargets": [ -1, -2 ], "bSortable": false, "bSearchable": false },
],
"aaSorting": [[ 0, "desc" ]],
"iDisplayLength": 10,
"sPaginationType": "full_numbers"
}));
const job_statuses = [
{'_id': 'new', '_str': _("New")},
{'_id': 'cancelled', '_str': _("Cancelled")},
{'_id': 'finished', '_str': _("Finished")},
{'_id': 'started', '_str': _("Started")},
{'_id': 'running', '_str': _("Running")},
{'_id': 'failed', '_str': _("Failed")},
];
function get_job_status (status) {
let status_lib = job_statuses.find( s => s._id == status );
if (status_lib) {
return status_lib._str;
}
return status;
}
$("#table_complete_jobs").dataTable($.extend(true, {}, dataTablesDefaults, {
"aoColumnDefs": [
{ "aTargets": [ -1, -2 ], "bSortable": false, "bSearchable": false },
],
"aaSorting": [[ 0, "desc" ]],
"iDisplayLength": 10,
"sPaginationType": "full_numbers"
}));
const job_types = [
{
'_id': 'batch_biblio_record_modification',
'_str': _("Batch bibliographic record modification")
},
{
'_id': 'batch_biblio_record_deletion',
'_str': _("Batch bibliographic record record deletion")
},
{
'_id': 'batch_authority_record_modification',
'_str': _("Batch authority record modification")
},
{
'_id': 'batch_authority_record_deletion',
'_str': _("Batch authority record deletion")
},
{
'_id': 'batch_item_record_modification',
'_str': _("Batch item record modification")
},
{
'_id': 'batch_item_record_deletion',
'_str': _("Batch item record deletion")
},
{
'_id': 'batch_hold_cancel',
'_str': _("Batch hold cancellation")
},
{
'_id': 'update_elastic_index',
'_str': _("Update Elasticsearch index")
},
{
'_id': 'update_holds_queue_for_biblios',
'_str': _("Holds queue update")
},
{
'_id': 'stage_marc_for_import',
'_str': _("Staged MARC records for import")
},
{
'_id': 'marc_import_commit_batch',
'_str': _("Import MARC records")
},
{
'_id': 'marc_import_revert_batch',
'_str': _("Revert import MARC records")
},
];
function get_job_type (job_type) {
let job_type_lib = job_types.find( t => t._id == job_type );
if ( job_type_lib ) {
return job_type_lib._str;
}
return _("Unknown job type '%s'").format(job_type);
}
$(document).ready(function() {
[% IF op == 'view' %]
$("#job_status_description").html( get_job_status("[% job.status | html %]") );
$("#job_type_description").html( get_job_type("[% job.type | html %]") );
$("fieldset.rows").show();
[% END %]
let additional_filters = {
started_on: function(){
let now = new Date();
if ( $("#include_last_hour").is(":checked") ) {
now.setHours(now.getHours() - 1);
return { ">": now.toISOString() };
} else {
return { "<": now.toISOString() };
}
}
};
let only_current_filter = function(){
if ( $("#only_current").is(":checked") ) {
return 'only_current=1';
} else {
return 'only_current=0';
}
}
let jobs_table = $("#table_jobs").kohaTable({
"ajax": {
"url": "/api/v1/jobs?" + only_current_filter()
},
"order": [[ 1, "desc" ]],
"columns": [
{
"data": "job_id",
"searchable": true,
"orderable": true
},
{
"data": "status",
"searchable": true,
"orderable": true,
"render": function(data, type, row, meta) {
return get_job_status(row.status).escapeHtml();
}
},
{
"data": "progress,size",
"searchable": false,
"orderable": true,
"render": function(data, type, row, meta) {
return "%s/%s".format(row.progress, row.size).escapeHtml();
}
},
{
"data": "type",
"searchable": true,
"orderable": true,
"render": function(data, type, row, meta) {
return get_job_type(row.type).escapeHtml();
}
},
{
"data": "enqueued_date",
"searchable": true,
"orderable": true,
"render": function(data, type, row, meta) {
return $datetime(row.enqueued_date);
}
},
{
"data": "started_date",
"searchable": true,
"orderable": true,
"render": function(data, type, row, meta) {
return $datetime(row.started_date);
}
},
{
"data": "ended_date",
"searchable": true,
"orderable": true,
"render": function(data, type, row, meta) {
return $datetime(row.ended_date);
}
},
{
"data": function( row, type, val, meta ) {
var result = '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/admin/background_jobs.pl?op=view&amp;id='+ encodeURIComponent(row.job_id) +'"><i class="fa fa-eye" aria-hidden="true"></i> '+_("View")+'</a>'+"\n";
if ( row.status == 'new' || row.status == 'started' ) {
result += '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/admin/bakcground_jobs.pl?op=cancel&amp;id='+ encodeURIComponent(row.job_id) +'"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Cancel")+'</a>';
}
return result;
},
"searchable": false,
"orderable": false
}
]
}, null, 1, additional_filters);
$("#include_last_hour").on("change", function(){
jobs_table.DataTable().draw();
return false;
});
$("#only_current").on("change", function(){
jobs_table.DataTable().ajax.url("/api/v1/jobs?" + only_current_filter()).load();
return false;
});
});
</script>
[% IF op == 'view' %]

View file

@ -19,9 +19,11 @@
use Modern::Perl;
use Test::More tests => 12;
use Test::More tests => 14;
use Test::MockModule;
use List::MoreUtils qw(any);
use Koha::Database;
use Koha::BackgroundJobs;
use Koha::DateUtils qw( dt_from_string );
@ -31,6 +33,7 @@ use t::lib::Mocks;
use t::lib::Dates;
use t::lib::Koha::BackgroundJob::BatchTest;
my $builder = t::lib::TestBuilder->new;
my $schema = Koha::Database->new->schema;
$schema->storage->txn_begin;
@ -91,3 +94,49 @@ is_deeply(
is_deeply( $new_job->additional_report(), {} );
$schema->storage->txn_rollback;
subtest 'filter_by_current() tests' => sub {
plan tests => 4;
$schema->storage->txn_begin;
my $job_new = $builder->build_object( { class => 'Koha::BackgroundJobs', value => { status => 'new' } } );
my $job_cancelled = $builder->build_object( { class => 'Koha::BackgroundJobs', value => { status => 'cancelled' } } );
my $job_failed = $builder->build_object( { class => 'Koha::BackgroundJobs', value => { status => 'failed' } } );
my $job_finished = $builder->build_object( { class => 'Koha::BackgroundJobs', value => { status => 'finished' } } );
my $rs = Koha::BackgroundJobs->search(
{
id => [ $job_new->id, $job_cancelled->id, $job_failed->id, $job_finished->id ]
}
);
is( $rs->count, 4, '4 jobs in resultset' );
ok( any {$_->status eq 'new'} @{$rs->as_list}, "There is a 'new' job" );
$rs = $rs->filter_by_current;
is( $rs->count, 1, 'Only 1 job in filtered resultset' );
is( $rs->next->status, 'new', "The only job in resultset is 'new'" );
$schema->storage->txn_rollback;
};
subtest 'search_limited' => sub {
plan tests => 3;
$schema->storage->txn_begin;
my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 0 } } );
my $patron2 = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 0 } } );
my $job1 = $builder->build_object( { class => 'Koha::BackgroundJobs', value => { borrowernumber => $patron1->id } } );
C4::Context->set_userenv( undef, q{} );
is( Koha::BackgroundJobs->search_limited->count, 0, 'No jobs found without userenv' );
C4::Context->set_userenv( $patron1->id, $patron1->userid );
is( Koha::BackgroundJobs->search_limited->count, 1, 'My job found' );
C4::Context->set_userenv( $patron2->id, $patron2->userid );
is( Koha::BackgroundJobs->search_limited->count, 0, 'No jobs for me' );
$schema->storage->txn_rollback;
};

125
t/db_dependent/api/v1/jobs.t Executable file
View file

@ -0,0 +1,125 @@
#!/usr/bin/env perl
# 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, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Test::More tests => 25;
use Test::Mojo;
use t::lib::TestBuilder;
use t::lib::Mocks;
use Koha::BackgroundJobs;
use Koha::Database;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
my $t = Test::Mojo->new('Koha::REST::V1');
#use t::lib::Mojo;
#my $t = t::lib::Mojo->new('Koha::REST::V1');
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
$schema->storage->txn_begin;
Koha::BackgroundJobs->delete;
my $superlibrarian = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 1 },
}
);
my $password = 'thePassword123';
$superlibrarian->set_password( { password => $password, skip_validation => 1 } );
my $superlibrarian_userid = $superlibrarian->userid;
my $librarian = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 2 ** 2 }, # catalogue flag = 2
}
);
$librarian->set_password( { password => $password, skip_validation => 1 } );
my $librarian_userid = $librarian->userid;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 0 },
}
);
$patron->set_password( { password => $password, skip_validation => 1 } );
my $patron_userid = $patron->userid;
$t->get_ok("//$librarian_userid:$password@/api/v1/jobs")
->status_is(200)
->json_is( [] );
my $job = $builder->build_object(
{
class => 'Koha::BackgroundJobs',
value => {
status => 'finished',
progress => 100,
size => 100,
borrowernumber => $patron->borrowernumber,
type => 'batch_item_record_modification',
queue => 'default',
#data => '{"record_ids":["1"],"regex_mod":null,"exclude_from_local_holds_priority":null,"new_values":{"itemnotes":"xxx"}}' ,
data => '{"regex_mod":null,"report":{"total_records":1,"modified_fields":1,"modified_itemnumbers":[1]},"new_values":{"itemnotes":"xxx"},"record_ids":["1"],"exclude_from_local_holds_priority":null}',
}
}
);
{
$t->get_ok("//$superlibrarian_userid:$password@/api/v1/jobs")
->status_is(200)->json_is( [ $job->to_api ] );
$t->get_ok("//$librarian_userid:$password@/api/v1/jobs")
->status_is(200)->json_is( [] );
$t->get_ok("//$patron_userid:$password@/api/v1/jobs")
->status_is(403);
$job->borrowernumber( $librarian->borrowernumber )->store;
$t->get_ok("//$librarian_userid:$password@/api/v1/jobs")
->status_is(200)->json_is( [ $job->to_api ] );
}
{
$t->get_ok( "//$superlibrarian_userid:$password@/api/v1/jobs/"
. $job->id )->status_is(200)
->json_is( $job->to_api );
$t->get_ok( "//$librarian_userid:$password@/api/v1/jobs/"
. $job->id )->status_is(200)
->json_is( $job->to_api );
$job->borrowernumber( $superlibrarian->borrowernumber )->store;
$t->get_ok( "//$librarian_userid:$password@/api/v1/jobs/"
. $job->id )->status_is(403);
}
{
$job->delete;
$t->get_ok( "//$superlibrarian_userid:$password@/api/v1/jobs/"
. $job->id )->status_is(404)
->json_is( '/error' => 'Object not found' );
}
$schema->storage->txn_rollback;

View file

@ -547,6 +547,9 @@ sub _gen_blob {
sub _gen_default_values {
my ($self) = @_;
return {
BackgroundJob => {
context => '{}'
},
Borrower => {
login_attempts => 0,
gonenoaddress => undef,