From 3bcda99e767623d6d929642c789a578f5478088a Mon Sep 17 00:00:00 2001
From: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Date: Thu, 29 Jun 2023 08:49:38 +0000
Subject: [PATCH] Bug 33537: Add staff template and script
Content-Type: text/plain; charset=utf-8

Test plan:
Go to Administration/SMTP Servers.
Click Domain limits.
Do some CRUD operations.
Bonus:
Add another tab with domain limits.
Edit or remove records on tab 1.
Try CRUD on changed or removed records on tab 2.

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
[EDIT] Added some preventDefault calls, as suggested by JD.
---
 admin/domain_limits.pl                        |  35 +++
 .../prog/en/modules/admin/domain_limits.tt    | 111 ++++++++
 .../prog/en/modules/admin/smtp_servers.tt     |   1 +
 .../intranet-tmpl/prog/js/domain_limits.js    | 246 ++++++++++++++++++
 4 files changed, 393 insertions(+)
 create mode 100755 admin/domain_limits.pl
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/domain_limits.tt
 create mode 100644 koha-tmpl/intranet-tmpl/prog/js/domain_limits.js

diff --git a/admin/domain_limits.pl b/admin/domain_limits.pl
new file mode 100755
index 0000000000..b6679a19ee
--- /dev/null
+++ b/admin/domain_limits.pl
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+# Copyright 2023 Rijksmuseum, Koha development team
+#
+# 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 C4::Auth qw( get_template_and_user );
+use C4::Output qw( output_html_with_http_headers );
+
+my $input = CGI->new;
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name => "admin/domain_limits.tt",
+        query         => $input,
+        type          => "intranet",
+        flagsrequired => { parameters => 'manage_smtp_servers' },
+    }
+);
+output_html_with_http_headers $input, $cookie, $template->output;
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/domain_limits.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/domain_limits.tt
new file mode 100644
index 0000000000..a830de71a7
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/domain_limits.tt
@@ -0,0 +1,111 @@
+[% USE raw %]
+[% USE Asset %]
+[% SET footerjs = 1 %]
+
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Domain limits &rsaquo; Administration &rsaquo; Koha</title>
+[% INCLUDE 'doc-head-close.inc' %]
+</head>
+
+<body id="admin_domain_limits" class="admin">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'prefs-admin-search.inc' %]
+
+[% BLOCK modal_form %]
+<div id="domain_limits_modal" class="modal" role="dialog" aria-hidden="true">
+<div class="modal-dialog">
+<div class="modal-content">
+<form class="modal-form">
+    <div class="modal-header">
+        <h3 class="modal-title add">Add domain limit</h4>
+        <h3 class="modal-title add_member">Add member</h4>
+        <h3 class="modal-title edit edit_member">Edit domain limit</h4>
+    </div>
+    <div class="modal-body"><fieldset class="rows"><ul>
+        <li class="add edit add_member edit_member"><label for="domain_name" class="">Domain name</label>
+        <input type="text" name="domain_name" class="" required/></li>
+
+        <li class="add edit"><label for="domain_limit">Message limit</label>
+        <input type="text" name="domain_limit" class="toggle_validation" required inputmode="numeric" pattern="[0-9]*"/></li>
+
+        <li class="add edit"><label for="units">Units</label></td>
+        <input type="text" name="units" class="toggle_validation" required inputmode="numeric" pattern="[0-9]*"/></li>
+
+        <li class="add edit"><label for="unit_type">Unit type</label>
+        <select name="unit_type" class="toggle_validation" required>
+            <option value="days"   >days   </option>
+            <option value="hours"  >hours  </option>
+            <option value="minutes">minutes</option>
+        </select></li>
+
+        <li class="add_member edit_member"><label for="group_domain">Group domain</label>
+        <input type="text" name="group_domain" class="" disabled/></li>
+    </ul></fieldset></div>
+    <div class="modal-footer">
+        <button class="btn btn-default save" data-dismiss="modal" role="button">Save</button>
+        <button class="btn btn-default cancel" data-dismiss="modal" role="button">Cancel</button>
+    </div>
+</form>
+</div>
+</div>
+</div>
+[% END %]
+
+[% WRAPPER 'sub-header.inc' %]
+    [% WRAPPER breadcrumbs %]
+        [% WRAPPER breadcrumb_item %]
+            <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a>
+        [% END %]
+        [% WRAPPER breadcrumb_item bc_active= 1 %]
+            <span>Domain limits</span>
+        [% END %]
+    [% END #/ WRAPPER breadcrumbs %]
+[% END #/ WRAPPER sub-header.inc %]
+
+<div class="main container-fluid">
+    <div class="row">
+    <div class="col-sm-10 col-sm-push-2">
+    <main>
+
+    <div class="dialog message" id="domain_limit_message" style="display:none;"></div>
+    <div class="dialog alert"   id="domain_limit_alert"   style="display:none;"></div>
+
+    <div id="toolbar" class="btn-toolbar">
+        <a class="btn btn-default add" href="#"><i class="fa fa-plus"></i> New limit</a>
+    </div>
+
+    <h1>Domain limits</h1>
+    <div class="page-section">
+        <table id="domain_limits">
+            <thead>
+                <tr>
+                    <th>Domain</th>
+                    <th>Message limit</th>
+                    <th>Units</th>
+                    <th>Unit type</th>
+                    <th>Belongs to</th>
+                    <th data-class-name="actions noExport">Actions</th>
+                </tr>
+            </thead>
+        </table>
+        [% INCLUDE modal_form %]
+    </div>
+
+    </main>
+    </div> <!-- /.col-sm-10.col-sm-push-2 -->
+
+    <div class="col-sm-2 col-sm-pull-10">
+        <aside>
+            [% INCLUDE 'admin-menu.inc' %]
+        </aside>
+    </div>
+    </div> <!-- /.row -->
+<!-- /div in bottom include -->
+
+[% MACRO jsinclude BLOCK %]
+    [% Asset.js("js/admin-menu.js") | $raw %]
+    [% INCLUDE 'datatables.inc' %]
+    [% Asset.js("js/domain_limits.js") | $raw %]
+[% END %]
+
+[% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smtp_servers.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smtp_servers.tt
index 83e6c628bd..07afa84259 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smtp_servers.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smtp_servers.tt
@@ -248,6 +248,7 @@
 
     <div id="toolbar" class="btn-toolbar">
         <a class="btn btn-default" id="new_smtp_server" href="/cgi-bin/koha/admin/smtp_servers.pl?op=add_form"><i class="fa fa-plus"></i> New SMTP server</a>
+        <a class="btn btn-default" id="mail_domain_limits" href="/cgi-bin/koha/admin/domain_limits.pl">Domain limits</a>
     </div>
 
     <h1>SMTP servers</h1>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/domain_limits.js b/koha-tmpl/intranet-tmpl/prog/js/domain_limits.js
new file mode 100644
index 0000000000..69ddb768d4
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/js/domain_limits.js
@@ -0,0 +1,246 @@
+// domain_limits.js: Code for the corresponding admin form
+
+var domain_limits_table;
+$(document).ready(function() {
+    domain_limits_table = init_table();
+    $('#toolbar').on( "click", 'a.add', function (ev) {
+        ev.preventDefault();
+        start_modal( 'add' );
+    });
+    $('#domain_limits').on( "click", 'a.add_member', function (ev) {
+        ev.preventDefault();
+        start_modal( 'add_member', { belongs_to: $(this).parents('tr').attr('id') } );
+    });
+    $('#domain_limits').on( "click", 'a.edit, a.edit_member', function (ev) {
+        ev.preventDefault();
+        hide_dialogs();
+        var limit_id = $(this).parents('tr').attr('id');
+        api_get(limit_id);
+    });
+    $('#domain_limits').on( "click", 'a.delete', function (ev) {
+        ev.preventDefault();
+        hide_dialogs();
+        if ( window.confirm( _("Delete this limit?") ) ) {
+            var limit_id = $(this).parents('tr').attr('id');
+            api_delete( limit_id );
+        }
+    });
+    $('#domain_limits_modal').on( 'keydown', function(ev) {
+        if ( ev.which == 27 ) { // respond to escape key
+          $('#domain_limits_modal').hide();
+          return false;
+        }
+    });
+    $('#domain_limits_modal button.save').on( 'click', function() {
+        if ( !$('#domain_limits_modal form').get(0).checkValidity() ) return; // Do not return false here to allow validation.
+        var limit_id = $('#domain_limits_modal .modal-form').data('id');
+        var row = get_modal_inputs();
+        if ( limit_id ) api_put(limit_id, row); else api_post(row);
+        return false;
+    });
+    $('#domain_limits_modal button.cancel').on( 'click', function() {
+        $('#domain_limits_modal').hide();
+        return false;
+    });
+});
+
+function api_get (limit_id) {
+    var row = domain_limits_table.DataTable().row('#'+limit_id).data();
+    $.ajax({
+        method: "GET",
+        url: '/api/v1/smtp/domain_limits/' + limit_id,
+    }).done(function (data) {
+        start_modal( row.belongs_to ? 'edit_member' : 'edit', data );
+    }).fail(function (xhr) {
+        status_message( 'get', xhr.status, row );
+    });
+}
+
+function api_delete (limit_id) {
+    var limit_name = domain_limits_table.DataTable().row('#'+limit_id).data().domain_name;
+    $.ajax({
+        method: "DELETE",
+        url: '/api/v1/smtp/domain_limits/' + limit_id,
+    }).done(function () {
+        domain_limits_table.api().ajax.reload();
+        $('#domain_limit_message').html( _("Limit for '%s' deleted.").format(limit_name) ).show();
+    }).fail(function (xhr) {
+        status_message( 'delete', xhr.status, { domain_name: limit_name } );
+    });
+}
+
+function api_post (row) {
+    $.ajax({
+        method: "POST",
+        url: '/api/v1/smtp/domain_limits',
+        data: JSON.stringify(row),
+        contentType: 'application/json',
+    }).done(function () {
+        domain_limits_table.api().ajax.reload();
+        $('#domain_limit_message').html( _("Limit for '%s' added.").format(row['domain_name']) ).show();
+    }).fail(function (xhr, status, error) {
+        status_message( 'post', xhr.status, row );
+    }).always(function () {
+        $('#domain_limits_modal').hide();
+    });;
+}
+
+function api_put (limit_id, row) {
+    $.ajax({
+        method: "PUT",
+        url: '/api/v1/smtp/domain_limits/' + limit_id,
+        data: JSON.stringify(row),
+        contentType: 'application/json',
+    }).done(function () {
+        domain_limits_table.api().ajax.reload();
+        $('#domain_limit_message').html( _("Limit for '%s' modified.").format(row.domain_name) ).show();
+    }).fail(function (xhr, status, error) {
+        status_message( 'put', xhr.status, row );
+    }).always(function () {
+        $('#domain_limits_modal').hide();
+    });
+}
+
+function status_message (method, status, row) { // only used in ajax.fail
+    // Extending with parsing xhr.responseText seems not needed here (now).
+    // POST and PUT may trigger similar exceptions, GET and DELETE are trivial.
+    var messages = {
+        delete          : _( "Deleting limit for '%s' failed." ),
+        get             : _( "Retrieving limit for '%s' failed." ),
+        put             : _( "Modifying limit for '%s' failed." ),
+        post            : _( "Adding limit for '%s' failed." ),
+        status_400      : _( " (Status 400 - Bad request: expected columns were empty.)" ),
+        status_401      : _( " (Status 401 - Unauthorized: session may have expired.)" ),
+        status_403      : _( " (Status 403 - Forbidden: check permissions.)" ),
+        status_404      : _( " (Status 404 - Not found)" ),
+        status_404_put  : _( " (Status 404 - Not found: group domain does not exist)" ),
+        status_404_post : _( " (Status 404 - Not found: group domain does not exist)" ),
+        status_409      : _( " (Status 409 - Conflict: duplicate domain name.)" ),
+    };
+    var str = messages['status_'+status+'_'+method] || messages['status_'+status] || '';
+    $( '#domain_limit_alert' ).html( messages[method].format(row.domain_name) + str ).show();
+}
+
+function hide_dialogs () {
+    $('div.dialog').html('').hide();
+}
+
+function get_actions_html (row) {
+    var result = '';
+    if ( row.belongs_to == undefined ) {
+        result += '<a class="btn btn-default btn-xs edit" href="#"><i class="fa fa-pencil-alt"></i> '+_("Edit")+'</a>'+"\n";
+        result += '<a class="btn btn-default btn-xs delete" href="#"><i class="fa fa-trash-can"></i> '+_("Delete")+'</a>'+"\n";
+        result += '<a class="btn btn-default btn-xs add_member" href="#"><i class="fa fa-plus"></i> '+_("New member")+'</a>'+"\n";
+    } else {
+        result += '<a class="btn btn-default btn-xs edit_member" href="#"><i class="fa fa-pencil-alt"></i> '+_("Edit")+'</a>'+"\n";
+        result += '<a class="btn btn-default btn-xs delete" href="#"><i class="fa fa-trash-can"></i> '+_("Delete")+'</a>'+"\n";
+    }
+    return result;
+}
+
+function get_modal_inputs () {
+    var row = {};
+    row.domain_name = $('input[name="domain_name"]').val();
+    if( $('input[name="group_domain"]:visible').length ) {
+        row.belongs_to = $('#domain_limits_modal .modal-form').data('belongs_to');
+    } else {
+        row.domain_limit = $('input[name="domain_limit"]').val();
+        row.units = $('input[name="units"]').val();
+        row.unit_type = $('select[name="unit_type"]').val();
+    }
+    return row;
+}
+
+function toggle_modal_inputs (action) {
+    hide_dialogs();
+    // Show one title
+    $('h3.modal-title').hide().filter('.'+action).show()
+    // Hide and disable some inputs (disable needed to skip validation)
+    $('.modal-body li').hide().filter('.'+action).show();
+    $('.modal-body .toggle_validation').prop('disabled', true);
+    $('.modal-body li.'+action+' .toggle_validation').prop('disabled', false);
+}
+
+function fill_modal_inputs (action, row) {
+    // Start by clearing
+    $('#domain_limits_modal input').val('');
+    $('#domain_limits_modal select').val('');
+
+    if ( action=='edit' ) {
+        for ( const field_name of [ 'domain_name', 'domain_limit', 'units' ] ) {
+            $('#domain_limits_modal input[name="'+field_name+'"]').val(row[field_name]);
+        }
+        $('#domain_limits_modal select').val(row['unit_type']);
+    } else if ( action=='edit_member' ) {
+        $('#domain_limits_modal input[name="domain_name"]').val(row.domain_name);
+        $('#domain_limits_modal input[name="group_domain"]').val(row.group_domain);
+    } else if( action=='add_member' ) {
+        $('#domain_limits_modal input[name="group_domain"]').val(row.group_domain);
+    }
+}
+
+function start_modal (action, row) {
+    toggle_modal_inputs(action);
+    $('#domain_limits_modal .modal-form').data('id', row ? row.domain_limit_id : null).data('belongs_to', row ? row.belongs_to : null);
+    if( action=='add_member' ) {
+        // fetch group domain name from corresponding row
+        var group_domain = domain_limits_table.DataTable().row('#' + row.belongs_to).data().domain_name;
+        fill_modal_inputs( action, { group_domain: group_domain } );
+    } else {
+        fill_modal_inputs(action, row);
+    }
+    $('#domain_limits_modal').show().find('input')[0].focus();
+}
+
+function init_table () {
+    return $("#domain_limits").kohaTable({
+        "ajax": {
+            "url": '/api/v1/smtp/domain_limits',
+        },
+        'language': {
+            'emptyTable': '<div>'+_("There are no domain limits defined.")+'</div>'
+        },
+        "columnDefs": [ {
+            "targets": [0,1,2,3,4],
+            "render": function (data, type, row, meta) {
+                if ( type == 'display' && data != null ) {
+                    return data.escapeHtml();
+                }
+                return data;
+            }
+        } ],
+        "columns": [
+            {
+                "data": "domain_name",
+                "searchable": true,
+                "orderable": true,
+            },
+            {
+                "data": "domain_limit",
+                "searchable": true,
+                "orderable": true,
+            },
+            {
+                "data": "units",
+                "searchable": true,
+                "orderable": true,
+            },
+            {
+                "data": "unit_type",
+                "searchable": true,
+                "orderable": true,
+            },
+            {
+                "data": "group_domain",
+                "searchable": false,
+                "orderable": false,
+            },
+            {
+                "data": function( row, type, val, meta ) { return get_actions_html(row) },
+                "searchable": false,
+                "orderable": false
+            }
+        ],
+        rowId: 'domain_limit_id',
+    });
+}
-- 
2.30.2