Browse Source

Bug 27251: Rewrite QOTD with the Koha REST API

This patch replace the QOTD editor with our new way to CRUD the
adminitration page (like libraries and STMP servers)

Test plan:
Play with the QOTD by adding, removing, updating quotes
Try to find bugs :)

Bug 27251: Fix capitalization

Signed-off-by: Andrew Fuerste-Henry <andrew@bywatersolutions.com>

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
21.05.x
Jonathan Druart 1 year ago
parent
commit
401987bdad
  1. 18
      Koha/Quote.pm
  2. 139
      Koha/REST/V1/Quotes.pm
  3. 3
      api/v1/swagger/definitions.json
  4. 22
      api/v1/swagger/definitions/quote.json
  5. 3
      api/v1/swagger/parameters.json
  6. 9
      api/v1/swagger/parameters/quote.json
  7. 6
      api/v1/swagger/paths.json
  8. 327
      api/v1/swagger/paths/quotes.json
  9. 6
      api/v1/swagger/x-primitives.json
  10. 5
      koha-tmpl/intranet-tmpl/prog/en/includes/quotes-toolbar.inc
  11. 5
      koha-tmpl/intranet-tmpl/prog/en/includes/quotes-upload-toolbar.inc
  12. 2
      koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
  13. 84
      koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt
  14. 378
      koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt
  15. 77
      tools/quotes.pl
  16. 65
      tools/quotes/quotes-upload_ajax.pl
  17. 109
      tools/quotes/quotes_ajax.pl

18
Koha/Quote.pm

@ -33,6 +33,22 @@ Koha::Quote - Koha Quote object class
=cut
=head3 to_api_mapping
This method returns the mapping for representing a Koha::Quote object
on the API.
=cut
sub to_api_mapping {
return {
id => 'quote_id',
source => 'source',
text => 'text',
timestamp => 'displayed_on',
};
}
=head3 _type
=cut
@ -41,4 +57,4 @@ sub _type {
return 'Quote';
}
1;
1;

139
Koha/REST/V1/Quotes.pm

@ -0,0 +1,139 @@
package Koha::REST::V1::Quotes;
# 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::Quotes;
use Try::Tiny;
=head1 API
=head2 Methods
=head3 list
=cut
sub list {
my $c = shift->openapi->valid_input or return;
return try {
my $quotes_set = Koha::Quotes->new;
my $quotes = $c->objects->search( $quotes_set );
return $c->render( status => 200, openapi => $quotes );
}
catch {
$c->unhandled_exception($_);
};
}
=head3 get
=cut
sub get {
my $c = shift->openapi->valid_input or return;
return try {
my $quote = Koha::Quotes->find( $c->validation->param('quote_id') );
unless ($quote) {
return $c->render( status => 404,
openapi => { error => "quote not found" } );
}
return $c->render( status => 200, openapi => $quote->to_api );
}
catch {
$c->unhandled_exception($_);
}
}
=head3 add
=cut
sub add {
my $c = shift->openapi->valid_input or return;
return try {
my $quote = Koha::Quote->new_from_api( $c->validation->param('body') );
$quote->store;
$c->res->headers->location( $c->req->url->to_string . '/' . $quote->id );
return $c->render(
status => 201,
openapi => $quote->to_api
);
}
catch {
$c->unhandled_exception($_);
};
}
=head3 update
=cut
sub update {
my $c = shift->openapi->valid_input or return;
my $quote = Koha::Quotes->find( $c->validation->param('quote_id') );
if ( not defined $quote ) {
return $c->render( status => 404,
openapi => { error => "Object not found" } );
}
return try {
$quote->set_from_api( $c->validation->param('body') );
$quote->store();
return $c->render( status => 200, openapi => $quote->to_api );
}
catch {
$c->unhandled_exception($_);
};
}
=head3 delete
=cut
sub delete {
my $c = shift->openapi->valid_input or return;
my $quote = Koha::Quotes->find( $c->validation->param('quote_id') );
if ( not defined $quote ) {
return $c->render( status => 404,
openapi => { error => "Object not found" } );
}
return try {
$quote->delete;
return $c->render(
status => 204,
openapi => q{}
);
}
catch {
$c->unhandled_exception($_);
};
}
1;

3
api/v1/swagger/definitions.json

@ -65,6 +65,9 @@
"patron_balance": {
"$ref": "definitions/patron_balance.json"
},
"quote": {
"$ref": "definitions/quote.json"
},
"allows_renewal": {
"$ref": "definitions/allows_renewal.json"
},

22
api/v1/swagger/definitions/quote.json

@ -0,0 +1,22 @@
{
"type": "object",
"properties": {
"quote_id": {
"$ref": "../x-primitives.json#/quote_id"
},
"source": {
"description": "source of the quote",
"type": "string"
},
"text": {
"description": "text",
"type": ["string", "null"]
},
"displayed_on": {
"description": "Last display date",
"type": ["string", "null"]
}
},
"additionalProperties": false,
"required": ["quote_id", "source", "text"]
}

3
api/v1/swagger/parameters.json

@ -32,6 +32,9 @@
"order_id_pp": {
"$ref": "parameters/order.json#/order_id_pp"
},
"quote_id_pp": {
"$ref": "parameters/quote.json#/quote_id_pp"
},
"smtp_server_id_pp": {
"$ref": "parameters/smtp_server.json#/smtp_server_id_pp"
},

9
api/v1/swagger/parameters/quote.json

@ -0,0 +1,9 @@
{
"quote_id_pp": {
"name": "quote_id",
"in": "path",
"description": "Quote internal identifier",
"required": true,
"type": "integer"
}
}

6
api/v1/swagger/paths.json

@ -137,6 +137,12 @@
"/public/patrons/{patron_id}/guarantors/can_see_checkouts": {
"$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1guarantors~1can_see_checkouts"
},
"/quotes": {
"$ref": "paths/quotes.json#/~1quotes"
},
"/quotes/{quote_id}": {
"$ref": "paths/quotes.json#/~1quotes~1{quote_id}"
},
"/return_claims": {
"$ref": "paths/return_claims.json#/~1return_claims"
},

327
api/v1/swagger/paths/quotes.json

@ -0,0 +1,327 @@
{
"/quotes": {
"get": {
"x-mojo-to": "Quotes#list",
"operationId": "listQuotes",
"tags": [
"quotes"
],
"produces": [
"application/json"
],
"parameters": [
{
"name": "quote_id",
"in": "query",
"description": "Case insensitive search on quote id",
"required": false,
"type": "string"
},
{
"name": "source",
"in": "query",
"description": "Case insensitive search on source",
"required": false,
"type": "string"
},
{
"name": "text",
"in": "query",
"description": "Case insensitive search on text",
"required": false,
"type": "string"
},
{
"name": "displayed_on",
"in": "query",
"description": "Case Insensative search on last displayed date",
"required": false,
"type": "string"
},
{
"$ref": "../parameters.json#/match"
},
{
"$ref": "../parameters.json#/order_by"
},
{
"$ref": "../parameters.json#/page"
},
{
"$ref": "../parameters.json#/per_page"
},
{
"$ref": "../parameters.json#/q_param"
},
{
"$ref": "../parameters.json#/q_body"
},
{
"$ref": "../parameters.json#/q_header"
}
],
"responses": {
"200": {
"description": "A list of quotes",
"schema": {
"type": "array",
"items": {
"$ref": "../definitions.json#/quote"
}
}
},
"403": {
"description": "Access forbidden",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"503": {
"description": "Under maintenance",
"schema": {
"$ref": "../definitions.json#/error"
}
}
},
"x-koha-authorization": {
"permissions": {
"catalogue": "1"
}
}
},
"post": {
"x-mojo-to": "Quotes#add",
"operationId": "addQuote",
"tags": [
"quotes"
],
"parameters": [
{
"name": "body",
"in": "body",
"description": "A JSON object containing informations about the new quote",
"required": true,
"schema": {
"$ref": "../definitions.json#/quote"
}
}
],
"produces": [
"application/json"
],
"responses": {
"201": {
"description": "Quote added",
"schema": {
"$ref": "../definitions.json#/quote"
}
},
"401": {
"description": "Authentication required",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"403": {
"description": "Access forbidden",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"503": {
"description": "Under maintenance",
"schema": {
"$ref": "../definitions.json#/error"
}
}
},
"x-koha-authorization": {
"permissions": {
"tools": "edit_quotes"
}
}
}
},
"/quotes/{quote_id}": {
"get": {
"x-mojo-to": "Quotes#get",
"operationId": "getQuote",
"tags": [
"quotes"
],
"parameters": [
{
"$ref": "../parameters.json#/quote_id_pp"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "A Quote",
"schema": {
"$ref": "../definitions.json#/quote"
}
},
"404": {
"description": "Quote not found",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"503": {
"description": "Under maintenance",
"schema": {
"$ref": "../definitions.json#/error"
}
}
},
"x-koha-authorization": {
"permissions": {
"catalogue": "1"
}
}
},
"put": {
"x-mojo-to": "Quotes#update",
"operationId": "updateQuote",
"tags": [
"quotes"
],
"parameters": [
{
"$ref": "../parameters.json#/quote_id_pp"
},
{
"name": "body",
"in": "body",
"description": "a quote object",
"required": true,
"schema": {
"$ref": "../definitions.json#/quote"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "A quote",
"schema": {
"$ref": "../definitions.json#/quote"
}
},
"401": {
"description": "Authentication required",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"403": {
"description": "Access forbidden",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"404": {
"description": "Quote not found",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"503": {
"description": "Under maintenance",
"schema": {
"$ref": "../definitions.json#/error"
}
}
},
"x-koha-authorization": {
"permissions": {
"tools": "edit_quotes"
}
}
},
"delete": {
"x-mojo-to": "Quotes#delete",
"operationId": "deleteQuote",
"tags": [
"quotes"
],
"parameters": [
{
"$ref": "../parameters.json#/quote_id_pp"
}
],
"produces": [
"application/json"
],
"responses": {
"204": {
"description": "Quote deleted"
},
"401": {
"description": "Authentication required",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"403": {
"description": "Access forbidden",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"404": {
"description": "Quote not found",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "../definitions.json#/error"
}
},
"503": {
"description": "Under maintenance",
"schema": {
"$ref": "../definitions.json#/error"
}
}
},
"x-koha-authorization": {
"permissions": {
"tools": "edit_quotes"
}
}
}
}
}

6
api/v1/swagger/x-primitives.json

@ -52,6 +52,10 @@
"type": "integer",
"description": "internally assigned fund identifier",
"readOnly": true
},
"quote_id": {
"type": "integer",
"description": "internally assigned quote identifier",
"readOnly": true
}
}

5
koha-tmpl/intranet-tmpl/prog/en/includes/quotes-toolbar.inc

@ -1,5 +0,0 @@
<div id="toolbar" class="btn-toolbar">
<div class="btn-group"><a class="btn btn-default" id="add_quote" href="#"><i class="fa fa-plus"></i> Add quote</a></div>
<div class="btn-group"><a class="btn btn-default" id="delete_quote" href="#"><i class="fa fa-trash"></i> Delete quote(s)</a></div>
<div class="btn-group"><a class="btn btn-default" id="import_quotes" href="/cgi-bin/koha/tools/quotes-upload.pl"><i class="fa fa-folder-open"></i> Import quotes</a></div>
</div>

5
koha-tmpl/intranet-tmpl/prog/en/includes/quotes-upload-toolbar.inc

@ -1,5 +0,0 @@
<div id="toolbar" class="btn-toolbar" style="visibility: hidden; position: absolute">
<div class="btn-group"><a class="btn btn-default" id="save_quotes" href="#"><i class="fa fa-save"></i> Save quotes</a></div>
<div class="btn-group"><a class="btn btn-default" id="delete_quote" href="#"><i class="fa fa-trash"></i> Delete quote(s)</a></div>
<div class="btn-group"><a href="/cgi-bin/koha/tools/quotes-upload.pl" id="cancel_quotes" class="btn btn-default"><i class="fa fa-remove"></i> Cancel import</a></div>
</div>

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

@ -37,7 +37,7 @@
[% END %]
[% IF ( daily_quote ) %]
<div id="area-news">
<h3>Quote of the Day</h3>
<h3>Quote of the day</h3>
<div class="newsitem">
<span id="daily-quote-text">[% daily_quote.text | html %]</span><span id="daily-quote-sep"> ~ </span><span id="daily-quote-source">[% daily_quote.source | html %]</span>
</div>

84
koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes-upload.tt

@ -18,9 +18,18 @@
<div class="row">
<div class="col-sm-10 col-sm-push-2">
<main>
<div id="toolbar" class="btn-toolbar" style="visibility: hidden; position: absolute">
<div class="btn-group"><a class="btn btn-default" id="save_quotes" href="#"><i class="fa fa-save"></i> Save quotes</a></div>
<div class="btn-group"><a href="/cgi-bin/koha/tools/quotes-upload.pl" id="cancel_quotes" class="btn btn-default"><i class="fa fa-remove"></i> Cancel import</a></div>
</div>
[% INCLUDE 'quotes-upload-toolbar.inc' %]
<h2>Quote uploader</h2>
<div id="messages" style="display: none;">
<div class="import_success dialog message" style="display: none;"></div>
<div class="import_errors dialog alert" style="display: none;"></div>
</div>
<div id="instructions">
<fieldset id="file_uploader_help" class="rows">
<legend>Instructions</legend>
@ -34,12 +43,12 @@
<div id="file_editor_inst">
<ul>
<li>Click on any field to edit the contents; Press the &lt;Enter&gt; key to save edit.</li>
<li>Click on one or more quote numbers to select entire quotes for deletion; Click the 'Delete Quote(s)' button to delete selected quotes.</li>
<li>Click the 'Save Quotes' button in the toolbar to save the entire batch of quotes.</li>
</ul>
</div>
</fieldset>
</div>
<fieldset id="file_uploader" class="rows">
<legend>Upload quotes</legend>
<div id="file_upload">
@ -232,7 +241,6 @@
$('#file_uploader').css("top","-150px");
$('#quotes_editor').css("visibility","visible");
$("#save_quotes").on("click", yuiGetData);
$("#delete_quote").on("click", fnClickDeleteRow);
oTable = $('#quotes_editor').dataTable( {
"bAutoWidth" : false,
@ -262,8 +270,6 @@
/* do foo on various cells in the current row */
var quoteNum = $('td', nRow)[0].innerHTML;
$(nRow).attr("id", quoteNum); /* set row ids to quote number */
$('td:eq(0)', nRow).click(function() {$(this.parentNode).toggleClass('selected',this.clicked);}); /* add row selectors */
$('td:eq(0)', nRow).attr("title", _("Click ID to select/deselect quote"));
/* apply no_edit id to noEditFields */
noEditFields = [0]; /* number */
for (i=0; i<noEditFields.length; i++) {
@ -335,44 +341,44 @@
$('#file_upload').one('change', fnHandleFileSelect);
});
var MSG_IMPORT_SUCCESS = _("%s quotes imported successfully");
var MSG_IMPORT_ERROR = _("%s quotes have not been imported. An error occurred");
function fnGetData(element) {
var jqXHR = $.ajax({
url : "/cgi-bin/koha/tools/quotes/quotes-upload_ajax.pl",
type : "POST",
contentType : "application/x-www-form-urlencoded", // we must claim this mimetype or CGI will not decode the URL encoding
dataType : "json",
data : {
"quote" : encodeURI ( JSON.stringify(oTable.fnGetData()) ),
"action" : "add",
},
success : function(){
var response = JSON.parse(jqXHR.responseText);
if (response.success) {
alert(_("%s quotes saved.").format(response.records));
window.location.reload(true); // is this the best route?
} else {
alert(_("%s quotes saved, but an error has occurred. Please ask your administrator to check the server log for more details.").format(response.records));
window.location.reload(true); // is this the best route?
}
},
var lines = oTable.fnGetData();
$(lines).each(function(line){
var data = {source: this[1], text: this[2]};
var success = 0; var error = 0;
$.ajax({
url : "/api/v1/quotes",
method : "POST",
data : JSON.stringify(data),
dataType : "application/json",
success : function(data) {
$("#messages").show();
var import_success = $("#messages .import_success");
import_success.show();
import_success.data("nb")
nb_success = import_success.data("nb") || 0;
nb_success++;
$("#messages .import_success").text(MSG_IMPORT_SUCCESS.format(nb_success));
import_success.data("nb", nb_success);
},
error : function(xhr) {
if (xhr.status==201) { this.success(null, "Created", xhr); return; }
$("#messages").show();
var import_error = $("#messages .import_error");
import_error.show();
import_error.data("nb")
nb_error = import_error.data("nb") || 0;
nb_error++;
$("#messages .import_error").text(MSG_IMPORT_ERROR.format(nb_error));
import_error.data("nb", nb_error);
},
});
});
}
function fnClickDeleteRow() {
var idsToDelete = oTable.$('.selected').map(function() {
return this.id;
}).get().join(', ');
if (!idsToDelete) {
alert(_("Please select a quote(s) by clicking the quote id(s) you desire to delete."));
}
else if (confirm(_("Are you sure you wish to delete quote(s) %s?").format(idsToDelete))) {
oTable.$('.selected').each(function(){
oTable.fnDeleteRow(this);
});
}
}
}); // $(document).ready
</script>
[% END %]

378
koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt

@ -1,10 +1,10 @@
[% USE raw %]
[% USE Asset %]
[% SET footerjs = 1 %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Tools &rsaquo; Quote editor</title>
[% INCLUDE 'doc-head-close.inc' %]
[% Asset.css("css/quotes.css") | $raw %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Tools &rsaquo; Quote editor</title>
[% INCLUDE 'doc-head-close.inc' %]
[% Asset.css("css/quotes.css") | $raw %]
</head>
<body id="tools_quotes" class="tools">
@ -18,43 +18,110 @@
<div class="col-sm-10 col-sm-push-2">
<main>
[% INCLUDE 'quotes-toolbar.inc' %]
<h2>Quote editor</h2>
<div id="instructions">
<fieldset id="quote_editor_help" class="rows">
<legend>Instructions</legend>
<div id="quote_editor_inst">
<ul>
<li>Click on the 'Add quote' button to add a single quote; Press the &lt;Enter&gt; key to save the quote.<br />
<strong>Note: </strong>Both the 'source' and 'text' fields must have content in order for the quote to be saved.</li>
<li>Click on any field to edit the contents; Press the &lt;Enter&gt; key to save edit.</li>
<li>Click on one or more quote numbers to select entire quotes for deletion; Click the 'Delete quote(s)' button to delete selected quotes.</li>
<li>Click the 'Import quotes' button in the toolbar to import a CSV file of quotes.</li>
</ul>
</div>
</fieldset>
[% FOREACH m IN messages %]
<div class="dialog [% m.type | html %]" id="quote_action_result_dialog">
[% SWITCH m.code %]
[% CASE 'error_on_update' %]
An error occurred when updating this quote. Perhaps it already exists.
[% CASE 'error_on_insert' %]
An error occurred when adding this quote.
[% CASE 'success_on_update' %]
Quote updated successfully.
[% CASE 'success_on_insert' %]
Quote added successfully.
[% CASE %]
[% m.code | html %]
[% END %]
</div>
[% END %]
<div class="dialog message" id="quote_delete_success" style="display: none;"></div>
<div class="dialog alert" id="quote_delete_error" style="display: none;"></div>
[% IF op == 'list' %]
<div id="toolbar" class="btn-toolbar">
<a class="btn btn-default" id="newquote" href="/cgi-bin/koha/tools/quotes.pl?op=add_form"><i class="fa fa-plus"></i> New quote</a>
<a class="btn btn-default" id="import_quotes" href="/cgi-bin/koha/tools/quotes-upload.pl"><i class="fa fa-folder-open"></i> Import quotes</a>
</div>
[% END %]
[% IF op == 'add_form' %]
<h3>[% IF quote %]Modify quote[% ELSE %]New quote[% END %]</h3>
<form action="/cgi-bin/koha/tools/quotes.pl" id="Aform" name="Aform" class="validated" method="post">
<fieldset class="rows">
<input type="hidden" name="op" value="add_validate" />
<ol>
<li>
<label for="text" class="required">Source: </label>
<input type="text" name="source" id="source" value="[% quote.source | html %]" class="required" required="required" />
<span class="required">Required</span>
</li>
<li>
<label for="text" class="required">Text: </label>
<textarea name="text" id="text" class="required" required="required" />[% quote.text | html %]</textarea>
<span class="required">Required</span>
</li>
</ol>
</fieldset>
<fieldset class="action">
<input type="hidden" name="id" value="[% quote.id %]" />
<input type="submit" value="Submit" />
<a class="cancel" href="/cgi-bin/koha/tools/quotes.pl">Cancel</a>
</fieldset>
</form>
[% END %]
[% IF op == 'delete_confirm' %]
<div class="dialog alert">
<form action="/cgi-bin/koha/tools/quotes.pl" method="post">
<h3>Are you sure you want to delete the following quote?</h3>
[% quote.source | html %] - [% quote.text | html %]
<input type="hidden" name="op" value="delete_confirmed" />
<input type="hidden" name="id" value="[% quote.id | html %]" />
<button type="submit" class="approve"><i class="fa fa-fw fa-check"></i> Yes, delete</button>
</form>
<form action="/cgi-bin/koha/tools/quotes.pl" method="get">
<button type="submit" class="deny"><i class="fa fa-fw fa-remove"></i> No, do not delete</button>
</form>
</div>
[% END %]
[% IF op == 'list' %]
<h3>Quotes</h3>
[% IF quotes_count > 0 %]
<table id="quotes">
<thead>
<tr>
<th>ID</th>
<th>Source</th>
<th>Text</th>
<th>Last display</th>
<th data-class-name="actions">Actions</th>
</tr>
</thead>
</table>
[% ELSE %]
<div class="dialog message">There are no quotes defined. <a href="/cgi-bin/koha/tools/quotes.pl?op=add_form">Start defining quotes</a>.</div>
[% END %]
<div id="delete_confirm_modal" class="modal" tabindex="-1" role="dialog" aria-labelledby="delete_confirm_modal_label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="delete_confirm_modal_label">Delete quote</h3>
</div>
<table id="quotes_editor">
<thead>
<tr>
<th><span style="cursor: help" id="id_help">ID</span></th>
<th>Source</th>
<th>Text</th>
<th>Last displayed</th>
</tr>
</thead>
<tbody>
<!-- tbody content is generated by DataTables -->
<tr>
<td></td>
<td></td>
<td>Loading data...</td>
<td></td>
</tr>
</tbody>
</table>
<fieldset id="footer" class="action">
</fieldset>
<div class="modal-body">
<div id="delete_confirm_dialog"></div>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-default" id="delete_confirm_modal_button" role="button" data-toggle="modal">Delete</a>
<button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- #delete_confirm_modal -->
[% END %]
</main>
</div> <!-- /.col-sm-10.col-sm-push-2 -->
@ -68,164 +135,99 @@
[% MACRO jsinclude BLOCK %]
[% Asset.js("js/tools-menu.js") | $raw %]
[% INCLUDE 'js-date-format.inc' %]
[% INCLUDE 'datatables.inc' %]
[% Asset.js("lib/jquery/plugins/dataTables.fnReloadAjax.js") | $raw %]
[% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %]
<script>
var oTable; /* oTable needs to be global */
var sEmptyTable = _("No quotes available. Please use the 'Add quote' button to add a quote."); /* override the default message in datatables.inc */
$(document).ready(function() {
/* NOTE: This is an ajax-source datatable and *not* a server-side sourced datatable. */
/* See the datatable docs if you don't understand this difference. */
oTable = $("#quotes_editor").dataTable({
"bAutoWidth" : false,
"bProcessing" : true,
"bPaginate" : true,
"sPaginationType" : "full_numbers",
"sDom": '<"top pager"iflp>rt<"bottom pager"flp><"clear">',
"sAjaxSource" : "/cgi-bin/koha/tools/quotes/quotes_ajax.pl",
"aoColumns" : [
{ "sWidth": "3%" },
{ "sWidth": "11%" },
{ "sWidth": "75%" },
{ "sWidth": "11%" },
],
"oLanguage": dataTablesDefaults.oLanguage,
"fnPreDrawCallback": function(oSettings) {
return true;
var quotes_url = '/api/v1/quotes';
var quotes = $("#quotes").api({
"ajax": {
"url": quotes_url
},
"fnRowCallback": function( nRow, aData, iDisplayIndex ) {
/* do foo on the current row and its child nodes */
var noEditFields = [];
var quoteID = $('td', nRow)[0].innerHTML;
$(nRow).attr("id", quoteID); /* set row ids to quote id */
$('td:eq(0)', nRow).click(function() {$(this.parentNode).toggleClass('selected',this.clicked);}); /* add row selectors */
$('td:eq(0)', nRow).attr("title", _("Click ID to select/deselect quote"));
$('td', nRow).attr("id",quoteID); /* FIXME: this is a bit of a hack */
if (isNaN(quoteID)) {
noEditFields = [0,1,2,3]; /* all fields when adding a quote */
} else {
noEditFields = [0,3]; /* id, timestamp */
}
/* apply no_edit id to noEditFields */
for (i=0; i<noEditFields.length; i++) {
$('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
'emptyTable': '<div class="dialog message">'+_("There are no quotes defined.")+' <a href="/cgi-bin/koha/tools/quotes.pl?op=add_form">'+_("Start defining quotes")+'</a>.</div>',
"columnDefs": [ {
"targets": [0,1,2,3],
"render": function (data, type, row, meta) {
if ( type == 'display' ) {
if ( data != null ) {
return data.escapeHtml();
}
else {
return "";
}
}
return data;
}
return nRow;
},
"fnDrawCallback": function(oSettings) {
/* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
$('#quotes_editor tbody td[id!="no_edit"]').editable( "/cgi-bin/koha/tools/quotes/quotes_ajax.pl", {
"submitdata" : function ( value, settings ) {
return {
"column" : oTable.fnGetPosition( this )[2],
"action" : "edit",
};
},
"placeholder" : "Saving data...",
});
},
});
$("#add_quote").click(function(){
fnClickAddRow();
return false;
});
$("#delete_quote").click(function(){
fnClickDeleteRow();
return false;
});
$("#id_help").on("click",function(e){
e.stopPropagation();
alert( _("Click on the quote's id to select or deselect the quote. Multiple quotes may be selected.") );
});
});
} ],
"columns": [
{
"data": "quote_id",
"searchable": true,
"orderable": true
},
{
"data": "source",
"searchable": true,
"orderable": true
},
{
"data": "text",
"searchable": true,
"orderable": true
},
{
"data": function( row, type, val, meta ) {
return $datetime(row.displayed_on);
},
"searchable": true,
"orderable": true
},
{
"data": function( row, type, val, meta ) {
function fnClickAddQuote(e, node) {
if (e.charCode) {
/* some browsers only give charCode, so this will need to be */
/* fixed up to handle that */
console.log('charCode: '+e.charCode);
}
if (e.keyCode == 13) {
var quoteSource = $('#quoteSource').val();
var quoteText = $('#quoteText').val()
/* If passed a quote source, add the quote to the db */
if (quoteSource && quoteText) {
$.ajax({
url: "/cgi-bin/koha/tools/quotes/quotes_ajax.pl",
type: "POST",
data: {
"source" : quoteSource,
"text" : quoteText,
"action" : "add",
var result = '<a class="btn btn-default btn-xs" href="/cgi-bin/koha/tools/quotes.pl?op=add_form&amp;id='+encodeURIComponent(row.quote_id)+'" role="button"><i class="fa fa-pencil" aria-hidden="true"></i> '+_("Edit")+'</a>';
result += '<form action="/cgi-bin/koha/tools/quotes.pl" method="post">';
result += '<input type="hidden" name="id" value="'+row.quote_id.escapeHtml()+'" />'+"\n";
result += '<a class="btn btn-default btn-xs delete_quote" role="button" href="#" data-toggle="modal" data-target="#delete_confirm_modal" data-quote-id="'+ encodeURIComponent(row.quote_id) +'"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Delete")+'</a>';
return result;
},
success: function(data){
var newQuote = data[0];
var aRow = oTable.fnUpdate(
newQuote,
node,
undefined,
false,
false
);
oTable.fnPageChange( 'last' );
$('.add_quote_button').attr('onclick', 'fnClickAddRow()'); // re-enable add button
}
});
} else {
alert(_("Please supply both the text and source of the quote before saving."));
}
} else if (e.keyCode == 27) {
if (confirm(_("Are you sure you want to cancel adding this quote?"))) {
oTable.fnDeleteRow(node);
} else {
return;
}
}
}
function fnClickAddRow() {
$('.add_quote_button').removeAttr('onclick'); // disable add button once it has been clicked
var aRow = oTable.fnAddData(
[
'NA', // this is hackish to fool the datatable sort routine into placing this row at the end of the list...
'<input id="quoteSource" type="text" style="width:99%" onkeydown="fnClickAddQuote(event,this.parentNode.parentNode)"/>',
'<input id="quoteText" type="text" style="width:99%" onkeydown="fnClickAddQuote(event,this.parentNode.parentNode)"/>',
'0000-00-00 00:00:00',
],
false
);
oTable.fnPageChange( 'last' );
$('#quoteSource').focus();
}
function fnClickDeleteRow() {
var idsToDelete = oTable.$('.selected').map(function() {
return this.id;
}).get().join(', ');
if (!idsToDelete) {
alert(_("Please select a quote(s) by clicking the quote id(s) you desire to delete."));
} else if (confirm(_("Are you sure you wish to delete quote(s) %s?").format(idsToDelete))) {
oTable.$('.selected').each(function(){
var quoteID = $(this).attr('id');
"searchable": false,
"orderable": false
},
]
});
$('#quotes').on( "click", '.delete_quote', function () {
var quote_id = decodeURIComponent($(this).data('quote-id'));
$("#delete_confirm_dialog").html(
_("You are about to delete the quote #%s.").format(quote_id)
);
$("#delete_confirm_modal_button").unbind("click").on( "click", function () {
$.ajax({
url: "/cgi-bin/koha/tools/quotes/quotes_ajax.pl",
type: "POST",
data: {
"id" : quoteID,
"action" : "delete",
},
/* Delete the row from the datatable */
success: function(){
oTable.fnDeleteRow(this);
oTable.fnReloadAjax(null, null, true);
}
method: "DELETE",
url: "/api/v1/quotes/"+quote_id
}).success(function() {
$("#delete_confirm_modal").modal('hide');
quotes.api().ajax.reload(function (data) {
if (data.recordsTotal == 0) {
$("#quotes").hide();
}
$("#quote_action_result_dialog").hide();
$("#quote_delete_success").html(_("Quote #%s deleted successfully.").format(quote_id)).show();
});
}).error(function () {
$("#quote_delete_error").html(_("Error deleting quote #%s. Check the logs.").format(quote_id)).show();
});
});
} else {
return;
}
}
});
});
</script>
[% END %]

77
tools/quotes.pl

@ -1,7 +1,5 @@
#!/usr/bin/perl
# Copyright 2012 Foundations Bible College Inc.
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
@ -20,23 +18,80 @@
use Modern::Perl;
use CGI qw ( -utf8 );
use autouse 'Data::Dumper' => qw(Dumper);
use Try::Tiny;
use C4::Auth;
use C4::Koha;
use C4::Context;
use C4::Output;
use Koha::Quotes;
my $cgi = CGI->new;
my $input = CGI->new;
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
{
template_name => "tools/quotes.tt",
query => $cgi,
type => "intranet",
flagsrequired => { tools => 'edit_quotes' },
debug => 1,
template_name => "tools/quotes.tt",
query => $input,
type => "intranet",
flagsrequired => { tools => 'edit_quotes' },
debug => 1,
}
);
output_html_with_http_headers $cgi, $cookie, $template->output;
my $id = $input->param('id');
my $op = $input->param('op') || 'list';
my @messages;
if ( $op eq 'add_form' ) {
$template->param( quote => Koha::Quotes->find($id), );
}
elsif ( $op eq 'add_validate' ) {
my @fields = qw(
source
text
);
if ($id) {
my $quote = Koha::Quotes->find($id);
for my $field (@fields) {
$quote->$field( scalar $input->param($field) );
}
try {
$quote->store;
push @messages, { type => 'message', code => 'success_on_update' };
}
catch {
push @messages, { type => 'alert', code => 'error_on_update' };
}
}
else {
my $quote = Koha::Quote->new(
{
id => $id,
( map { $_ => scalar $input->param($_) || undef } @fields )
}
);
try {
$quote->store;
push @messages, { type => 'message', code => 'success_on_insert' };
}
catch {
push @messages, { type => 'alert', code => 'error_on_insert' };
};
}
$op = 'list';
}
else {
$op = 'list';
}
$template->param( quotes_count => Koha::Quotes->search->count )
if $op eq 'list';
$template->param(
messages => \@messages,
op => $op,
);
output_html_with_http_headers $input, $cookie, $template->output;

65
tools/quotes/quotes-upload_ajax.pl

@ -1,65 +0,0 @@
#!/usr/bin/perl
# Copyright 2012 Foundations Bible College Inc.
#
# 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 CGI qw ( -utf8 );
use JSON;
use URI::Escape;
use autouse 'Data::Dumper' => qw(Dumper);
use C4::Auth;
use C4::Koha;
use C4::Context;
use C4::Output;
my $cgi = CGI->new;
my $dbh = C4::Context->dbh;
my ( $status, $cookie, $sessionID ) = C4::Auth::check_api_auth( $cgi, { tools => 'edit_quotes' } );
unless ($status eq "ok") {
print $cgi->header(-type => 'application/json', -status => '403 Forbidden');
print to_json({ auth_status => $status });
exit 0;
}
my $success = 'true';
my $quotes_tmp = uri_unescape( $cgi->param('quote' ) );
my $quotes = decode_json( $quotes_tmp );
my $action = $cgi->param('action');
my $sth = $dbh->prepare('INSERT INTO quotes (source, text) VALUES (?, ?);');
my $insert_count = 0;
foreach my $quote (@$quotes) {
$insert_count++ if $sth->execute($quote->[1], $quote->[2]);
if ($sth->err) {
warn sprintf('Database returned the following error: %s', $sth->errstr);
$success = 'false';
}
}
print $cgi->header('application/json');
print to_json({
success => $success,
records => $insert_count,
});

109
tools/quotes/quotes_ajax.pl

@ -1,109 +0,0 @@
#!/usr/bin/perl
# Copyright 2012 Foundations Bible College Inc.
#
# 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 CGI qw ( -utf8 );
use JSON;
use autouse 'Data::Dumper' => qw(Dumper);
use C4::Auth;
use C4::Context;
my $cgi = CGI->new;
my $dbh = C4::Context->dbh;
my $sort_columns = ["id", "source", "text", "timestamp"];
my ( $status, $cookie, $sessionID ) = C4::Auth::check_api_auth( $cgi, { tools => 'edit_quotes' } );
unless ($status eq "ok") {
print $cgi->header(-type => 'application/json', -status => '403 Forbidden');
print to_json({ auth_status => $status });
exit 0;
}
# NOTE: This is a collection of ajax functions for use with tools/quotes.pl
my $params = $cgi->Vars; # NOTE: Multivalue parameters NOT allowed!!
print $cgi->header('application/json; charset=utf-8');
my $action = $params->{'action'} || 'get';
if ($action eq 'add') {
my $sth = $dbh->prepare('INSERT INTO quotes (source, text) VALUES (?, ?);');
$sth->execute($params->{'source'}, $params->{'text'});
if ($sth->err) {
warn sprintf('Database returned the following error: %s', $sth->errstr);
exit 0;
}
my $new_quote_id = $dbh->{q{mysql_insertid}}; # ALERT: mysqlism here
$sth = $dbh->prepare('SELECT * FROM quotes WHERE id = ?;');
$sth->execute($new_quote_id);
print to_json($sth->fetchall_arrayref, {utf8 =>1});
exit 0;
}
elsif ($action eq 'edit') {
my $aaData = [];
my $editable_columns = [qw(source text)]; # pay attention to element order; these columns match the quotes table columns
my $sth = $dbh->prepare("UPDATE quotes SET $editable_columns->[$params->{'column'}-1] = ? WHERE id = ?;");
$sth->execute($params->{'value'}, $params->{'id'});
if ($sth->err) {
warn sprintf('Database returned the following error: %s', $sth->errstr);
exit 1;
}
$sth = $dbh->prepare("SELECT $editable_columns->[$params->{'column'}-1] FROM quotes WHERE id = ?;");
$sth->execute($params->{'id'});
$aaData = $sth->fetchrow_array();
print Encode::encode('utf8', $aaData);
exit 0;
}
elsif ($action eq 'delete') {
my $sth = $dbh->prepare("DELETE FROM quotes WHERE id = ?;");
$sth->execute($params->{'id'});
if ($sth->err) {
warn sprintf('Database returned the following error: %s', $sth->errstr);
exit 1;
}
exit 0;
}
else {
my $aaData = [];
my $iTotalRecords = '';
my $sth = '';
$iTotalRecords = $dbh->selectrow_array('SELECT count(*) FROM quotes;');
$sth = $dbh->prepare("SELECT * FROM quotes;");
$sth->execute();
if ($sth->err) {
warn sprintf('Database returned the following error: %s', $sth->errstr);
exit 1;
}
$aaData = $sth->fetchall_arrayref;
my $iTotalDisplayRecords = $iTotalRecords; # no filtering happening here
print to_json({
iTotalRecords => $iTotalRecords,
iTotalDisplayRecords=> $iTotalDisplayRecords,
sEcho => $params->{'sEcho'},
aaData => $aaData,
}, {utf8 =>1});
}
Loading…
Cancel
Save