Browse Source

Bug 32030: Create eHolding titles from a list

Add the ability to create new titles and attach them to a package.
The MARC to KBART2 mapping is the following (based on
https://github.com/adambuttrick/marc_to_kbart/blob/master/convert.py):

publication_title = biblio.title
print_identifier  = 020$a||020$z||022$a||022$y
online_identifier = 020$a||020$z||022$a||022$y
date_first_issue_online = 866$a (before '-')
date_last_issue_online  = 866$a (after '-')
num_first_vol_online    = 863$a (before '-')
num_last_vol_online     = 863$a (after '-')
num_first_issue_online  = ?
num_last_issue_online   = ?
title_url = 856$u
first_author = biblio.first_author
embargo_info = ?
coverage_depth = title_url ? 'fulltext' : 'print'
notes = $852$z
publisher_name = 260$b
publication_type = ?
date_monograph_published_print = ?
date_monograph_published_online = ?
monograph_volume = ?
monograph_edition = ?
first_editor = ?
parent_publication_title_id = ?
preceeding_publication_title_id = ?
access_type = ?

Note that title is not created (and so the resource) if a title from
this package already has a link to this bibliographic record.
Is that correct, or should we create another resource?

Should the import screen also have "start date" and "end date" to set for the
resource?

QA note: Ideally we would like to fetch the list from the REST API but the routes
are not there yet.

Signed-off-by: Jonathan Field <jonathan.field@ptfs-europe.com>

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
22.11.x
Jonathan Druart 2 years ago
committed by Tomas Cohen Arazi
parent
commit
94c67c439e
Signed by: tomascohen GPG Key ID: 0A272EA1B2F3C15F
  1. 1
      Koha/BackgroundJob.pm
  2. 227
      Koha/BackgroundJob/CreateEHoldingsFromBiblios.pm
  3. 11
      Koha/REST/V1/ERM/EHoldings/Titles.pm
  4. 39
      Koha/REST/V1/ERM/EHoldings/Titles/Local.pm
  5. 66
      api/v1/swagger/paths/erm_eholdings_titles.yaml
  6. 4
      api/v1/swagger/swagger.yaml
  7. 43
      koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/create_eholdings_from_biblios.inc
  8. 4
      koha-tmpl/intranet-tmpl/prog/en/modules/admin/background_jobs.tt
  9. 2
      koha-tmpl/intranet-tmpl/prog/en/modules/erm/erm.tt
  10. 141
      koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitlesFormImport.vue
  11. 5
      koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitlesToolbar.vue
  12. 4
      koha-tmpl/intranet-tmpl/prog/js/vue/fetch.js
  13. 24
      koha-tmpl/intranet-tmpl/prog/js/vue/routes.js

1
Koha/BackgroundJob.pm

@ -419,6 +419,7 @@ sub core_types_to_classes {
batch_item_record_deletion => 'Koha::BackgroundJob::BatchDeleteItem',
batch_item_record_modification => 'Koha::BackgroundJob::BatchUpdateItem',
batch_hold_cancel => 'Koha::BackgroundJob::BatchCancelHold',
create_eholdings_from_biblios => 'Koha::BackgroundJob::CreateEHoldingsFromBiblios',
update_elastic_index => 'Koha::BackgroundJob::UpdateElasticIndex',
update_holds_queue_for_biblios => 'Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue',
stage_marc_for_import => 'Koha::BackgroundJob::StageMARCForImport',

227
Koha/BackgroundJob/CreateEHoldingsFromBiblios.pm

@ -0,0 +1,227 @@
package Koha::BackgroundJob::CreateEHoldingsFromBiblios;
# 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 JSON qw( decode_json encode_json );
use Try::Tiny;
use Koha::Biblios;
use Koha::DateUtils qw( dt_from_string );
use Koha::ERM::EHoldings::Titles;
use Koha::ERM::EHoldings::Resources;
use C4::Context;
use base 'Koha::BackgroundJob';
=head1 NAME
CreateEHoldingsFromBiblios - Create new eHoldings titles from biblios
This is a subclass of Koha::BackgroundJob.
=head1 API
=head2 Class methods
=head3 job_type
Define the job type of this job.
=cut
sub job_type {
return 'create_eholdings_from_biblios';
}
=head3 process
Process the import.
=cut
sub process {
my ( $self, $args ) = @_;
if ( $self->status eq 'cancelled' ) {
return;
}
my $job_progress = 0;
$self->started_on(dt_from_string)
->progress($job_progress)
->status('started')
->store;
my @messages;
my @record_ids = @{ $args->{record_ids} };
my $package_id = $args->{package_id};
my $package = Koha::ERM::EHoldings::Packages->find($package_id);
unless ( $package ) {
push @messages, {
type => 'error',
code => 'package_do_not_exist',
package_id => $package_id,
};
}
my $report = {
total_records => scalar @record_ids,
total_success => 0,
};
my $fix_coverage = sub {
my $coverage = shift || q{};
my @coverages = split '-', $coverage;
return ($coverages[0], (@coverages > 1 ? $coverages[1] : q{}));
};
my %existing_biblio_ids = map {
my $resource = $_;
map { $_->biblio_id => $resource->resource_id } $resource->title
} $package->resources->as_list;
RECORD_IDS: for my $biblio_id ( sort { $a <=> $b } @record_ids ) {
last if $self->get_from_storage->status eq 'cancelled';
next unless $biblio_id;
try {
if ( grep { $_ eq $biblio_id } keys %existing_biblio_ids ) {
push @messages,
{
type => 'warning',
code => 'biblio_already_exists',
biblio_id => $biblio_id,
resource_id => $existing_biblio_ids{$biblio_id},
};
return;
}
my $biblio = Koha::Biblios->find($biblio_id);
my $record = $biblio->metadata->record;
my $publication_title = $biblio->title;
my $print_identifier =
$record->subfield( '020', 'a' )
|| $record->subfield( '020', 'z' )
|| $record->subfield( '022', 'a' )
|| $record->subfield( '022', 'y' );
my $online_identifier = $print_identifier;
my ( $date_first_issue_online, $date_last_issue_online ) =
$fix_coverage->( $record->subfield( '866', 'a' ) );
my ( $num_first_vol_online, $num_last_vol_online ) =
fix_coverage->( $record->subfield( '863', 'a' ) );
my ( $num_first_issue_online, $num_last_issue_online ) =
( '', '' ); # FIXME ?
my $title_url = $record->subfield( '856', 'u' );
my $first_author = $biblio->author;
my $embargo_info = ''; # FIXME ?
my $coverage_depth = $title_url ? 'fulltext' : 'print';
my $notes = $record->subfield( '852', 'z' );
my $publisher_name = $record->subfield( '260', 'b' );
my $publication_type = ''; # FIXME ?
my $date_monograph_published_print = ''; # FIXME ?
my $date_monograph_published_online = ''; # FIXME ?
my $monograph_volume = ''; # FIXME ?
my $monograph_edition = ''; # FIXME ?
my $first_editor = ''; # FIXME ?
my $parent_publication_title_id = ''; # FIXME ?
my $preceeding_publication_title_id = ''; # FIXME ?
my $access_type = ''; # FIXME ?
my $eholding_title = {
biblio_id => $biblio_id,
publication_title => $publication_title,
print_identifier => $print_identifier,
online_identifier => $online_identifier,
date_first_issue_online => $date_first_issue_online,
num_first_vol_online => $num_first_vol_online,
num_first_issue_online => $num_first_issue_online,
date_last_issue_online => $date_last_issue_online,
num_last_vol_online => $num_last_vol_online,
num_last_issue_online => $num_last_issue_online,
title_url => $title_url,
first_author => $first_author,
embargo_info => $embargo_info,
coverage_depth => $coverage_depth,
notes => $notes,
publisher_name => $publisher_name,
publication_type => $publication_type,
date_monograph_published_print => $date_monograph_published_print,
date_monograph_published_online => $date_monograph_published_online,
monograph_volume => $monograph_volume,
monograph_edition => $monograph_edition,
first_editor => $first_editor,
parent_publication_title_id => $parent_publication_title_id,
preceeding_publication_title_id => $preceeding_publication_title_id,
access_type => $access_type,
};
$eholding_title = Koha::ERM::EHoldings::Title->new($eholding_title)->store;
Koha::ERM::EHoldings::Resource->new({ title_id => $eholding_title->title_id, package_id => $package_id })->store;
$report->{total_success}++;
} catch {
push @messages, {
type => 'error',
code => 'eholding_not_created',
error => $_,
};
};
$self->progress( ++$job_progress )->store;
}
my $job_data = decode_json $self->data;
$job_data->{messages} = \@messages;
$job_data->{report} = $report;
$self->ended_on(dt_from_string)
->data(encode_json $job_data);
$self->status('finished') if $self->status ne 'cancelled';
$self->store;
}
=head3 enqueue
Enqueue the new job
=cut
sub enqueue {
my ( $self, $args) = @_;
return unless exists $args->{package_id};
return unless exists $args->{record_ids};
$self->SUPER::enqueue({
job_size => scalar @{$args->{record_ids}},
job_args => $args,
queue => 'long_tasks',
});
}
=head3 additional_report
=cut
sub additional_report {
my ($self) = @_;
my $loggedinuser = C4::Context->userenv ? C4::Context->userenv->{'number'} : undef;
return {};
}
1;

11
Koha/REST/V1/ERM/EHoldings/Titles.pm

@ -111,4 +111,15 @@ sub delete {
}
}
=head3 import_from_list
Controller function that handles importing bibliographic record as local eholdings
=cut
sub import_from_list {
my $c = shift->openapi->valid_input or return;
return Koha::REST::V1::ERM::EHoldings::Titles::Local::import_from_list($c);
}
1;

39
Koha/REST/V1/ERM/EHoldings/Titles/Local.pm

@ -20,6 +20,7 @@ use Modern::Perl;
use Mojo::Base 'Mojolicious::Controller';
use Koha::ERM::EHoldings::Titles;
use Koha::BackgroundJob::CreateEHoldingsFromBiblios;
use Scalar::Util qw( blessed );
use Try::Tiny qw( catch try );
@ -238,4 +239,42 @@ sub delete {
};
}
=head3 import_from_list
=cut
sub import_from_list {
my $c = shift->openapi->valid_input or return;
my $body = $c->validation->param('body');
my $list_id = $body->{list_id};
my $package_id = $body->{package_id};
my $list = Koha::Virtualshelves->find($list_id);
my $patron = $c->stash('koha.user');
unless ( $list && $list->owner == $c->stash('koha.user')->borrowernumber ) {
return $c->render(
status => 404,
openapi => { error => "List not found" }
);
}
return try {
my @biblionumbers = $list->get_contents->get_column('biblionumber');
my $params = { record_ids => \@biblionumbers, package_id => $package_id };
my $job_id = Koha::BackgroundJob::CreateEHoldingsFromBiblios->new->enqueue( $params);
return $c->render(
status => 201,
openapi => { job_id => $job_id }
);
}
catch {
$c->unhandled_exception($_);
};
}
1;

66
api/v1/swagger/paths/erm_eholdings_titles.yaml

@ -387,3 +387,69 @@
x-koha-authorization:
permissions:
erm: 1
/erm/eholdings/local/titles/import:
post:
x-mojo-to: ERM::EHoldings::Titles#import_from_list
operationId: importErmEHoldingsTitles
tags:
- eholdings
summary: Import local titles
consumes:
- application/json
produces:
- application/json
parameters:
- description: The list_id of the list to import
in: body
name: body
required: true
schema:
type: object
properties:
list_id:
type: string
package_id:
type: string
responses:
201:
description: Successfully enqueued the import job
schema:
type: object
properties:
job_id:
type: string
additionalProperties: false
400:
description: Bad parameter
schema:
$ref: "../swagger.yaml#/definitions/error"
401:
description: Authentication required
schema:
$ref: "../swagger.yaml#/definitions/error"
403:
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
404:
description: Ressource not found
schema:
$ref: "../swagger.yaml#/definitions/error"
409:
description: Conflict in creating resource
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:
erm: 1

4
api/v1/swagger/swagger.yaml

@ -174,7 +174,9 @@ paths:
"/erm/agreements/{agreement_id}/documents/{document_id}/file/content":
$ref: "./paths/erm_documents.yaml#/~1erm~1agreements~1{agreement_id}~1documents~1{document_id}~1file~1content"
"/erm/eholdings/{provider}/titles":
$ref: ./paths/erm_eholdings_titles.yaml#/~1erm~1eholdings~1{provider}~1titles
$ref: "./paths/erm_eholdings_titles.yaml#/~1erm~1eholdings~1{provider}~1titles"
/erm/eholdings/local/titles/import:
$ref: ./paths/erm_eholdings_titles.yaml#/~1erm~1eholdings~1local~1titles~1import
"/erm/eholdings/{provider}/titles/{title_id}":
$ref: "./paths/erm_eholdings_titles.yaml#/~1erm~1eholdings~1{provider}~1titles~1{title_id}"
"/erm/eholdings/{provider}/titles/{title_id}/resources":

43
koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/create_eholdings_from_biblios.inc

@ -0,0 +1,43 @@
[% BLOCK report %]
[% SET report = job.report %]
[% IF report && job.status != 'started' && job.status != 'new' %]
[% IF report.total_records == report.total_success %]
<div class="dialog message">
All eHolding titles have been created successfully!
</div>
[% ELSIF report.total_success == 0 %]
<div class="dialog message">
No eHolding titles have been created. An error occurred.
</div>
[% ELSE %]
<div class="dialog message">
[% report.total_success | html %] / [% report.total_records | html %] eHolding titles have been created successfully but some errors occurred.
</div>
[% END %]
[% END %]
[% END %]
[% BLOCK detail %]
[% FOR m IN job.messages %]
<div class="dialog message">
[% IF m.type == 'success' %]
<i class="fa fa-check success"></i>
[% ELSIF m.type == 'warning' %]
<i class="fa fa-warning warn"></i>
[% ELSIF m.type == 'error' %]
<i class="fa fa-exclamation error"></i>
[% END %]
[% SWITCH m.code %]
[% CASE 'package_do_not_exist' %]
<span>The package #[% m.package_id | html %] does not exist.</span>
[% CASE 'biblio_already_exists' %]
<span>The bibliographic record ([% m.biblio_id | html %]) already exists in this package (<a href="/cgi-bin/koha/erm/eholdings/local/resources/[% m.resource_id | uri %]">resource #[% m.resource_id | html %]</a>)</span>
[% CASE 'eholding_not_created' %]
<span>eHolding title cannot be created from bibliographic record #[% m.biblio_id | html %], encountered the following error: [% m.error | html %].</span>
[% END %]
</div>
[% END %]
[% END %]
[% BLOCK js %]
[% END %]

4
koha-tmpl/intranet-tmpl/prog/en/modules/admin/background_jobs.tt

@ -209,6 +209,10 @@
'_id': 'batch_hold_cancel',
'_str': _("Batch hold cancellation")
},
{
'_id': 'create_eholdings_from_biblios',
'_str': _("Create eHolding titles")
},
{
'_id': 'update_elastic_index',
'_str': _("Update Elasticsearch index")

2
koha-tmpl/intranet-tmpl/prog/en/modules/erm/erm.tt

@ -56,6 +56,8 @@
const lang = "[% lang || 'en' | html %]";
const logged_in_user_lists = [% To.json(logged_in_user.virtualshelves.unblessed) | $raw %];
</script>
[% Asset.js("js/vue/dist/main.js") | $raw %]

141
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitlesFormImport.vue

@ -0,0 +1,141 @@
<template>
<h2>{{ $t("Import from a list") }}</h2>
<div v-if="job_id" class="dialog message">
{{ $t("Import in progress,") }}
<router-link :to="`/cgi-bin/koha/admin/background_jobs/${job_id}`">
{{ $t("see job #%s").format(job_id) }}
</router-link>
</div>
<div id="package_list">
{{ $t("To the following local package") }}:
<v-select
v-model="package_id"
label="name"
:reduce="(p) => p.package_id"
:options="packages"
:clearable="false"
>
</v-select>
</div>
<div id="import_list_result">
<table :id="table_id"></table>
</div>
</template>
<script>
import { setMessage, setError, setWarning } from "../../messages"
import { createVNode, render } from 'vue'
import { useDataTable } from "../../composables/datatables"
import { checkError, fetchLocalPackages } from '../../fetch.js'
export default {
setup() {
const table_id = "list_list"
useDataTable(table_id)
return {
table_id,
logged_in_user_lists,
}
},
data() {
return {
job_id: null,
package_id: null,
packages: [],
}
},
beforeCreate() {
fetchLocalPackages().then((packages) => {
this.packages = packages
if (this.packages.length) {
this.package_id = packages[0].package_id
}
})
},
methods: {
import_from_list: async function (list_id) {
if (!this.package_id) {
setError(this.$t("Cannot import, no package selected"))
return
}
if (!list_id) return
await fetch('/api/v1/erm/eholdings/local/titles/import', {
method: "POST",
body: JSON.stringify({ list_id, package_id: this.package_id }),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then(checkError)
.then(
(result) => {
this.job_id = result.job_id
},
(error) => {
setError(error)
}
)
},
build_datatable: function () {
let lists = this.logged_in_user_lists
let table_id = this.table_id
let import_from_list = this.import_from_list
$('#' + table_id).dataTable($.extend(true, {}, dataTablesDefaults, {
data: lists,
order: [[0, "asc"]],
autoWidth: false,
columns: [
{
title: __("Name"),
data: "shelfname",
searchable: true,
orderable: true,
width: '100%',
render: function (data, type, row, meta) {
return row.shelfname + ' (#' + row.shelfnumber + ')'
}
},
{
title: __("Actions"),
data: function (row, type, val, meta) {
return '<div class="actions"></div>'
},
className: "actions noExport",
searchable: false,
orderable: false
}
],
drawCallback: function (settings) {
var api = new $.fn.dataTable.Api(settings)
$.each($(this).find("td .actions"), function (index, e) {
let tr = $(this).parent().parent()
let list_id = api.row(tr).data().shelfnumber
let importButton = createVNode("a", {
class: "btn btn-default btn-xs", role: "button", onClick: () => {
import_from_list(list_id)
}
},
[createVNode("i", { class: "fa fa-download", 'aria-hidden': "true" }), __("Import")])
let n = createVNode('span', {}, [importButton])
render(n, e)
})
}
}))
},
},
mounted() {
this.build_datatable()
},
name: "EHoldingsLocalTitlesFormImport",
}
</script>
<style scoped>
fieldset.rows label {
width: 25rem;
}
</style>

5
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitlesToolbar.vue

@ -3,6 +3,11 @@
><font-awesome-icon icon="plus" />
{{ $t("New title") }}</router-link
>
&nbsp;
<router-link to="/cgi-bin/koha/erm/eholdings/local/titles/import" class="btn btn-default"
><font-awesome-icon icon="plus" />
{{ $t("Import from list") }}</router-link
>
</template>
<script>

4
koha-tmpl/intranet-tmpl/prog/js/vue/fetch.js

@ -367,7 +367,7 @@ export const fetchEBSCOResources = function () {
return _fetchResources(apiUrl);
};
function checkError(response) {
export const checkError = function(response) {
if (response.status >= 200 && response.status <= 299) {
return response.json();
} else {
@ -375,4 +375,4 @@ function checkError(response) {
console.log(response);
setError("%s (%s)".format(response.statusText, response.status));
}
}
};

24
koha-tmpl/intranet-tmpl/prog/js/vue/routes.js

@ -10,6 +10,7 @@ import EHoldingsLocalHome from "./components/ERM/EHoldingsLocalHome.vue";
import EHoldingsLocalPackagesFormAdd from "./components/ERM/EHoldingsLocalPackagesFormAdd.vue";
import EHoldingsLocalTitlesFormConfirmDelete from "./components/ERM/EHoldingsLocalTitlesFormConfirmDelete.vue";
import EHoldingsLocalTitlesFormAdd from "./components/ERM/EHoldingsLocalTitlesFormAdd.vue";
import EHoldingsLocalTitlesFormImport from "./components/ERM/EHoldingsLocalTitlesFormImport.vue";
import EHoldingsLocalPackagesList from "./components/ERM/EHoldingsLocalPackagesList.vue";
import EHoldingsLocalPackagesShow from "./components/ERM/EHoldingsLocalPackagesShow.vue";
import EHoldingsLocalPackagesFormConfirmDelete from "./components/ERM/EHoldingsLocalPackagesFormConfirmDelete.vue";
@ -122,6 +123,14 @@ export const routes = [
window.location.href = "/cgi-bin/koha/mainpage.pl";
},
},
{
path: "/cgi-bin/koha/admin/background_jobs/:id",
beforeEnter(to, from, next) {
window.location.href =
"/cgi-bin/koha/admin/background_jobs.pl?op=view&id=" +
to.params.id;
},
},
{
path: "/cgi-bin/koha/erm/erm.pl",
component: ERMHome,
@ -366,6 +375,21 @@ export const routes = [
),
},
},
{
path: "import",
component: EHoldingsLocalTitlesFormImport,
meta: {
breadcrumb: () =>
build_breadcrumb(
[
breadcrumb_paths.eholdings_local,
breadcrumbs.eholdings.local
.titles,
],
"Import from a list" // $t("Import from a list")
),
},
},
],
},
{

Loading…
Cancel
Save