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>
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',
--- /dev/null
+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;
}
}
+=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;
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 );
};
}
+=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;
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
"/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":
--- /dev/null
+[% 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 %]
'_str': _("Batch hold cancellation")
},
{
+ '_id': 'create_eholdings_from_biblios',
+ '_str': _("Create eHolding titles")
+ },
+ {
'_id': 'update_elastic_index',
'_str': _("Update Elasticsearch index")
},
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 %]
--- /dev/null
+<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>
><font-awesome-icon icon="plus" />
{{ $t("New title") }}</router-link
>
+
+ <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>
return _fetchResources(apiUrl);
};
-function checkError(response) {
+export const checkError = function(response) {
if (response.status >= 200 && response.status <= 299) {
return response.json();
} else {
console.log(response);
setError("%s (%s)".format(response.statusText, response.status));
}
-}
+};
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";
},
},
{
+ 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,
meta: {
),
},
},
+ {
+ 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")
+ ),
+ },
+ },
],
},
{