--- /dev/null
+package Koha::ERM::Agreement::License;
+
+# 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 Koha::Database;
+
+use Koha::Agreements;
+use Koha::Licenses;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::ERM::Agreement::License - Koha Agreement License Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 license
+
+Return the license
+
+=cut
+
+sub license {
+ my ( $self ) = @_;
+ my $license_rs = $self->_result->license;
+ return Koha::ERM::License->_new_from_dbic($license_rs);
+}
+
+=head3 agreement
+
+Return the agreement
+
+=cut
+
+sub agreement {
+ my ( $self ) = @_;
+ my $agreement_rs = $self->_result->agreement;
+ return Koha::ERM::Agreement->_new_from_dbic($agreement_rs);
+}
+
+=head2 Internal methods
+
+=head3 _type
+
+=cut
+
+sub _type {
+ return 'ErmAgreementLicense';
+}
+
+1;
--- /dev/null
+package Koha::ERM::Agreement::Licenses;
+
+# 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 Koha::Database;
+
+use Koha::ERM::Agreement::License;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::ERM::Agreement::Licenses- Koha Agreement License Object set class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+ return 'ErmAgreementLicense';
+}
+
+=head3 object_class
+
+=cut
+
+sub object_class {
+ return 'Koha::ERM::Agreement::License';
+}
+
+1;
--- /dev/null
+package Koha::ERM::License;
+
+# 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 Koha::Database;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::ERM::License - Koha ERM License Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head2 Internal methods
+
+=head3 _type
+
+=cut
+
+sub _type {
+ return 'ErmLicense';
+}
+
+1;
--- /dev/null
+package Koha::ERM::Licenses;
+
+# 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 Koha::Database;
+
+use Koha::ERM::License;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::ERM::Licenses - Koha ERM License Object set class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+ return 'ErmLicense';
+}
+
+=head3 object_class
+
+=cut
+
+sub object_class {
+ return 'Koha::ERM::License';
+}
+
+1;
--- /dev/null
+package Koha::REST::V1::ERM::Licenses;
+
+# 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::ERM::Licenses;
+
+use Scalar::Util qw( blessed );
+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 $licenses_set = Koha::ERM::Licenses->new;
+ my $licenses = $c->objects->search( $licenses_set );
+ return $c->render( status => 200, openapi => $licenses );
+ }
+ catch {
+ $c->unhandled_exception($_);
+ };
+
+}
+
+=head3 get
+
+Controller function that handles retrieving a single Koha::ERM::License object
+
+=cut
+
+sub get {
+ my $c = shift->openapi->valid_input or return;
+
+ return try {
+ my $license_id = $c->validation->param('license_id');
+ my $license = $c->objects->find( Koha::ERM::Licenses->search, $license_id );
+
+ unless ($license) {
+ return $c->render(
+ status => 404,
+ openapi => { error => "License not found" }
+ );
+ }
+
+ return $c->render(
+ status => 200,
+ openapi => $license
+ );
+ }
+ catch {
+ $c->unhandled_exception($_);
+ };
+}
+
+=head3 add
+
+Controller function that handles adding a new Koha::ERM::License object
+
+=cut
+
+sub add {
+ my $c = shift->openapi->valid_input or return;
+
+ return try {
+ Koha::Database->new->schema->txn_do(
+ sub {
+
+ my $body = $c->validation->param('body');
+
+ my $license = Koha::ERM::License->new_from_api($body)->store;
+
+ $c->res->headers->location($c->req->url->to_string . '/' . $license->license_id);
+ return $c->render(
+ status => 201,
+ openapi => $license->to_api
+ );
+ }
+ );
+ }
+ catch {
+
+ my $to_api_mapping = Koha::ERM::License->new->to_api_mapping;
+
+ if ( blessed $_ ) {
+ if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
+ return $c->render(
+ status => 409,
+ openapi => { error => $_->error, conflict => $_->duplicate_id }
+ );
+ }
+ elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
+ return $c->render(
+ status => 400,
+ openapi => {
+ error => "Given "
+ . $to_api_mapping->{ $_->broken_fk }
+ . " does not exist"
+ }
+ );
+ }
+ elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
+ return $c->render(
+ status => 400,
+ openapi => {
+ error => "Given "
+ . $to_api_mapping->{ $_->parameter }
+ . " does not exist"
+ }
+ );
+ }
+ }
+
+ $c->unhandled_exception($_);
+ };
+}
+
+=head3 update
+
+Controller function that handles updating a Koha::ERM::License object
+
+=cut
+
+sub update {
+ my $c = shift->openapi->valid_input or return;
+
+ my $license_id = $c->validation->param('license_id');
+ my $license = Koha::ERM::Licenses->find( $license_id );
+
+ unless ($license) {
+ return $c->render(
+ status => 404,
+ openapi => { error => "License not found" }
+ );
+ }
+
+ return try {
+ Koha::Database->new->schema->txn_do(
+ sub {
+
+ my $body = $c->validation->param('body');
+
+ $license->set_from_api($body)->store;
+
+ $c->res->headers->location($c->req->url->to_string . '/' . $license->license_id);
+ return $c->render(
+ status => 200,
+ openapi => $license->to_api
+ );
+ }
+ );
+ }
+ catch {
+ my $to_api_mapping = Koha::ERM::License->new->to_api_mapping;
+
+ if ( blessed $_ ) {
+ if ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
+ return $c->render(
+ status => 400,
+ openapi => {
+ error => "Given "
+ . $to_api_mapping->{ $_->broken_fk }
+ . " does not exist"
+ }
+ );
+ }
+ elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
+ return $c->render(
+ status => 400,
+ openapi => {
+ error => "Given "
+ . $to_api_mapping->{ $_->parameter }
+ . " does not exist"
+ }
+ );
+ }
+ }
+
+ $c->unhandled_exception($_);
+ };
+};
+
+=head3 delete
+
+=cut
+
+sub delete {
+ my $c = shift->openapi->valid_input or return;
+
+ my $license = Koha::ERM::Licenses->find( $c->validation->param('license_id') );
+ unless ($license) {
+ return $c->render(
+ status => 404,
+ openapi => { error => "License not found" }
+ );
+ }
+
+ return try {
+ $license->delete;
+ return $c->render(
+ status => 204,
+ openapi => q{}
+ );
+ }
+ catch {
+ $c->unhandled_exception($_);
+ };
+}
+
+1;
--- /dev/null
+---
+type: object
+properties:
+ license_id:
+ type: integer
+ description: internally assigned license identifier
+ readOnly: true
+ name:
+ description: name of the license
+ type: string
+ description:
+ description: description of the license
+ type:
+ - string
+ - "null"
+ type:
+ description: description of the license
+ type:
+ - string
+ - "null"
+ status:
+ description: status of the license
+ type: string
+ started_on:
+ type:
+ - string
+ - "null"
+ format: date
+ description: Start of the license
+ ended_on:
+ type:
+ - string
+ - "null"
+ format: date
+ description: End of the license
+additionalProperties: false
+required:
+ - license_id
+ - name
+ - status
--- /dev/null
+---
+type: object
+properties:
+ license_id:
+ type: integer
+ description: internally assigned license identifier
+ readOnly: true
+ agreement_id:
+ description: foreign key to agreements
+ type: integer
+ status:
+ description: status of the license
+ type:
+ - string
+ - "null"
+ physical_location:
+ description: physical location of the license
+ type:
+ - string
+ - "null"
+ notes:
+ description: notes about the license
+ type:
+ - string
+ - "null"
+ uri:
+ description: URI of the license
+ type:
+ - string
+ - "null"
+additionalProperties: false
+required:
+ - license_id
+ - agreement_id
--- /dev/null
+---
+/erm/licenses:
+ get:
+ x-mojo-to: ERM::Licenses#list
+ operationId: listErmLicenses
+ tags:
+ - license
+ summary: List licenses for agreements
+ produces:
+ - application/json
+ parameters:
+ - description: Case insensitive search on license license_id
+ in: query
+ name: license_id
+ required: false
+ type: integer
+ - description: Case insensitive search on license name
+ in: query
+ name: name
+ required: false
+ type: string
+ - description: Case insensitive search on license type
+ in: query
+ name: type
+ required: false
+ type: string
+ - description: Case insensitive search on license status
+ in: query
+ name: status
+ required: false
+ type: string
+ - description: Case insensitive search on license start date
+ in: query
+ name: started_on
+ required: false
+ type: string
+ - description: Case insensitive search on license end date
+ in: query
+ name: ended_on
+ required: false
+ type: string
+ - $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"
+ responses:
+ 200:
+ description: A list of agreements' licenses
+ schema:
+ items:
+ $ref: "../swagger.yaml#/definitions/erm_license"
+ type: array
+ 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:
+ erm: 1
+ post:
+ x-mojo-to: ERM::Licenses#add
+ operationId: addERMLicenses
+ tags:
+ - license
+ summary: Add license
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ parameters:
+ - description: A JSON object containing information about the new agreement's license
+ in: body
+ name: body
+ required: true
+ schema:
+ $ref: "../swagger.yaml#/definitions/erm_license"
+ responses:
+ 201:
+ description: A successfully created license
+ schema:
+ items:
+ $ref: "../swagger.yaml#/definitions/erm_license"
+ 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/licenses/{license_id}":
+ get:
+ x-mojo-to: ERM::Licenses#get
+ operationId: getERMlicense
+ tags:
+ - license
+ summary: get license
+ produces:
+ - application/json
+ parameters:
+ - $ref: "../swagger.yaml#/parameters/license_id_pp"
+ responses:
+ 200:
+ description: license
+ schema:
+ items:
+ $ref: "../swagger.yaml#/definitions/erm_license"
+ 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"
+ 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
+ x-koha-embed:
+ - agreements
+ put:
+ x-mojo-to: ERM::Licenses#update
+ operationId: updateERMlicenses
+ tags:
+ - license
+ summary: update license
+ consumes:
+ - application/json
+ produces:
+ - application/json
+ parameters:
+ - $ref: "../swagger.yaml#/parameters/license_id_pp"
+ - name: body
+ in: body
+ description: a json object containing new information about existing license
+ required: true
+ schema:
+ $ref: "../swagger.yaml#/definitions/erm_license"
+
+ responses:
+ 200:
+ description: a successfully updated license
+ schema:
+ items:
+ $ref: "../swagger.yaml#/definitions/erm_license"
+ 400:
+ description: bad parameter
+ 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 updating 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
+ x-koha-embed:
+ - agreements
+ delete:
+ x-mojo-to: ERM::Licenses#delete
+ operationId: deleteERMlicenses
+ tags:
+ - license
+ summary: Delete license
+ produces:
+ - application/json
+ parameters:
+ - $ref: "../swagger.yaml#/parameters/license_id_pp"
+ responses:
+ 204:
+ description: license deleted
+ 400:
+ description: license deletion failed
+ 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 deleting 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
$ref: ./definitions/city.yaml
erm_agreement:
$ref: ./definitions/erm_agreement.yaml
+ erm_license:
+ $ref: ./definitions/erm_license.yaml
error:
$ref: ./definitions/error.yaml
fund:
$ref: ./paths/erm_agreements.yaml#/~1erm~1agreements
"/erm/agreements/{agreement_id}":
$ref: "./paths/erm_agreements.yaml#/~1erm~1agreements~1{agreement_id}"
+ /erm/licenses:
+ $ref: ./paths/erm_licenses.yaml#/~1erm~1licenses
+ "/erm/licenses/{license_id}":
+ $ref: "./paths/erm_licenses.yaml#/~1erm~1licenses~1{license_id}"
/erm/users:
$ref: ./paths/erm_users.yaml#/~1erm~1users
/holds:
name: library_id
required: true
type: string
+ license_id_pp:
+ description: License internal identifier
+ in: path
+ name: license_id
+ required: true
+ type: integer
match:
description: Matching criteria
enum:
--- /dev/null
+import { mount } from "@cypress/vue";
+const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!)
+ Also note that moment.js is deprecated */
+
+const dates = {
+ today_iso: dayjs().format("YYYY-MM-DD"),
+ today_us: dayjs().format("MM/DD/YYYY"),
+ tomorrow_iso: dayjs().add(1, "day").format("YYYY-MM-DD"),
+ tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"),
+};
+function get_license() {
+ return {
+ license_id: 1,
+ name: "license 1",
+ description: "my first license",
+ type: "local",
+ status: "active",
+ started_on: dates["today_iso"],
+ ended_on: dates["tomorrow_iso"],
+ };
+}
+
+describe("License CRUD operations", () => {
+ beforeEach(() => {
+ cy.login("koha", "koha");
+ cy.title().should("eq", "Koha staff interface");
+ });
+
+ it("List license", () => {
+ // GET license returns 500
+ cy.intercept("GET", "/api/v1/erm/licenses", {
+ statusCode: 500,
+ error: "Something went wrong",
+ });
+ cy.visit("/cgi-bin/koha/erm/erm.pl");
+ cy.get("#navmenulist").contains("Licenses").click();
+ cy.get("main div[class='dialog alert']").contains(
+ /Something went wrong/
+ );
+
+ // GET licenses returns empty list
+ cy.intercept("GET", "/api/v1/erm/licenses*", []);
+ cy.visit("/cgi-bin/koha/erm/licenses");
+ cy.get("#licenses_list").contains("There are no licenses defined.");
+
+ // GET licenses returns something
+ let license = get_license();
+ let licenses = [license];
+
+ cy.intercept("GET", "/api/v1/erm/licenses*", {
+ statusCode: 200,
+ body: licenses,
+ headers: {
+ "X-Base-Total-Count": "1",
+ "X-Total-Count": "1",
+ },
+ });
+ cy.intercept("GET", "/api/v1/erm/licenses/*", license);
+ cy.visit("/cgi-bin/koha/erm/licenses");
+ cy.get("#licenses_list").contains("Showing 1 to 1 of 1 entries");
+ });
+
+ it("Add license", () => {
+ // Click the button in the toolbar
+ cy.visit("/cgi-bin/koha/erm/licenses");
+ cy.contains("New license").click();
+ cy.get("#licenses_add h2").contains("New license");
+
+ // Fill in the form for normal attributes
+ let license = get_license();
+
+ cy.get("#licenses_add").contains("Submit").click();
+ cy.get("input:invalid,textarea:invalid,select:invalid").should(
+ "have.length",
+ 4
+ );
+ cy.get("#license_name").type(license.name);
+ cy.get("#license_description").type(license.description);
+ cy.get("#licenses_add").contains("Submit").click();
+ cy.get("#license_type").select(license.type);
+ cy.get("#license_status").select(license.status);
+
+ cy.get("#started_on").click();
+ cy.get(".flatpickr-calendar")
+ .eq(0)
+ .find("span.today")
+ .click({ force: true });
+
+ cy.get("#ended_on").click();
+ cy.get(".flatpickr-calendar")
+ .eq(1)
+ .find("span.today")
+ .next("span")
+ .click();
+
+ // Submit the form, get 500
+ cy.intercept("POST", "/api/v1/erm/licenses", {
+ statusCode: 500,
+ error: "Something went wrong",
+ });
+ cy.get("#licenses_add").contains("Submit").click();
+ cy.get("main div[class='dialog alert']").contains(
+ "Something went wrong: Internal Server Error"
+ );
+
+ // Submit the form, success!
+ cy.intercept("POST", "/api/v1/erm/licenses", {
+ statusCode: 201,
+ body: license,
+ });
+ cy.get("#licenses_add").contains("Submit").click();
+ cy.get("main div[class='dialog message']").contains(
+ "License created"
+ );
+ });
+
+ it("Edit license", () => {
+ let license = get_license();
+ let licenses = [license];
+ // Click the 'Edit' button from the list
+ cy.intercept("GET", "/api/v1/erm/licenses*", {
+ statusCode: 200,
+ body: licenses,
+ headers: {
+ "X-Base-Total-Count": "1",
+ "X-Total-Count": "1",
+ },
+ });
+ cy.intercept("GET", "/api/v1/erm/licenses/*", license).as(
+ "get-license"
+ );
+ cy.visit("/cgi-bin/koha/erm/licenses");
+ cy.get("#licenses_list table tbody tr:first")
+ .contains("Edit")
+ .click();
+ cy.wait("@get-license");
+ cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
+ cy.get("#licenses_add h2").contains("Edit license");
+
+ // Form has been correctly filled in
+ cy.get("#license_name").should("have.value", license.name);
+ cy.get("#license_description").should(
+ "have.value",
+ license.description
+ );
+ cy.get("#license_type").should("have.value", license.type);
+ cy.get("#license_status").should("have.value", license.status);
+ cy.get("#started_on").invoke("val").should("eq", dates["today_us"]);
+ cy.get("#ended_on").invoke("val").should("eq", dates["tomorrow_us"]);
+
+ // Submit the form, get 500
+ cy.intercept("PUT", "/api/v1/erm/licenses/*", {
+ statusCode: 500,
+ error: "Something went wrong",
+ });
+ cy.get("#licenses_add").contains("Submit").click();
+ cy.get("main div[class='dialog alert']").contains(
+ "Something went wrong: Internal Server Error"
+ );
+
+ // Submit the form, success!
+ cy.intercept("PUT", "/api/v1/erm/licenses/*", {
+ statusCode: 200,
+ body: license,
+ });
+ cy.get("#licenses_add").contains("Submit").click();
+ cy.get("main div[class='dialog message']").contains(
+ "License updated"
+ );
+ });
+
+ it("Show license", () => {
+ let license = get_license();
+ let licenses = [license];
+ // Click the "name" link from the list
+ cy.intercept("GET", "/api/v1/erm/licenses*", {
+ statusCode: 200,
+ body: licenses,
+ headers: {
+ "X-Base-Total-Count": "1",
+ "X-Total-Count": "1",
+ },
+ });
+ cy.intercept("GET", "/api/v1/erm/licenses/*", license).as(
+ "get-license"
+ );
+ cy.visit("/cgi-bin/koha/erm/licenses");
+ let name_link = cy.get(
+ "#licenses_list table tbody tr:first td:first a"
+ );
+ name_link.should(
+ "have.text",
+ license.name + " (#" + license.license_id + ")"
+ );
+ name_link.click();
+ cy.wait("@get-license");
+ cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
+ cy.get("#licenses_show h2").contains(
+ "License #" + license.license_id
+ );
+ });
+
+ it("Delete license", () => {
+ let license = get_license();
+ let licenses = [license];
+
+ // Click the 'Delete' button from the list
+ cy.intercept("GET", "/api/v1/erm/licenses*", {
+ statusCode: 200,
+ body: licenses,
+ headers: {
+ "X-Base-Total-Count": "1",
+ "X-Total-Count": "1",
+ },
+ });
+ cy.intercept("GET", "/api/v1/erm/licenses/*", license);
+ cy.visit("/cgi-bin/koha/erm/licenses");
+
+ cy.get("#licenses_list table tbody tr:first")
+ .contains("Delete")
+ .click();
+ cy.get("#licenses_confirm_delete h2").contains("Delete license");
+ cy.contains("License name: " + license.name);
+
+ // Submit the form, get 500
+ cy.intercept("DELETE", "/api/v1/erm/licenses/*", {
+ statusCode: 500,
+ error: "Something went wrong",
+ });
+ cy.contains("Yes, delete").click();
+ cy.get("main div[class='dialog alert']").contains(
+ "Something went wrong: Internal Server Error"
+ );
+
+ // Submit the form, success!
+ cy.intercept("DELETE", "/api/v1/erm/licenses/*", {
+ statusCode: 204,
+ body: null,
+ });
+ cy.contains("Yes, delete").click();
+ cy.get("main div[class='dialog message']").contains(
+ "License deleted"
+ );
+ });
+});
('ERM_AGREEMENT_RENEWAL_PRIORITY', 'cancel', 'Cancel'),
('ERM_AGREEMENT_USER_ROLES', 'librarian', 'ERM librarian'),
('ERM_AGREEMENT_USER_ROLES', 'subject_specialist', 'Subject specialist'),
+ ('ERM_LICENSE_TYPE', 'local', 'Local'),
+ ('ERM_LICENSE_TYPE', 'consortial', 'Consortial'),
+ ('ERM_LICENSE_TYPE', 'national', 'National'),
+ ('ERM_LICENSE_TYPE', 'alliance', 'Alliance'),
+ ('ERM_LICENSE_STATUS', 'in_negotiation', 'In negociation'),
+ ('ERM_LICENSE_STATUS', 'not_yet_active', 'Not yet active'),
+ ('ERM_LICENSE_STATUS', 'active', 'Active'),
+ ('ERM_LICENSE_STATUS', 'rejected', 'Rejected'),
+ ('ERM_LICENSE_STATUS', 'expired', 'Expired'),
('ERM_AGREEMENT_LICENSE_STATUS', 'controlling', 'Controlling'),
('ERM_AGREEMENT_LICENSE_STATUS', 'future', 'Future'),
('ERM_AGREEMENT_LICENSE_STATUS', 'history', 'Historic'),
const agreement_user_roles = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_USER_ROLES')) | $raw %];
var table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
- var agreements_table_url = '/api/v1/erm/agreements?';
+ var agreements_table_url = '/api/v1/erm/agreements?';
[% IF agreement_name_filter %]
var agreement_name_filter = {
'name': {
};
agreements_table_url += 'q='+ encodeURIComponent(JSON.stringify(agreement_name_filter));
[% END %]
+
+ const license_types = [% To.json(AuthorisedValues.Get('ERM_LICENSE_TYPE')) | $raw %];
+ const license_statuses = [% To.json(AuthorisedValues.Get('ERM_LICENSE_STATUS')) | $raw %];
+
+ var table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
+
+ var licenses_table_url = '/api/v1/erm/licenses?';
+ [% IF license_name_filter %]
+ var license_name_filter = {
+ 'name': {
+ "like": '%[%- license_name_filter | html -%]%'
+ }
+ };
+ licenses_table_url += 'q='+ encodeURIComponent(JSON.stringify(license_name_filter));
+ [% END %]
+
</script>
[% Asset.js("js/vue/dist/main.js") | $raw %]
+++ /dev/null
-<template>
- <a class="btn btn-default btn-xs" role="button"
- ><i class="fa fa-trash" aria-hidden="true" /> Delete</a
- >
-</template>
-
-<script>
-export default {
- name: "AgreementsButtonDelete"
-}
-</script>
+++ /dev/null
-<template>
- <a class="btn btn-default btn-xs" role="button"
- ><i class="fa fa-pencil" aria-hidden="true" /> Edit</a
- >
-</template>
-
-<script>
-export default {
- name: "AgreementsButtonEdit",
-}
-</script>
<template>
<div>
- <table v-if="agreements.length" id="my_table"></table>
+ <table v-if="agreements.length" id="agreement_list"></table>
<div v-else-if="this.initialized" class="dialog message">
There are no agreements defined.
</div>
</template>
<script>
-import AgreementsButtonEdit from "./AgreementsButtonEdit.vue"
-import AgreementsButtonDelete from "./AgreementsButtonDelete.vue"
+import ButtonEdit from "./ButtonEdit.vue"
+import ButtonDelete from "./ButtonDelete.vue"
import { createVNode, defineComponent, render, resolveComponent } from 'vue'
export default {
created() {
let show_agreement = this.show_agreement
let edit_agreement = this.edit_agreement
let delete_agreement = this.delete_agreement
- window['av_vendors'] = this.vendors.map(e => {
+ window['agreements_av_vendors'] = this.vendors.map(e => {
e['_id'] = e['id']
e['_str'] = e['name']
return e
map[e.id] = e
return map
}, {})
- window['av_statuses'] = this.av_statuses.map(e => {
+ window['agreements_av_statuses'] = this.av_statuses.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
map[e.authorised_value] = e
return map
}, {})
- window['av_closure_reasons'] = this.av_closure_reasons.map(e => {
+ window['agreements_av_closure_reasons'] = this.av_closure_reasons.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
map[e.authorised_value] = e
return map
}, {})
- window['av_renewal_priorities'] = this.av_renewal_priorities.map(e => {
+ window['agreements_av_renewal_priorities'] = this.av_renewal_priorities.map(e => {
e['_id'] = e['authorised_value']
e['_str'] = e['lib']
return e
map[e.authorised_value] = e
return map
}, {})
- window['av_is_perpetual'] = [{ _id: 0, _str: _('No') }, { _id: 1, _str: _("Yes") }]
+ window['agreements_av_is_perpetual'] = [{ _id: 0, _str: _('No') }, { _id: 1, _str: _("Yes") }]
- $('#my_table').kohaTable({
+ $('#agreement_list').kohaTable({
"ajax": {
"url": agreements_table_url,
},
}
},
{
- "title": __("description"),
+ "title": __("Description"),
"data": "description",
"searchable": true,
"orderable": true
$.each($(this).find("td .actions"), function (index, e) {
let agreement_id = api.row(index).data().agreement_id
- let editButton = createVNode(AgreementsButtonEdit, {
+ let editButton = createVNode(ButtonEdit, {
onClick: () => {
edit_agreement(agreement_id)
}
})
- let deleteButton = createVNode(AgreementsButtonDelete, {
+ let deleteButton = createVNode(ButtonDelete, {
onClick: () => {
delete_agreement(agreement_id)
}
},
preDrawCallback: function (settings) {
var table_id = settings.nTable.id
- $("#" + table_id).find("thead th").eq(1).attr('data-filter', 'av_vendors')
- $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'av_statuses')
- $("#" + table_id).find("thead th").eq(4).attr('data-filter', 'av_closure_reasons')
- $("#" + table_id).find("thead th").eq(5).attr('data-filter', 'av_is_perpetual')
- $("#" + table_id).find("thead th").eq(6).attr('data-filter', 'av_renewal_priorities')
+ $("#" + table_id).find("thead th").eq(1).attr('data-filter', 'agreements_av_vendors')
+ $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'agreements_av_statuses')
+ $("#" + table_id).find("thead th").eq(4).attr('data-filter', 'agreements_av_closure_reasons')
+ $("#" + table_id).find("thead th").eq(5).attr('data-filter', 'agreements_av_is_perpetual')
+ $("#" + table_id).find("thead th").eq(6).attr('data-filter', 'agreements_av_renewal_priorities')
}
}, table_settings, 1)
},
beforeUnmount() {
- $('#my_table')
+ $('#agreement_list')
.DataTable()
.destroy(true)
},
--- /dev/null
+<template>
+ <a class="btn btn-default btn-xs" role="button"
+ ><i class="fa fa-trash" aria-hidden="true" /> Delete</a
+ >
+</template>
+
+<script>
+export default {
+ name: "ButtonDelete"
+}
+</script>
--- /dev/null
+<template>
+ <a class="btn btn-default btn-xs" role="button"
+ ><i class="fa fa-pencil" aria-hidden="true" /> Edit</a
+ >
+</template>
+
+<script>
+export default {
+ name: "ButtonEdit",
+}
+</script>
Agreements</router-link
>
</li>
+ <li>
+ <router-link
+ to="/cgi-bin/koha/erm/licenses"
+ >
+ <i class="fa fa-file-text-o"></i>
+ Licenses</router-link
+ >
+ </li>
+
</ul>
</div>
</div>
--- /dev/null
+<template>
+ <Toolbar v-if="op == 'list'" @switch-view="switchView" />
+ <div class="dialog message" v-if="message">{{ message }}</div>
+ <div class="dialog alert" v-if="error">{{ error }}</div>
+ <List
+ v-if="op == 'list'"
+ :av_types="types"
+ :av_statuses="statuses"
+ @set-current-license-id="setCurrentLicenseID"
+ @switch-view="switchView"
+ @set-error="setError"
+ />
+ <Show
+ v-if="op == 'show'"
+ :license_id="license_id"
+ :av_types="types"
+ :av_statuses="statuses"
+ @switch-view="switchView"
+ @set-error="setError"
+ />
+ <AddForm
+ v-if="op == 'add-form'"
+ :license_id="license_id"
+ :av_types="types"
+ :av_statuses="statuses"
+ @license-created="licenseCreated"
+ @license-updated="licenseUpdated"
+ @switch-view="switchView"
+ @set-error="setError"
+ />
+ <ConfirmDeleteForm
+ v-if="op == 'confirm-delete-form'"
+ :license_id="license_id"
+ @license-deleted="licenseDeleted"
+ @switch-view="switchView"
+ @set-error="setError"
+ />
+</template>
+
+<script>
+import Toolbar from "./LicensesToolbar.vue"
+import List from "./LicensesList.vue"
+import Show from "./LicensesShow.vue"
+import AddForm from "./LicensesFormAdd.vue"
+import ConfirmDeleteForm from "./LicensesFormConfirmDelete.vue"
+
+import { reactive, computed } from "vue"
+
+export default {
+ data() {
+ return {
+ license_id: null,
+ op: "list",
+ message: null,
+ error: null,
+ types: license_types,
+ statuses: license_statuses,
+ }
+ },
+ methods: {
+ switchView(view) {
+ this.message = null
+ this.error = null
+ this.op = view
+ if (view == "list") this.license_id = null
+ },
+ licenseCreated() {
+ this.message = "License created"
+ this.error = null
+ this.license_id = null
+ this.op = "list"
+ },
+ licenseUpdated() {
+ this.message = "License updated"
+ this.error = null
+ this.license_id = null
+ this.op = "list"
+ },
+ licenseDeleted() {
+ this.message = "License deleted"
+ this.error = null
+ this.license_id = null
+ this.op = "list"
+ },
+ setCurrentLicenseID(license_id) {
+ this.license_id = license_id
+ },
+ setError(error) {
+ this.message = null
+ this.error = "Something went wrong: " + error
+ },
+ },
+ components: {
+ Toolbar,
+ List,
+ Show,
+ AddForm,
+ ConfirmDeleteForm,
+ },
+ emits: ["set-error"],
+};
+</script>
--- /dev/null
+<template>
+ <h2 v-if="license.license_id">Edit license</h2>
+ <h2 v-else>New license</h2>
+ <div>
+ <form @submit="onSubmit($event)">
+ <fieldset class="rows">
+ <ol>
+ <li>
+ <label class="required" for="license_name"
+ >License name:</label
+ >
+ <input
+ id="license_name"
+ v-model="license.name"
+ placeholder="License name"
+ required
+ />
+ <span class="required">Required</span>
+ </li>
+ <li>
+ <label for="license_description">Description: </label>
+ <textarea
+ id="license_description"
+ v-model="license.description"
+ placeholder="Description"
+ rows="10"
+ cols="50"
+ required
+ />
+ <span class="required">Required</span>
+ </li>
+ <li>
+ <label for="license_type">Type: </label>
+ <select
+ id="license_type"
+ v-model="license.type"
+ required
+ >
+ <option value=""></option>
+ <option
+ v-for="type in av_types"
+ :key="type.authorised_values"
+ :value="type.authorised_value"
+ :selected="
+ type.authorised_value == license.type
+ ? true
+ : false
+ "
+ >
+ {{ type.lib }}
+ </option>
+ </select>
+ <span class="required">Required</span>
+ </li>
+ <li>
+ <label for="license_status">Status: </label>
+ <select
+ id="license_status"
+ v-model="license.status"
+ required
+ >
+ <option value=""></option>
+ <option
+ v-for="status in av_statuses"
+ :key="status.authorised_values"
+ :value="status.authorised_value"
+ :selected="
+ status.authorised_value == license.status
+ ? true
+ : false
+ "
+ >
+ {{ status.lib }}
+ </option>
+ </select>
+ <span class="required">Required</span>
+ </li>
+ <li>
+ <label for="started_on">Start date: </label>
+ <flat-pickr
+ id="started_on"
+ v-model="license.started_on"
+ :config="fp_config"
+ data-date_to="ended_on"
+ />
+ </li>
+ <li>
+ <label for="ended_on">End date: </label>
+ <flat-pickr
+ id="ended_on"
+ v-model="license.ended_on"
+ :config="fp_config"
+ />
+ </li>
+ </ol>
+ </fieldset>
+ <fieldset class="action">
+ <input type="submit" value="Submit" />
+ <a
+ role="button"
+ class="cancel"
+ @click="$emit('switch-view', 'list')"
+ >Cancel</a
+ >
+ </fieldset>
+ </form>
+ </div>
+</template>
+
+<script>
+import flatPickr from 'vue-flatpickr-component'
+
+export default {
+ data() {
+ return {
+ fp_config: flatpickr_defaults, dates_fixed: 0,
+
+ license: {
+ license_id: null,
+ name: '',
+ description: '',
+ type: '',
+ status: '',
+ started_on: undefined,
+ ended_on: undefined,
+ }
+ }
+ },
+ beforeUpdate() {
+ if (!this.dates_fixed) {
+ this.license.started_on = $date(this.license.started_on)
+ this.license.ended_on = $date(this.license.ended_on)
+ this.dates_fixed = 1
+ }
+ },
+ created() {
+ if (!this.license_id) return
+ const apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+ fetch(apiUrl, {
+ //headers: {
+ // 'x-koha-embed': 'periods,user_roles,user_roles.patron'
+ //}
+ })
+ .then(res => res.json())
+ .then(
+ (result) => {
+ this.license = result
+ },
+ (error) => {
+ this.$emit('set-error', error)
+ }
+ )
+ },
+ methods: {
+ onSubmit(e) {
+ e.preventDefault()
+
+ let license = JSON.parse(JSON.stringify(this.license)) // copy
+ let apiUrl = '/api/v1/erm/licenses'
+
+ let method = 'POST'
+ if (license.license_id) {
+ method = 'PUT'
+ apiUrl += '/' + license.license_id
+ }
+ delete license.license_id
+
+ license.started_on = license.started_on ? $date_to_rfc3339(license.started_on) : null
+ license.ended_on = license.ended_on ? $date_to_rfc3339(license.ended_on) : null
+
+ const options = {
+ method: method,
+ body: JSON.stringify(license),
+ headers: {
+ 'Content-Type': 'application/json;charset=utf-8'
+ },
+ }
+
+ fetch(apiUrl, options)
+ .then(response => {
+ if (response.status == 200) {
+ this.$emit('license-updated')
+ } else if (response.status == 201) {
+ this.$emit('license-created')
+ } else {
+ this.$emit('set-error', response.message || response.statusText)
+ }
+ }, (error) => {
+ this.$emit('set-error', error)
+ }).catch(e => { console.log(e) })
+ },
+ },
+ emits: ['license-created', 'license-updated', 'set-error', 'switch-view'],
+ props: {
+ license_id: Number,
+ av_types: Array,
+ av_statuses: Array,
+ },
+ components: {
+ flatPickr
+ },
+ name: "LicensesFormAdd",
+}
+</script>
--- /dev/null
+<template>
+ <h2>Delete license</h2>
+ <div>
+ <form @submit="onSubmit($event)">
+ <fieldset class="rows">
+ <ol>
+ <li>
+ License name:
+ {{ license.name }}
+ </li>
+ <li>
+ Description:
+ {{ license.description }}
+ </li>
+ </ol>
+ </fieldset>
+ <fieldset class="action">
+ <input type="submit" variant="primary" value="Yes, delete" />
+ <a role="button" class="cancel" @click="$emit('switch-view', 'list')"
+ >No, do not delete</a
+ >
+ </fieldset>
+ </form>
+ </div>
+</template>
+
+<script>
+
+export default {
+ data() {
+ return {
+ license: {},
+ }
+ },
+ created() {
+ const apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+ fetch(apiUrl)
+ .then(res => res.json())
+ .then(
+ (result) => {
+ this.license= result
+ },
+ ).catch(
+ (error) => {
+ this.$emit('set-error', error)
+ }
+ )
+ },
+ methods: {
+ onSubmit(e) {
+ e.preventDefault()
+
+ let apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+ const options = {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json;charset=utf-8'
+ },
+ }
+
+ fetch(apiUrl, options)
+ .then(
+ (response) => {
+ if (response.status == 204) {
+ this.$emit('license-deleted')
+ } else {
+ this.$emit('set-error', response.message || response.statusText)
+ }
+ }
+ ).catch(
+ (error) => {
+ this.$emit('set-error', error)
+ }
+ )
+ }
+ },
+ emits: ['license-deleted', 'set-error', 'switch-view'],
+ props: {
+ license_id: Number
+ },
+ name: "LicensesFormConfirmDelete",
+}
+</script>
--- /dev/null
+<template>
+ <div>
+ <table v-if="licenses.length" id="license_list"></table>
+ <div v-else-if="this.initialized" class="dialog message">
+ There are no licenses defined.
+ </div>
+ <div v-else>Loading...</div>
+ </div>
+</template>
+
+<script>
+import ButtonEdit from "./ButtonEdit.vue"
+import ButtonDelete from "./ButtonDelete.vue"
+import { createVNode, defineComponent, render, resolveComponent } from 'vue'
+export default {
+ created() {
+ const apiUrl = '/api/v1/erm/licenses'
+
+ fetch(apiUrl)
+ .then(res => res.json())
+ .then(
+ (result) => {
+ this.licenses = result
+ this.initialized = true
+ },
+ (error) => {
+ this.$emit('set-error', error)
+ }
+ )
+ },
+ updated() {
+ let show_license = this.show_license
+ let edit_license = this.edit_license
+ let delete_license = this.delete_license
+ window['licenses_av_types'] = this.av_types.map(e => {
+ e['_id'] = e['authorised_value']
+ e['_str'] = e['lib']
+ return e
+ })
+ let types_map = this.av_types.reduce((map, e) => {
+ map[e.authorised_value] = e
+ return map
+ }, {})
+ window['licenses_av_statuses'] = this.av_statuses.map(e => {
+ e['_id'] = e['authorised_value']
+ e['_str'] = e['lib']
+ return e
+ })
+ let statuses_map = this.av_statuses.reduce((map, e) => {
+ map[e.authorised_value] = e
+ return map
+ }, {})
+
+ $('#license_list').kohaTable({
+ "ajax": {
+ "url": licenses_table_url,
+ },
+ "order": [[1, "asc"]],
+ "columnDefs": [{
+ "targets": [0, 1],
+ "render": function (data, type, row, meta) {
+ if (type == 'display') {
+ return escape_str(data)
+ }
+ return data
+ }
+ }],
+ "columns": [
+ {
+ "title": __("Name"),
+ "data": ["me.license_id", "me.name"],
+ "searchable": true,
+ "orderable": true,
+ // Rendering done in drawCallback
+ },
+ {
+ "title": __("Description"),
+ "data": "description",
+ "searchable": true,
+ "orderable": true
+ },
+ {
+ "title": __("Type"),
+ "data": "type",
+ "searchable": true,
+ "orderable": true,
+ "render": function (data, type, row, meta) {
+ return escape_str(types_map[row.type].lib)
+ }
+ },
+ {
+ "title": __("Status"),
+ "data": "status",
+ "searchable": true,
+ "orderable": true,
+ "render": function (data, type, row, meta) {
+ return escape_str(statuses_map[row.status].lib)
+ }
+ },
+ {
+ "title": __("Started on"),
+ "data": "started_on",
+ "searchable": true,
+ "orderable": true,
+ "render": function (data, type, row, meta) {
+ return escape_str(row.started_on)
+ }
+ },
+ {
+ "title": __("Ended on"),
+ "data": "ended_on",
+ "searchable": true,
+ "orderable": true,
+ "render": function (data, type, row, meta) {
+ return escape_str(row.ended_on)
+ }
+ },
+ {
+ "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 license_id = api.row(index).data().license_id
+ let editButton = createVNode(ButtonEdit, {
+ onClick: () => {
+ edit_license(license_id)
+ }
+ })
+ let deleteButton = createVNode(ButtonDelete, {
+ onClick: () => {
+ delete_license(license_id)
+ }
+ })
+ let n = createVNode('span', {}, [editButton, " ", deleteButton])
+ render(n, e)
+ })
+
+ $.each($(this).find("tbody tr td:first-child"), function (index, e) {
+ let row = api.row(index).data()
+ if (!row) return // Happen if the table is empty
+ let n = createVNode("a", {
+ role: "button",
+ onClick: () => {
+ show_license(row.license_id)
+ }
+ },
+ escape_str(`${row.name} (#${row.license_id})`)
+ )
+ render(n, e)
+ })
+ },
+ preDrawCallback: function (settings) {
+ var table_id = settings.nTable.id
+ $("#" + table_id).find("thead th").eq(2).attr('data-filter', 'licenses_av_types')
+ $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'licenses_av_statuses')
+ }
+
+ }, table_settings, 1)
+ },
+ beforeUnmount() {
+ $('#license_list')
+ .DataTable()
+ .destroy(true)
+ },
+ data: function () {
+ return {
+ licenses: [],
+ initialized: false,
+ }
+ },
+ methods: {
+ show_license: function (license_id) {
+ this.$emit('set-current-license-id', license_id)
+ this.$emit('switch-view', 'show')
+ },
+ edit_license: function (license_id) {
+ this.$emit('set-current-license-id', license_id)
+ this.$emit('switch-view', 'add-form')
+ },
+ delete_license: function (license_id) {
+ this.$emit('set-current-license-id', license_id)
+ this.$emit('switch-view', 'confirm-delete-form')
+ },
+ },
+ props: {
+ av_types: Array,
+ av_statuses: Array,
+ },
+ name: "LicensesList",
+}
+</script>
--- /dev/null
+<template>
+ <h2>License #{{ license.license_id }}</h2>
+ <div>
+ <fieldset class="rows">
+ <ol>
+ <li>
+ <label>License name:</label>
+ <span>
+ {{ license.name }}
+ </span>
+ </li>
+ <li>
+ <label>Description: </label>
+ <span>
+ {{ license.description }}
+ </span>
+ </li>
+ <li>
+ <label>Type: </label>
+ <span>{{
+ get_lib_from_av(av_types, license.type)
+ }}</span>
+ </li>
+ <li>
+ <label>Status: </label>
+ <span>{{
+ get_lib_from_av(av_statuses, license.status)
+ }}</span>
+ </li>
+
+ <li>
+ <label>Started on:</label>
+ <span>{{ format_date(license.started_on) }}</span>
+ </li>
+
+ <li>
+ <label>Ended on:</label>
+ <span>{{ format_date(license.ended_on) }}</span>
+ </li>
+
+ </ol>
+ </fieldset>
+ <fieldset class="action">
+ <a
+ role="button"
+ class="cancel"
+ @click="$emit('switch-view', 'list')"
+ >Close</a
+ >
+ </fieldset>
+ </div>
+</template>
+
+<script>
+
+export default {
+ setup() {
+ const format_date = $date
+ const get_lib_from_av = function (arr, av) {
+ let o = arr.find(
+ (e) => e.authorised_value == av
+ )
+ return o ? o.lib : ""
+ }
+ return {
+ format_date,
+ get_lib_from_av
+ }
+ },
+ data() {
+ return {
+ license: {
+ license_id: null,
+ name: '',
+ description: '',
+ type: '',
+ status: '',
+ started_on: undefined,
+ ended_on: undefined,
+ }
+ }
+ },
+ created() {
+ if (!this.license_id) return
+ const apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+ fetch(apiUrl, {
+ //headers: {
+ // 'x-koha-embed': 'periods,user_roles,user_roles.patron'
+ //}
+ })
+ .then(res => res.json())
+ .then(
+ (result) => {
+ this.license = result
+ },
+ (error) => {
+ this.$emit('set-error', error)
+ }
+ )
+ },
+ methods: {
+ },
+ emits: ['set-error', 'switch-view'],
+ props: {
+ license_id: Number,
+ av_types: Array,
+ av_statuses: Array,
+ },
+ components: {
+ },
+ name: "LicensesShow",
+}
+</script>
--- /dev/null
+<template>
+ <a class="btn btn-default" @click="$emit('switch-view', 'add-form')"
+ ><font-awesome-icon icon="plus" /> New license</a
+ >
+</template>
+
+<script>
+export default {
+ name: "LicensesToolbar",
+ emits: ['switch-view'],
+}
+</script>
import App from "./components/ERM/ERMMain.vue";
import ERMHome from "./components/ERM/ERMHome.vue";
import Agreements from "./components/ERM/Agreements.vue";
+import Licenses from "./components/ERM/Licenses.vue";
const Bar = { template: "<div>bar</div>" };
const routes = [
],
},
},
+ {
+ path: "/cgi-bin/koha/erm/licenses",
+ component: Licenses,
+ meta: {
+ breadcrumb: [
+ { text: "Home", path: "/cgi-bin/koha/mainpage.pl" },
+ {
+ text: "Electronic resources management",
+ path: "/cgi-bin/koha/erm/erm.pl",
+ },
+ { text: "Licenses", path: "/cgi-bin/koha/erm/licenses" },
+ ],
+ },
+ },
];
const router = createRouter({ history: createWebHistory(), routes });
--- /dev/null
+#!/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 => 5;
+use Test::Mojo;
+
+use t::lib::TestBuilder;
+use t::lib::Mocks;
+
+use Koha::ERM::Licenses;
+use Koha::Database;
+
+my $schema = Koha::Database->new->schema;
+my $builder = t::lib::TestBuilder->new;
+
+my $t = Test::Mojo->new('Koha::REST::V1');
+
+subtest 'list() tests' => sub {
+
+ plan tests => 8;
+
+ $schema->storage->txn_begin;
+
+ Koha::ERM::Licenses->search->delete;
+
+ my $librarian = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 2**28 }
+ }
+ );
+ my $password = 'thePassword123';
+ $librarian->set_password( { password => $password, skip_validation => 1 } );
+ my $userid = $librarian->userid;
+
+ my $patron = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 0 }
+ }
+ );
+
+ $patron->set_password( { password => $password, skip_validation => 1 } );
+ my $unauth_userid = $patron->userid;
+
+ ## Authorized user tests
+ # No licenses, so empty array should be returned
+ $t->get_ok("//$userid:$password@/api/v1/erm/licenses")->status_is(200)
+ ->json_is( [] );
+
+ my $license =
+ $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+
+ # One license created, should get returned
+ $t->get_ok("//$userid:$password@/api/v1/erm/licenses")->status_is(200)
+ ->json_is( [ $license->to_api ] );
+
+ # Unauthorized access
+ $t->get_ok("//$unauth_userid:$password@/api/v1/erm/licenses")
+ ->status_is(403);
+
+ $schema->storage->txn_rollback;
+};
+
+subtest 'get() tests' => sub {
+
+ plan tests => 8;
+
+ $schema->storage->txn_begin;
+
+ my $license =
+ $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+ my $librarian = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 2**28 }
+ }
+ );
+ my $password = 'thePassword123';
+ $librarian->set_password( { password => $password, skip_validation => 1 } );
+ my $userid = $librarian->userid;
+
+ my $patron = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 0 }
+ }
+ );
+
+ $patron->set_password( { password => $password, skip_validation => 1 } );
+ my $unauth_userid = $patron->userid;
+
+ $t->get_ok( "//$userid:$password@/api/v1/erm/licenses/"
+ . $license->license_id )->status_is(200)
+ ->json_is( $license->to_api );
+
+ $t->get_ok( "//$unauth_userid:$password@/api/v1/erm/licenses/"
+ . $license->license_id )->status_is(403);
+
+ my $license_to_delete =
+ $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+ my $non_existent_id = $license_to_delete->id;
+ $license_to_delete->delete;
+
+ $t->get_ok("//$userid:$password@/api/v1/erm/licenses/$non_existent_id")
+ ->status_is(404)->json_is( '/error' => 'License not found' );
+
+ $schema->storage->txn_rollback;
+};
+
+subtest 'add() tests' => sub {
+
+ plan tests => 18;
+
+ $schema->storage->txn_begin;
+
+ my $librarian = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 2**28 }
+ }
+ );
+ my $password = 'thePassword123';
+ $librarian->set_password( { password => $password, skip_validation => 1 } );
+ my $userid = $librarian->userid;
+
+ my $patron = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 0 }
+ }
+ );
+
+ $patron->set_password( { password => $password, skip_validation => 1 } );
+ my $unauth_userid = $patron->userid;
+
+ my $license = {
+ name => "License name",
+ description => "License description",
+ type => 'local',
+ status => "active",
+ started_on => undef,
+ ended_on => undef,
+ };
+
+ # Unauthorized attempt to write
+ $t->post_ok( "//$unauth_userid:$password@/api/v1/erm/licenses" => json =>
+ $license )->status_is(403);
+
+ # Authorized attempt to write invalid data
+ my $license_with_invalid_field = {
+ blah => "License Blah",
+ name => "License name",
+ description => "License description",
+ type => 'local',
+ status => "active",
+ started_on => undef,
+ ended_on => undef,
+ };
+
+ $t->post_ok( "//$userid:$password@/api/v1/erm/licenses" => json =>
+ $license_with_invalid_field )->status_is(400)->json_is(
+ "/errors" => [
+ {
+ message => "Properties not allowed: blah.",
+ path => "/body"
+ }
+ ]
+ );
+
+ # Authorized attempt to write
+ my $license_id =
+ $t->post_ok(
+ "//$userid:$password@/api/v1/erm/licenses" => json => $license )
+ ->status_is( 201, 'SWAGGER3.2.1' )->header_like(
+ Location => qr|^/api/v1/erm/licenses/\d*|,
+ 'SWAGGER3.4.1'
+ )->json_is( '/name' => $license->{name} )
+ ->json_is( '/description' => $license->{description} )
+ ->json_is( '/type' => $license->{type} )
+ ->json_is( '/status' => $license->{status} )
+ ->tx->res->json->{license_id};
+
+ # Authorized attempt to create with null id
+ $license->{license_id} = undef;
+ $t->post_ok(
+ "//$userid:$password@/api/v1/erm/licenses" => json => $license )
+ ->status_is(400)->json_has('/errors');
+
+ # Authorized attempt to create with existing id
+ $license->{license_id} = $license_id;
+ $t->post_ok(
+ "//$userid:$password@/api/v1/erm/licenses" => json => $license )
+ ->status_is(400)->json_is(
+ "/errors" => [
+ {
+ message => "Read-only.",
+ path => "/body/license_id"
+ }
+ ]
+ );
+
+ $schema->storage->txn_rollback;
+};
+
+subtest 'update() tests' => sub {
+
+ plan tests => 12;
+
+ $schema->storage->txn_begin;
+
+ my $librarian = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 2**28 }
+ }
+ );
+ my $password = 'thePassword123';
+ $librarian->set_password( { password => $password, skip_validation => 1 } );
+ my $userid = $librarian->userid;
+
+ my $patron = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 0 }
+ }
+ );
+
+ $patron->set_password( { password => $password, skip_validation => 1 } );
+ my $unauth_userid = $patron->userid;
+
+ my $license_id =
+ $builder->build_object( { class => 'Koha::ERM::Licenses' } )->license_id;
+
+ # Unauthorized attempt to update
+ $t->put_ok(
+ "//$unauth_userid:$password@/api/v1/erm/licenses/$license_id" =>
+ json => { name => 'New unauthorized name change' } )->status_is(403);
+
+ # Full object update on PUT
+ my $license_with_updated_field = {
+ name => 'New name',
+ description => 'New description',
+ type => 'national',
+ status => 'expired',
+ started_on => undef,
+ ended_on => undef,
+ };
+
+ $t->put_ok(
+ "//$userid:$password@/api/v1/erm/licenses/$license_id" => json =>
+ $license_with_updated_field )->status_is(200)
+ ->json_is( '/name' => 'New name' );
+
+ # Authorized attempt to write invalid data
+ my $license_with_invalid_field = {
+ blah => "License Blah",
+ name => "License name",
+ description => "License description",
+ type => 'national',
+ status => 'expired',
+ started_on => undef,
+ ended_on => undef,
+ };
+
+ $t->put_ok(
+ "//$userid:$password@/api/v1/erm/licenses/$license_id" => json =>
+ $license_with_invalid_field )->status_is(400)->json_is(
+ "/errors" => [
+ {
+ message => "Properties not allowed: blah.",
+ path => "/body"
+ }
+ ]
+ );
+
+ my $license_to_delete =
+ $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+ my $non_existent_id = $license_to_delete->id;
+ $license_to_delete->delete;
+
+ $t->put_ok( "//$userid:$password@/api/v1/erm/licenses/$non_existent_id" =>
+ json => $license_with_updated_field )->status_is(404);
+
+ # Wrong method (POST)
+ $license_with_updated_field->{license_id} = 2;
+
+ $t->post_ok(
+ "//$userid:$password@/api/v1/erm/licenses/$license_id" => json =>
+ $license_with_updated_field )->status_is(404);
+
+ $schema->storage->txn_rollback;
+};
+
+subtest 'delete() tests' => sub {
+
+ plan tests => 7;
+
+ $schema->storage->txn_begin;
+
+ my $librarian = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 2**28 }
+ }
+ );
+ my $password = 'thePassword123';
+ $librarian->set_password( { password => $password, skip_validation => 1 } );
+ my $userid = $librarian->userid;
+
+ my $patron = $builder->build_object(
+ {
+ class => 'Koha::Patrons',
+ value => { flags => 0 }
+ }
+ );
+
+ $patron->set_password( { password => $password, skip_validation => 1 } );
+ my $unauth_userid = $patron->userid;
+
+ my $license_id =
+ $builder->build_object( { class => 'Koha::ERM::Licenses' } )->id;
+
+ # Unauthorized attempt to delete
+ $t->delete_ok(
+ "//$unauth_userid:$password@/api/v1/erm/licenses/$license_id")
+ ->status_is(403);
+
+ $t->delete_ok("//$userid:$password@/api/v1/erm/licenses/$license_id")
+ ->status_is( 204, 'SWAGGER3.2.4' )->content_is( '', 'SWAGGER3.3.4' );
+
+ $t->delete_ok("//$userid:$password@/api/v1/erm/licenses/$license_id")
+ ->status_is(404);
+
+ $schema->storage->txn_rollback;
+};
+