From 862a1cd9c613927ded084cb00def4599baf18fae Mon Sep 17 00:00:00 2001 From: Agustin Moyano Date: Thu, 2 Feb 2023 21:07:55 -0300 Subject: [PATCH] Bug 32607: Add CRUD for import sources This patch adds vue components to create, read, update and delete import sources To test: 1. apply patches 2. updatedatabase 3. yarn build_js 4. go to Administration -> Import sources 5. play around with the ui and create, update, remove and search import sources 6. Sign off ;) --- Koha/ImportSource.pm | 56 +++ Koha/ImportSources.pm | 51 +++ Koha/REST/V1/ImportSources.pm | 141 ++++++++ Koha/Schema/Result/ImportSource.pm | 23 ++ admin/import-sources.pl | 38 +++ api/v1/swagger/definitions/import_source.yaml | 13 + api/v1/swagger/paths/import_sources.yaml | 247 ++++++++++++++ api/v1/swagger/swagger.yaml | 15 + .../prog/en/modules/admin/admin-home.tt | 2 + .../prog/en/modules/admin/import-sources.tt | 37 ++ .../prog/js/vue/components/Form.vue | 144 ++++++++ .../prog/js/vue/components/Page.vue | 53 +++ .../vue/components/import-sources/ISEdit.vue | 80 +++++ .../vue/components/import-sources/ISList.vue | 100 ++++++ .../vue/components/import-sources/ISMain.vue | 28 ++ .../prog/js/vue/fetch/import-sources.js | 40 +++ .../prog/js/vue/fetch/patron-api-client.js | 4 + .../intranet-tmpl/prog/js/vue/i18n/index.js | 2 +- .../prog/js/vue/import-source/router.js | 12 + .../prog/js/vue/inputs/autocomplete.js | 199 +++++++++++ .../intranet-tmpl/prog/js/vue/inputs/index.js | 5 + .../prog/js/vue/models/import-sources.js | 85 +++++ .../prog/js/vue/modules/import-sources.ts | 76 +++++ .../prog/js/vue/routes/import-sources.js | 31 ++ .../intranet-tmpl/prog/js/vue/stores/model.js | 51 +++ package.json | 2 + t/db_dependent/Koha/ImportSources.t | 44 +++ t/db_dependent/api/v1/import_sources.t | 318 ++++++++++++++++++ webpack.config.js | 1 + 29 files changed, 1897 insertions(+), 1 deletion(-) create mode 100644 Koha/ImportSource.pm create mode 100644 Koha/ImportSources.pm create mode 100644 Koha/REST/V1/ImportSources.pm create mode 100755 admin/import-sources.pl create mode 100644 api/v1/swagger/definitions/import_source.yaml create mode 100644 api/v1/swagger/paths/import_sources.yaml create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js create mode 100755 t/db_dependent/Koha/ImportSources.t create mode 100755 t/db_dependent/api/v1/import_sources.t diff --git a/Koha/ImportSource.pm b/Koha/ImportSource.pm new file mode 100644 index 0000000000..b23fc95422 --- /dev/null +++ b/Koha/ImportSource.pm @@ -0,0 +1,56 @@ +package Koha::ImportSource; + +# This file is part of Koha. +# +# Copyright 2020 Koha Development Team +# +# 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 +# + +use Modern::Perl; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::ImportSource - Koha ImportSource Object class + +=head1 API + +=head2 Class Methods + +=head3 patron + +my $patron = $import_source->patron + +Return the patron for this import source + +=cut + +sub patron { + my ($self) = @_; + + my $patron_rs = $self->_result->patron; + return Koha::Patron->_new_from_dbic($patron_rs); +} + +=head3 _type + +=cut + +sub _type { + return 'ImportSource'; +} + +1; \ No newline at end of file diff --git a/Koha/ImportSources.pm b/Koha/ImportSources.pm new file mode 100644 index 0000000000..1cdca37b1b --- /dev/null +++ b/Koha/ImportSources.pm @@ -0,0 +1,51 @@ +package Koha::ImportSources; + +# This file is part of Koha. +# +# Copyright 2020 Koha Development Team +# +# 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 +# + +use Modern::Perl; + +use base qw(Koha::Objects); + +use Koha::ImportSource; + +=head1 NAME + +Koha::ImportSources - Koha ImportSources Object class + +=head1 API + +=head2 Class Methods + +=head3 _type + +=cut + +sub _type { + return 'ImportSource'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::ImportSource'; +} + +1; \ No newline at end of file diff --git a/Koha/REST/V1/ImportSources.pm b/Koha/REST/V1/ImportSources.pm new file mode 100644 index 0000000000..b77e2cadca --- /dev/null +++ b/Koha/REST/V1/ImportSources.pm @@ -0,0 +1,141 @@ +package Koha::REST::V1::ImportSources; + +# Copyright 2023 Theke Solutions +# +# 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 . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::ImportSources; + +use Try::Tiny qw( catch try ); + +=head1 API + +=head2 Methods + +=head3 list + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $import_sources_set = Koha::ImportSources->new; + my $import_sources = $c->objects->search( $import_sources_set ); + return $c->render( status => 200, openapi => $import_sources ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $import_sources_set = Koha::ImportSources->new; + my $import_source = $c->objects->find( $import_sources_set, $c->validation->param('import_source_id') ); + unless ($import_source) { + return $c->render( status => 404, + openapi => { error => "Import source not found" } ); + } + + return $c->render( status => 200, openapi => $import_source ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 add + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + my $import_source = Koha::ImportSource->new_from_api( $c->validation->param('body') ); + $import_source->store; + $c->res->headers->location( $c->req->url->to_string . '/' . $import_source->import_source_id ); + return $c->render( + status => 201, + openapi => $import_source->to_api + ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 update + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $import_source = Koha::ImportSources->find( $c->validation->param('import_source_id') ); + + if ( not defined $import_source ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + return try { + $import_source->set_from_api( $c->validation->param('body') ); + $import_source->store(); + return $c->render( status => 200, openapi => $import_source->to_api ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 delete + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $import_source = Koha::ImportSources->find( $c->validation->param('import_source_id') ); + if ( not defined $import_source ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + return try { + $import_source->delete; + return $c->render( + status => 204, + openapi => q{} + ); + } + catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/Koha/Schema/Result/ImportSource.pm b/Koha/Schema/Result/ImportSource.pm index a902cb0892..fa44d0a526 100644 --- a/Koha/Schema/Result/ImportSource.pm +++ b/Koha/Schema/Result/ImportSource.pm @@ -86,4 +86,27 @@ __PACKAGE__->belongs_to( # You can replace this text with custom code or comments, and it will be preserved on regeneration + +=head1 CUSTOM CODE + +=head2 koha_object_class + +name of the corresponding Koha::Object + +=cut + +sub koha_object_class { + 'Koha::ImportSource'; +} + +=head2 koha_objects_class + +name of the corresponding Koha::Objects + +=cut + +sub koha_objects_class { + 'Koha::ImportSources'; +} + 1; diff --git a/admin/import-sources.pl b/admin/import-sources.pl new file mode 100755 index 0000000000..c23c53270a --- /dev/null +++ b/admin/import-sources.pl @@ -0,0 +1,38 @@ +#!/usr/bin/perl + +# Copyright 2023 Theke Solutions +# +# 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 . + +use Modern::Perl; + +use CGI qw ( -utf8 ); +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); +use Koha::Plugins; + +my $query = CGI->new; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "admin/import-sources.tt", + query => $query, + type => "intranet", + flagsrequired => { parameters => 'manage_import_sources' }, + } +); + +output_html_with_http_headers $query, $cookie, $template->output; diff --git a/api/v1/swagger/definitions/import_source.yaml b/api/v1/swagger/definitions/import_source.yaml new file mode 100644 index 0000000000..b7d3555d1d --- /dev/null +++ b/api/v1/swagger/definitions/import_source.yaml @@ -0,0 +1,13 @@ +--- +type: object +properties: + import_source_id: + type: integer + description: internally assigned import source identifier + readOnly: true + name: + description: import source name + type: string + patron_id: + description: linked patron identifier + type: integer diff --git a/api/v1/swagger/paths/import_sources.yaml b/api/v1/swagger/paths/import_sources.yaml new file mode 100644 index 0000000000..63c8b50214 --- /dev/null +++ b/api/v1/swagger/paths/import_sources.yaml @@ -0,0 +1,247 @@ +/import_sources: + get: + x-mojo-to: ImportSources#list + operationId: listImportSources + summary: List import sources + tags: + - import_sources + parameters: + - $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" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - patron + collectionFormat: csv + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: A list of import sources + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + 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: + parameters: manage_import_sources + post: + x-mojo-to: ImportSources#add + operationId: addImportSources + summary: Add an import source + tags: + - import_sources + parameters: + - name: body + in: body + description: A JSON object containing informations about the new import source + required: true + schema: + $ref: "../swagger.yaml#/definitions/import_source" + consumes: + - application/json + produces: + - application/json + responses: + "201": + description: An import source + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + 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: + parameters: manage_import_sources +"/import_sources/{import_source_id}": + get: + x-mojo-to: ImportSources#get + operationId: getImportSources + summary: Get an import source + tags: + - import_sources + parameters: + - $ref: "../swagger.yaml#/parameters/import_source_id_pp" + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: An import source + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: 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: + parameters: manage_import_sources + put: + x-mojo-to: ImportSources#update + operationId: updateImportSources + summary: Update an import source + tags: + - import_sources + parameters: + - $ref: "../swagger.yaml#/parameters/import_source_id_pp" + - name: body + in: body + description: A JSON object containing informations about the new import source + required: true + schema: + $ref: "../swagger.yaml#/definitions/import_source" + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: An import source + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: 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: + parameters: manage_import_sources + delete: + x-mojo-to: ImportSources#delete + operationId: deleteImportSources + summary: Delete an import source + tags: + - import_sources + parameters: + - $ref: "../swagger.yaml#/parameters/import_source_id_pp" + consumes: + - application/json + produces: + - application/json + responses: + "204": + description: Deleted + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: 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: + parameters: manage_import_sources diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 48d8b5af62..cafc1d68fd 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -70,6 +70,8 @@ definitions: $ref: ./definitions/import_batch_profiles.yaml import_record_match: $ref: ./definitions/import_record_match.yaml + import_source: + $ref: ./definitions/import_source.yaml invoice: $ref: ./definitions/invoice.yaml item: @@ -275,6 +277,10 @@ paths: $ref: ./paths/import_batch_profiles.yaml#/~1import_batch_profiles "/import_batch_profiles/{import_batch_profile_id}": $ref: "./paths/import_batch_profiles.yaml#/~1import_batch_profiles~1{import_batch_profile_id}" + /import_sources: + $ref: ./paths/import_sources.yaml#/~1import_sources + "/import_sources/{import_source_id}": + $ref: ./paths/import_sources.yaml#/~1import_sources~1{import_source_id} /items: $ref: ./paths/items.yaml#/~1items "/items/{item_id}": @@ -533,6 +539,12 @@ parameters: name: import_record_id required: true type: integer + import_source_id_pp: + description: Internal import source identifier + in: path + name: import_source_id + required: true + type: integer item_id_pp: description: Internal item identifier in: path @@ -830,6 +842,9 @@ tags: - description: "Manage item groups\n" name: item_groups x-displayName: Item groups + - description: "Manage import sources\n" + name: import_sources + x-displayName: Import source - description: "Manage items\n" name: items x-displayName: Items diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt index 47b2142fea..9bf4480968 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -206,6 +206,8 @@
Search engine configuration (Elasticsearch)
Manage indexes, facets, and their mappings to MARC fields and subfields
[% END %] +
Import sources
+
Define sources to import from
[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt new file mode 100644 index 0000000000..63fd41f541 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/import-sources.tt @@ -0,0 +1,37 @@ +[% USE raw %] +[% USE To %] +[% USE Asset %] +[% USE KohaDates %] +[% USE TablesSettings %] +[% USE AuthorisedValues %] +[% SET footerjs = 1 %] +[% PROCESS 'i18n.inc' %] +[% INCLUDE 'doc-head-open.inc' %] + + Import sources › Koha + +[% INCLUDE 'doc-head-close.inc' %] + + + +[% WRAPPER 'header.inc' %] + [% INCLUDE 'erm-search.inc' %] +[% END %] + +
+ +[% MACRO jsinclude BLOCK %] + [% INCLUDE 'calendar.inc' %] + [% INCLUDE 'datatables.inc' %] + [% INCLUDE 'columns_settings.inc' %] + [% INCLUDE 'js-patron-format.inc' %] + [% INCLUDE 'js-date-format.inc' %] + + + + [% Asset.js("js/vue/dist/import-source.js") | $raw %] + +[% END %] +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue new file mode 100644 index 0000000000..fcdfa5a773 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Form.vue @@ -0,0 +1,144 @@ + + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue new file mode 100644 index 0000000000..bb98c147e7 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Page.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue new file mode 100644 index 0000000000..167d36d90e --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISEdit.vue @@ -0,0 +1,80 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue new file mode 100644 index 0000000000..f649af949b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISList.vue @@ -0,0 +1,100 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue new file mode 100644 index 0000000000..51800b8772 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/import-sources/ISMain.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js new file mode 100644 index 0000000000..b5f0df93aa --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/import-sources.js @@ -0,0 +1,40 @@ +import HttpClient from "./http-client"; + +export class ImportSourcesClient extends HttpClient { + constructor() { + super({ + baseURL: '/api/v1/import_sources', + }); + } + async add({row, headers={}}) { + const requestHeaders = {...headers, 'Content-Type': 'application/json'} + return this.post({ + endpoint: '', + headers: requestHeaders, + body: JSON.stringify(row) + }) + } + + async remove({id, headers={}}) { + return this.delete({ + endpoint: '/'+id, + headers, + }) + } + + async update({id, headers={}, row}) { + const requestHeaders = {...headers, 'Content-Type': 'application/json'} + return this.put({ + endpoint: '/'+id, + headers: requestHeaders, + body: JSON.stringify(row) + }) + } + + async getOne({id, headers={}}) { + return this.get({ + endpoint: '/'+id, + headers, + }) + } +} diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js index 4c39e977fd..4f30a8e2e8 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/patron-api-client.js @@ -13,6 +13,10 @@ export class PatronAPIClient extends HttpClient { this.get({ endpoint: "patrons/" + id, }), + list: (query) => + this.get({ + endpoint: "patrons" + (query ? '?'+query:''), + }) }; } } diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js b/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js index 27c8e75f8c..359bc69377 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/i18n/index.js @@ -6,4 +6,4 @@ export default { install: (app, options) => { app.config.globalProperties.$__ = $__; }, -}; +}; \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js b/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js new file mode 100644 index 0000000000..8fd05b5ce5 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/import-source/router.js @@ -0,0 +1,12 @@ +import { createWebHistory, createRouter } from "vue-router"; + +export default (menuStore) => { + + const { routes } = menuStore + const router = createRouter({ + history: createWebHistory(), + linkExactActiveClass: "current", + routes, + }) + return router +} \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js new file mode 100644 index 0000000000..5333b6e73b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/autocomplete.js @@ -0,0 +1,199 @@ +import { createInput } from '@formkit/vue' + +/** + * This is an input "feature" — a function that accepts a node and exposes + * some additional functionality to an input. When using schemas, this can + * take the place of a traditional "script" block in a Vue component. In this + * example, we expose: + * + * - An input handler `search`. + * - An input handler `selections`. + * - Commit middleware to place filtered options into the `matches` prop. + * + * Once written, input features are added via the input declaration. + */ +const searchFeature = (node) => { + + const _transform_options = (options) => { + if(!Array.isArray(options)) { + return Object.keys(options).map((key) => ({label: options[key], value: key})) + } + return options.map((option) => { + if(typeof option === 'string') return {label: option, value: option} + return option + }) + } + + // We wait for our node to be fully "created" before we start to add our + // handlers to ensure the core Vue plugin has added its context object: + node.on('created', async () => { + // Ensure our matches prop starts as an array. + node.props.matches = [] + node.props.selection = '' + + if(node.value !== undefined && node.props.attrs && typeof node.props.attrs.optionLoader === 'function') { + node.props.selection = await node.props.attrs.optionLoader(node.value) + } + + const clearValue = async (e) => { + if (e && typeof e.preventDefault === 'function') e.preventDefault() + node.input('') + node.props.matches = [] + node.props.selection = '' + node.props.searchValue = '' + setTimeout(() => { + if (document.querySelector('input#' + node.props.id)) { + document.querySelector('input#' + node.props.id).focus() + } + }, 50) + } + // When we actually have an value to set: + const setValue = async (e) => { + if (e && typeof e.preventDefault === 'function') e.preventDefault() + node.input(node.props.selection.value) + node.props.display = node.props.selection.label + node.props.searchValue = '' + await new Promise((r) => setTimeout(r, 50)) // "next tick" + } + + // Perform a soft selection, this is shown as a highlight in the dropdown + const select = (delta) => { + const available = node.props.matches + let idx = available.indexOf(node.props.selection) + delta + if (idx >= available.length) { + idx = 0 + } else if (idx < 0) { + idx = available.length - 1 + } + node.props.selection = available[idx] + } + + // Add some new "handlers" for our autocomplete. The handlers object is + // just a conventionally good place to put event handlers. Auto complete + // inputs always have to deal with lots of keyboard events, so that logic + // is registered here. + Object.assign(node.context.handlers, { + setValue, + clearValue, + selection: (e) => { + // This handler is called when entering data into the search input. + switch (e.key) { + case 'Enter': + return setValue() + case 'ArrowDown': + e.preventDefault() + return select(1) + case 'ArrowUp': + e.preventDefault() + return select(-1) + case 'Escape': + return node.props.matches = [] + } + }, + blur: (e) => { + if(!node.props.hovering) node.props.matches = [] + }, + search(e) { + node.props.searchValue = e.target.value + }, + hover: (e) => { + node.props.hovering = true + node.props.selection = node.props.matches.find(match => String(match.value) === e.target.attributes.value.value) + console.log(node.props.selection) + }, + unhover: (e) => { + if (node.props.matches.find(match => String(match.value) === e.target.attributes.value.value) === node.props.selection) { + node.props.selection = '' + node.props.hovering = false + } + }, + }) + }) + + // Perform filtering when the search value changes + node.on('prop:searchValue', async ({ payload: value }) => { + let results + if(typeof node.props.options === 'function') { + results = _transform_options(await node.props.options(value)) + } else { + results = _transform_options(node.props.options).filter((option) => + option.label.toLowerCase().split(/\W/).find((part).startsWith(value.toLowerCase())) + ) + } + //if (!results.length) results.push('No matches') + node.props.matches = results + }) +} + +/** + * This is our input schema responsible for rendering the inner “input” + * section. In our example, we render an text input which will be used + * to filter search results, and an unordered list that shows all remaining + * matches. + */ +const schema = { + if: '$value', + then: [ + { + $el: 'a', + attrs: { + id: '$id', + href: '#', + class: '$classes.value', + onClick: '$handlers.clearValue', + }, + children: '$selection.label' + } + ], + else: [ + { + $el: 'input', + bind: '$attrs', + attrs: { + id: '$id', + class: '$classes.input', + onKeydown: '$handlers.selection', + onInput: '$handlers.search', + onBlur: '$handlers.blur', + value: '$searchValue', + }, + }, + { + $el: 'ul', + if: '$matches.length', + attrs: { + class: '$classes.dropdown', + }, + children: [ + { + $el: 'li', + for: ['match', '$matches'], + attrs: { + 'data-selected': { + if: '$selection === $match', + then: 'true', + else: 'false', + }, + value: '$match.value', + class: '$classes.dropdownItem', + onClick: '$handlers.setValue', + onMouseenter: '$handlers.hover', + onMouseleave: '$handlers.unhover', + }, + children: '$match.label', + }, + ], + }, + ], +} + +/** + * Finally we create our actual input declaration by using `createInput` this + * places our schema into a "standard" FormKit schema feature set with slots, + * labels, help, messages etc. The return value of this function is a proper + * input declaration. + */ +export default createInput(schema, { + props: ['options', 'matches', 'selection', 'searchValue', 'value'], + features: [searchFeature], +}) diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js new file mode 100644 index 0000000000..fb70888a28 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/inputs/index.js @@ -0,0 +1,5 @@ +import autocomplete from './autocomplete' + +export default { + autocomplete +} \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js new file mode 100644 index 0000000000..53d05c409b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/models/import-sources.js @@ -0,0 +1,85 @@ + +import PatronAPIClient from "../fetch/patron-api-client"; +import { $__ } from "../i18n"; + +const api = new PatronAPIClient() + +export default [ + { + name: 'import_source_id', + table: { + data: 'import_source_id', + visible: false + }, + form: { + type: 'hidden' + } + }, + { + name: 'name', + title: $__('Source name'), + table: { + data: 'name', + searchable: true + }, + form: { + type: 'text', + label: { + $ref: 'title' + }, + validation: 'required' + } + }, + { + name: 'patron_id', + title: $__('Patron'), + table: { + searchable: true, + data: "patron.firstname:patron.surname:patron.middle_name", + render: (data, type, row) => { + const {firstname, surname, middle_name} = row.patron + return [firstname, middle_name, surname].filter(part => (part||'').trim()).join(' '); + } + }, + form: { + type: 'autocomplete', + label: 'Choose a patron', + validation: 'required', + optionLoader: async (id) => { + const {firstname, surname, middle_name, patron_id} = ( await api.patrons.get(id) || {} ); + return {value: patron_id, label: [firstname, middle_name, surname].filter(part => (part||'').trim()).join(' ')} + }, + options: async (value) => { + let query = ''; + if(value !== undefined) { + query = new URLSearchParams({ + q: JSON.stringify([ + { + firstname: { + like: '%'+value+'%' + } + }, + { + middle_name: { + like: '%'+value+'%' + } + }, + { + surname: { + like: '%'+value+'%' + } + } + ]) + }) + } + + const response = await api.patrons.list(query) + if(!response) return [] + return response.map(row => { + const {firstname, surname, middle_name, patron_id} = row + return {value: patron_id, label: [firstname, middle_name, surname].filter(part => (part||'').trim()).join(' ')} + }) + } + } + } +] diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts new file mode 100644 index 0000000000..2ed204e77f --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts @@ -0,0 +1,76 @@ +import { createApp } from "vue"; +import { createPinia } from "pinia"; +import { plugin as formkit, defaultConfig } from '@formkit/vue' +import { createWebHistory, createRouter } from "vue-router"; +import '@formkit/themes/genesis' + +import { library } from "@fortawesome/fontawesome-svg-core"; +import { + faPlus, + faMinus, + faPencil, + faTrash, + faSpinner, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; +import vSelect from "vue-select"; +import { useNavigationStore } from '../stores/navigation'; +import { useMainStore } from '../stores/main'; +import { useModelStore } from '../stores/model'; +import routesDef from '../routes/import-sources'; +import model from '../models/import-sources'; + +library.add(faPlus, faMinus, faPencil, faTrash, faSpinner); + +const pinia = createPinia(); +const navigationStore = useNavigationStore(pinia); +const mainStore = useMainStore(pinia); +const modelStore = useModelStore(pinia); +const { removeMessages } = mainStore; +const { setRoutes } = navigationStore; +const { setModel } = modelStore; +const routes = setRoutes(routesDef); +setModel(model); + +const router = createRouter({ + history: createWebHistory(), + linkExactActiveClass: "current", + routes, +}) + +import App from "../components/import-sources/ISMain.vue"; + +// import { routes } from "./routes"; + +// const router = createRouter({ +// history: createWebHistory(), +// linkExactActiveClass: "current", +// routes, +// }); + +// import { useAVStore } from "../stores/authorised_values"; + +import i18n from "../i18n" +import inputs from "../inputs" +const app = createApp(App); + +const rootComponent = app + .use(i18n) + .use(pinia) + .use(router) + .use(formkit, defaultConfig({ + inputs + })) + .component("font-awesome-icon", FontAwesomeIcon) + .component("v-select", vSelect); + +app.config.unwrapInjectedRef = true; +app.provide("mainStore", mainStore); +app.provide("navigationStore", navigationStore); +app.provide("modelStore", modelStore); +app.mount("#import-source"); + +router.beforeEach((to, from) => { + navigationStore.$patch({current: to.meta.self}); + removeMessages(); // This will actually flag the messages as displayed already +}); diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js new file mode 100644 index 0000000000..471dc59472 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/routes/import-sources.js @@ -0,0 +1,31 @@ +import ISEdit from '../components/import-sources/ISEdit.vue' +import ISList from '../components/import-sources/ISList.vue' + +export default { + title: 'Administration', + href: '/cgi-bin/koha/admin/admin-home.pl', + children: [ + { + title: 'Import sources', + path: '/cgi-bin/koha/admin/import-sources.pl', + component: {template: '', name: 'ISBase'}, + children: [ + { + title: 'List', + path: '', + component: ISList + }, + { + title: 'Add import source', + path: 'add', + component: ISEdit + }, + { + title: 'Edit import source', + path: ':id', + component: ISEdit + } + ] + } + ] +} \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js new file mode 100644 index 0000000000..f0ccd58acd --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/stores/model.js @@ -0,0 +1,51 @@ +import { defineStore } from "pinia"; + +export const useModelStore = defineStore("model", { + state: () => ({ + model: [] + }), + actions: { + setModel(model) { + this.model = model + }, + getFormSchema() { + return this.model.map(row => { + const {table, form = {}, ...common} = row + Object.keys(form).forEach(key => { + if (key === 'type') { + form.$formkit = form[key] + key = '$formkit' + delete form.type; + } + if(form[key].$ref) { + const path = form[key].$ref.split('.') + form[key] = path.reduce((obj, key, idx) => { + if(obj === undefined || obj[key] === undefined) return undefined + const value = obj[key] + if(idx === path.lenght - 1) delete obj[key] + return value + }, common) + } + }) + return {...common, ...form} + }) + }, + getTableColumns() { + return this.model.map(row => { + const {table = {}, form, ...common} = row + Object.keys(table).forEach(key => { + if(table[key].$ref) { + const path = table[key].$ref.split('.') + table[key] = path.reduce((obj, key, idx) => { + if(obj === undefined || obj[key] === undefined) return undefined + const value = obj[key] + if(idx === path.lenght - 1) delete obj[key] + return value + }, common) + } + }) + return {...common, ...table} + }) + } + }, +}); diff --git a/package.json b/package.json index 1a4d20e98e..940b59af81 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "dependencies": { "@cypress/vue": "^3.1.1", "@cypress/webpack-dev-server": "^1.8.3", + "@formkit/themes": "^1.0.0-beta.14", + "@formkit/vue": "^1.0.0-beta.14", "@fortawesome/fontawesome-svg-core": "^6.1.0", "@fortawesome/free-solid-svg-icons": "^6.0.0", "@fortawesome/vue-fontawesome": "^3.0.0-5", diff --git a/t/db_dependent/Koha/ImportSources.t b/t/db_dependent/Koha/ImportSources.t new file mode 100755 index 0000000000..6788dab4ee --- /dev/null +++ b/t/db_dependent/Koha/ImportSources.t @@ -0,0 +1,44 @@ +#!/usr/bin/perl + +# Copyright 2023 Theke Solutions +# +# 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 . + +use Modern::Perl; + +use Test::More tests => 1; + +use Koha::ImportSources; + +use t::lib::TestBuilder; +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'patron' => sub { + plan tests => 1; + + $schema->storage->txn_begin; + + my $patron = $builder->build_object({class => 'Koha::Patrons', value => {firstname => 'Afirstname'}}); + + my $import_source = $builder->build_object({class => 'Koha::ImportSources', value => {name => 'import_source', patron_id => $patron->borrowernumber}}); + + my $fetched_patron = $import_source->patron; + + is($fetched_patron->firstname, 'Afirstname', 'Patron matches'); + + $schema->storage->txn_rollback; +}; \ No newline at end of file diff --git a/t/db_dependent/api/v1/import_sources.t b/t/db_dependent/api/v1/import_sources.t new file mode 100755 index 0000000000..2175a20754 --- /dev/null +++ b/t/db_dependent/api/v1/import_sources.t @@ -0,0 +1,318 @@ +#!/usr/bin/env perl + +# Copyright 2023 Theke Solutions +# +# 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 . + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use C4::Auth; +use Koha::ImportSources; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +my $t = Test::Mojo->new('Koha::REST::V1'); + +subtest 'list() tests' => sub { + + plan tests => 12; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( + { + class => 'Koha::ImportSources' + } + ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + for ( 1..10 ) { + $builder->build_object( + { + class => 'Koha::ImportSources' + } + ); + } + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( + { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/import_sources" ) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/import_sources?_per_page=10" ) + ->status_is( 200, 'SWAGGER3.2.2' ); + + my $response_count = scalar @{ $t->tx->res->json }; + + is( $response_count, 10, 'The API returns 10 sources' ); + + my $id = $source->import_source_id; + $t->get_ok( "//$userid:$password@/api/v1/import_sources?q={\"import_source_id\": $id}" ) + ->status_is(200) + ->json_is( '' => [ $source->to_api ], 'SWAGGER3.3.2'); + + $source->delete; + + $t->get_ok( "//$userid:$password@/api/v1/import_sources?q={\"import_source_id\": $id}" ) + ->status_is(200) + ->json_is( '' => [] ); + + $schema->storage->txn_rollback; + +}; + +subtest 'get() tests' => sub { + + plan tests => 9; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( + { + class => 'Koha::ImportSources' + } + ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( + { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + my $id = $source->import_source_id; + + $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" ) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" ) + ->status_is( 200, 'SWAGGER3.2.2' ) + ->json_is( '' => $source->to_api, 'SWAGGER3.3.2' ); + + $source->delete; + + $t->get_ok( "//$userid:$password@/api/v1/import_sources/$id" ) + ->status_is(404) + ->json_is( '/error' => 'Import source not found' ); + + $schema->storage->txn_rollback; + +}; + +subtest 'delete() tests' => sub { + + plan tests => 6; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( + { + class => 'Koha::ImportSources' + } + ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( + { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + my $id = $source->import_source_id; + + $t->delete_ok( "//$userid:$password@/api/v1/import_sources/$id" ) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->delete_ok( "//$userid:$password@/api/v1/import_sources/$id" ) + ->status_is( 204, 'SWAGGER3.2.2' ); + + my $deleted_source = Koha::ImportSources->search({import_source_id => $id}); + + is($deleted_source->count, 0, 'No import source found'); + + $schema->storage->txn_rollback; + +}; + +subtest 'add() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + Koha::ImportSources->delete; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( + { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + my $patron_id = $nonprivilegedpatron->borrowernumber; + + $t->post_ok( "//$userid:$password@/api/v1/import_sources" => json => { name => 'test1', patron_id => $patron_id }) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->post_ok( "//$userid:$password@/api/v1/import_sources" => json => { name => 'test1', patron_id => $patron_id }) + ->status_is( 201, 'SWAGGER3.2.2' ) + ->json_is('/name', 'test1') + ->json_is('/patron_id', $patron_id); + + my $created_source = Koha::ImportSources->search->next; + + is($created_source->name, 'test1', 'Import source found'); + + $schema->storage->txn_rollback; + +}; + +subtest 'update() tests' => sub { + + plan tests => 7; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( + { + class => 'Koha::ImportSources', + value => { name => 'Oldname' } + } + ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( + { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + my $id = $source->import_source_id; + + $t->put_ok( "//$userid:$password@/api/v1/import_sources/$id" => json => { name => 'Newname', patron_id => $source->patron_id } ) + ->status_is(403) + ->json_is( + '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->put_ok( "//$userid:$password@/api/v1/import_sources/$id" => json => { name => 'Newname', patron_id => $source->patron_id } ) + ->status_is( 200, 'SWAGGER3.2.2' ) + ->json_is('/name', 'Newname'); + + my $updated_source = Koha::ImportSources->find($id); + + is($updated_source->name, 'Newname', 'Import source updated'); + + $schema->storage->txn_rollback; + +}; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index bbdcced844..5a6cff9fd7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,6 +6,7 @@ const webpack = require('webpack'); module.exports = { entry: { erm: "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/erm.ts", + "import-source": "./koha-tmpl/intranet-tmpl/prog/js/vue/modules/import-sources.ts" }, output: { filename: "[name].js", -- 2.30.2