-
Henrik tom Wörden authoredHenrik tom Wörden authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
box_loan.js 29.10 KiB
/*
* This file is a part of the LinkAhead Project.
*
* Copyright (C) 2020-2024 Henrik tom Wörden (h.tomwoerden@indiscale.com)
* Copyright (C) 2020-2024 IndiScale GmbH (info@indiscale.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
/**
* Return the formatted date of today.
*/
function get_formatted_date(split) {
split = split || ".";
var date = new Date();
var tzoffset = (new Date()).getTimezoneOffset() * 60000;
return ((new Date(Date.now() - tzoffset)).toISOString()).split(split)[0];
}
/**
* This function retrieves an html form from the html subdirectory.
*
*/
var getHTMLForm = async function(pageName, variables) {
var site = await connection.get("webinterface/html/forms/" + pageName + ".html", "html");
var htmltext = site.getElementById("caosdb-form").innerHTML;
var req = /\{([0-9a-z-A-Z_]+)\}/g;
return htmltext.replace(req, (match, p1) => {
return variables[p1] || "";
});
}
/**
* Box Loan, refactored code.
*/
var box_loan = function(logger, box_loan_config) {
const datamodel = box_loan_config.datamodel;
const _dismiss_button = '<button class="btn btn-secondary box-loan-btn">OK</button>'
const _server_did_not_respond = "The server did not respond. Please reload the page.";
const _return_box_button = `<a title="Return ${datamodel.box}." class="btn btn-link box-loan-btn">Return ${datamodel.box}</a>`;
const _borrow_box_button = `<a title="Borrow ${datamodel.box}." class="btn btn-link box-loan-btn">Borrow ${datamodel.box}</a>`;
const _confirm_loan_button = '<a title="Confirm Loan." class="btn btn-link box-loan-btn">Confirm Loan</a>';
const _manual_return_button = '<a title="Manual Return." class="btn btn-link box-loan-btn">Manual Return</a>';
const _accept_return_button = '<a title="Accept Return" class="btn btn-link box-loan-btn">Accept Return</a>';
const _reject_return_button = '<a title="Reject Return." class="btn btn-link box-loan-btn">Reject Return</a>';
const _accept_loan_button = '<a title="Accept Borrow Request." class="btn btn-link box-loan-btn">Accept Loan Request</a>';
const _box_update_alert = `
<div class="alert alert-danger alert-dismissible" role="alert">
<h4>Update the Box</h4>
<p>
Are you sure that the box record is up-to-date (references etc.)?<br>Possibly have a look at the notes in the loan by clicking on the "References" button.<br> Do you you want to apply further changes?
</p><p>
<button type="button" class="btn btn-danger caosdb-f-manual-return-cancel">Cancel and change the box manually</button>
<button type="button" class="btn btn-secondary caosdb-f-manual-return-proceed">The box is up-to-date.</button>
</p>
</div>
`;
/**
* Return a HTML span which tells the user to wait for something.
*
* The optional text parameter can be used to set the text that is shown.
*
* @param text
* @return HTMLElement
*/
var getPleaseWaitSpan = function(text = "Processing. Please wait...") {
return $('<span>' + text + '</span>')[0];
}
/**
* Return the actions panel of a box entity.
*/
var get_actions_panel = function(box) {
return $(box).find('.caosdb-entity-actions-panel');
}
/**
* Open a mail with recepient address address, subject subject and body text.
*/
this.open_mail_generic = function(address, subject, text) {
const mailto = 'mailto:' + address + '?subject=' + subject + '&body=' + text;
location.href = mailto;
}
/**
* This function checks whether a property with name name
* is present in the property list p.
* This function is used for determining the loan state.
*/
var loan_state_function = function(p, name) {
var p1 = p.filter(x => x.name == name);
if (p1.length == 1 && p1[0].value.length > 0) {
return true;
}
return false;
}
/**
* This function returns a string on the state of the loan.
*/
var get_loan_state_string = function(loan) {
const p = getProperties(loan);
if (loan_state_function(p, datamodel.returned)) {
return "returned";
}
if (loan_state_function(p, datamodel.returnAccepted)) {
return "return_accepted";
}
if (loan_state_function(p, datamodel.returnRequested)) {
return "return_requested";
}
if (loan_state_function(p, datamodel.lent)) {
return "lent";
}
if (loan_state_function(p, datamodel.loanAccepted)) {
return "loan_accepted";
}
if (loan_state_function(p, datamodel.loanRequested)) {
return "loan_requested";
}
const loan_id = getEntityID(loan);
throw new Error(`Unknown loan state for Loan ${loan_id}.`);
}
/**
* Query for a Loan entity which references the box and which has no
* `returned` property.
*/
var get_active_loans = async function(boxid) {
return await query(`FIND ${datamodel.loan} WITH ${datamodel.box} -> ${boxid} AND WHICH DOES NOT HAVE A ${datamodel.returned}`);
}
/**
* Returns detailed information about the loan state of this box:
*/
var get_loan_state = async function(box) {
const loan_state = {
loan: undefined,
state: undefined,
};
const loan = (await get_active_loans(box.id))[0];
if (typeof loan === "undefined") {
// no loan found
return loan_state;
}
loan_state.loan = loan
loan_state.state = get_loan_state_string(loan);
return loan_state;
}
var run_script = async function(script, data) {
const json_str = JSON.stringify(data);
const params = {
"-p0": {
"filename": "form.json",
"blob": new Blob([json_str], {
type: "application/json"
})
}
};
try {
const response = await connection.runScript(script, params);
return form_elements.parse_script_result(response);
} catch (error) {
return error;
}
}
var show_result = function(container, result, box) {
const dismissable = $('<div/>');
const restore = $(container).children();
restore.hide();
$(container).append(dismissable);
if (typeof result === "undefined") {
$(dismissable).append(_server_did_not_respond);
}
if (result["code"] == 0) {
// success!
$(dismissable).append(result["stdout"]);
} else if (result["code"] > 0) {
// :(
$(dismissable)
.append(result["stderr"])
.append(result["stdout"]);
} else {
$(dismissable).append(result);
}
const dismiss_btn = $(_dismiss_button);
dismiss_btn.click(function() {
dismissable.remove();
restore.show();
init(box);
});
dismissable.append(dismiss_btn);
}
var get_request_data = function(form) {
const email = $(form).find("#email").val();
const first_name = $(form).find("#first-name").val();
const last_name = $(form).find("#last-name").val();
const comment = $(form).find("#loan-comment").val();
const expected_return_date = $(form).find("#expected-return").val();
const current_location = $(form).find("[name='location']").val();
const destination = $(form).find("[name='destination']").val();
const exhaust_contents = $(form).find("#exhaust-contents").prop("checked");
window.localStorage["borrower_email"] = email;
window.localStorage["borrower_first_name"] = first_name;
window.localStorage["borrower_last_name"] = last_name;
window.localStorage["borrower_return_date"] = expected_return_date
if (destination) {
window.localStorage["borrower_destination"] = destination;
}
return {
email: email,
first_name: first_name,
last_name: last_name,
comment: comment,
expected_return_date: expected_return_date,
current_location: current_location,
exhaust_contents: exhaust_contents,
destination: destination
};
}
/**
* Generate a borrow function for a box.
* This function can also be used to assign values to multiple boxes.
*/
var borrow_function = async function(form, box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
$(form).hide()
const loan_request = get_request_data(form);
$.extend(loan_request, {
box: getEntityID(box),
});
const result = await run_script("loan_management/request_loan.py", loan_request);
$(wait).remove();
$(form).remove();
show_result(actions_panel, result, box);
}
var _add_form = async function(box, form_generator, submit_callback) {
const form = $(await form_generator(getEntityID(box)));
if (typeof form === "undefined") {
return;
}
form.insertAfter($(box).find('.caosdb-entity-actions-panel')).hide();
form.submit(() => {
submit_callback(form[0], box);
return false;
});
form.find("label").has(":required").prepend('<span style="font-size: 10px; color: red; margin-right: 4px; font-weight: 100;">*</span>');
// the footer adds the default styling as well as the *required-marker
const submit = form.find(":input[type='submit']")[0];
const footer = form_elements.make_footer();
footer.append(submit);
form.find("form").append(footer);
return form[0];
}
var _init_validator = function(form) {
// initiate validator
// Workaround for missing checkValidity function:
if (form.checkValidity == undefined) {
form.checkValidity = () => true;
}
if (form.reportValidity == undefined) {
form.reportValidity = () => logger.debug("Workaround for validity report");
}
form_elements.init_validator(form);
const submit = $(form).find(":input[type='submit']")[0];
form_elements.toggle_submit_button_form_valid(form, submit);
}
/**
* Add Return button to boxes' actions panels.
*/
var add_return_button = async function(box) {
const but = $(_return_box_button);
const form = await _add_form(box, generate_return_form, return_function);
if (typeof form === "undefined") {
return;
}
get_actions_panel(box).append(but);
const config = {
type: "reference_drop_down",
name: "location",
label: "",
required: true,
query: `FIND Record ${datamodel.location}`,
make_desc: getEntityName,
}
const dd = form_elements.make_form_field(config);
$(dd).find(".col-sm-9").removeClass("col-sm-9");
$(dd).find(".col-sm-3").removeClass("col-sm-3");
$(form).find("#current-location").replaceWith(dd);
$(form).find("label").css("display", "block");
_init_validator(form);
but.click(() => {
but.remove();
$(form).toggle()
});
}
/**
* If the user is administrator (or a data curator) a button is added
* to accept the borrow request. Else a message is added "pending loan request".
*/
var add_borrow_accept_button = function(box, loan) {
const actionPanel = get_actions_panel(box);
if (userIsAdministrator() === true || userHasRole("curator") === true) {
const but = $(_accept_loan_button);
but.click(() => {
but.remove();
accept_loan_function(box);
});
actionPanel.append(but);
} else {
actionPanel.append($(create_link_to_loan(loan, "Pending loan request.")));
}
}
var create_link_to_loan = function(loan, text) {
const loan_id = getEntityID(loan);
const href = `${connection.getBasePath()}Entity/${loan_id}`;
const title = "Open loan entity.";
const classes = "btn btn-link box-loan-btn";
return `<a title="${title}" class="${classes}"
href="${href}">${text}<a>`;
}
/**
* Add Borrow buttons to the boxes' action panels
*/
var add_borrow_button = async function(box) {
const but = $(_borrow_box_button);
get_actions_panel(box).append(but);
const form = await _add_form(box, generate_form_borrow_checkout, borrow_function);
replace_loan_destination(form);
$(form).find("label").css("display", "block");
_init_validator(form);
but.click(() => {
but.remove();
$(form).toggle()
});
}
var open_accept_loan_mail = async function(loan, box) {
try {
const borrower = (await retrieve(getProperty(loan, "Borrower"), case_sensitive = false))[0];
const email = getProperty(borrower, datamodel.email, case_sensitive = false);
const firstName = getProperty(borrower, datamodel.firstName, case_sensitive = false);
const lastName = getProperty(borrower, datamodel.lastName, case_sensitive = false);
const bn = getProperty(box, datamodel.number, case_sensitive = false);
const date = getProperty(loan, datamodel.expectedReturn, case_sensitive = false);
const subject = `Loan Request for ${datamodel.box} ${bn} accepted`;
const body = `Dear ${firstName} ${lastName}, %0D%0D` +
`You can pickup ${datamodel.box} number ${bn} on ${date}. %0D%0D` +
`Kind Regards,%0D%0D ${getUserName()}`;
open_mail_generic(email, subject, body)
} catch (err) {
logger.error(err);
}
}
var accept_loan_function = async function(box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
const loan = (await get_active_loans(box.id))[0];
const accept_loan_request = {
loan: getEntityID(loan),
};
const result = await run_script("loan_management/accept_loan_request.py", accept_loan_request);
$(wait).remove();
show_result(actions_panel, result, box);
open_accept_loan_mail(loan, box);
}
var manual_return_function = async function(box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
const loan = (await get_active_loans(box.id))[0];
const manual_return_request = {
loan: getEntityID(loan),
};
const result = await run_script("loan_management/manual_return.py", manual_return_request);
$(wait).remove();
show_result(actions_panel, result, box);
}
/**
* Add Manual Return button to boxes' actions panels.
*/
var add_manual_return_button = function(box, loan) {
const actionPanel = get_actions_panel(box);
if (userIsAdministrator() === true || userHasRole("curator") === true) {
const but = $(_manual_return_button);
actionPanel.append(but);
const box_update_alert = $(_box_update_alert);
box_update_alert.find(".caosdb-f-manual-return-cancel").click(() => {
but.show();
box_update_alert.hide();
});
box_update_alert.find(".caosdb-f-manual-return-proceed").click(() => {
but.remove();
box_update_alert.remove();
manual_return_function(box);
});
actionPanel.append(box_update_alert);
box_update_alert.hide();
but.click(() => {
but.hide();
box_update_alert.show();
});
} else {
actionPanel.append($(create_link_to_loan(loan, "Pending return.")));
}
}
var confirm_loan_function = async function(box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
const loan = (await get_active_loans(box.id))[0];
const confirm_loan_request = {
loan: getEntityID(loan),
};
const result = await run_script("loan_management/confirm_loan.py", confirm_loan_request);
$(wait).remove();
show_result(actions_panel, result, box);
}
var open_reject_return_mail = async function(loan, box) {
try {
const borrower = (await retrieve(getProperty(loan, "Borrower"), case_sensitive = false))[0];
const email = getProperty(borrower, datamodel.email, case_sensitive = false);
const firstName = getProperty(borrower, datamodel.firstName, case_sensitive = false);
const lastName = getProperty(borrower, datamodel.lastName, case_sensitive = false);
const bn = getProperty(box, datamodel.number, case_sensitive = false);
const subject = `Return Request for ${datamodel.box} ${bn} rejected`;
const body = `Dear ${firstName} ${lastName}, %0D%0D` +
`your return request for ${datamodel.box} number ${bn} cannot be accepted.` +
`Kind Regards,%0D ${getUserName()}`;
open_mail_generic(email, subject, body)
} catch (err) {
logger.error(err);
}
}
var open_accept_return_mail = async function(loan, box) {
try {
const borrower = (await retrieve(getProperty(loan, "Borrower"), case_sensitive = false))[0];
const email = getProperty(borrower, datamodel.email, case_sensitive = false);
const firstName = getProperty(borrower, datamodel.firstName, case_sensitive = false);
const lastName = getProperty(borrower, datamodel.lastName, case_sensitive = false);
const bn = getProperty(box, datamodel.number, case_sensitive = false);
const date = getProperty(loan, datamodel.expectedReturn, case_sensitive = false);
const subject = `Return Request for ${datamodel.box} ${bn} accepted`;
const body = `Dear ${firstName} ${lastName}, %0D%0D` +
`please return ${datamodel.box} number ${bn} on ${date}. %0D%0D` +
`Kind Regards,%0D ${getUserName()}`;
open_mail_generic(email, subject, body)
} catch (err) {
logger.error(err);
}
}
var accept_return_function = async function(box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
const loan = (await get_active_loans(box.id))[0];
const accept_return_request = {
loan: getEntityID(loan),
};
const result = await run_script("loan_management/accept_return_request.py", accept_return_request);
$(wait).remove();
show_result(actions_panel, result, box);
open_accept_return_mail(loan, box);
}
var reject_return_function = async function(box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
const loan = (await get_active_loans(box.id))[0];
const reject_return_request = {
loan: getEntityID(loan),
};
const result = await run_script(
"loan_management/reject_return_request.py",
reject_return_request);
$(wait).remove();
show_result(actions_panel, result, box);
open_reject_return_mail(loan, box);
}
/**
* Add two buttons for confirming or rejecting a return request.
*/
var add_confirm_reject_return = function(box, loan) {
const actionPanel = get_actions_panel(box);
if (userIsAdministrator() === true || userHasRole("curator") === true) {
const confirmButton = $(_accept_return_button);
actionPanel.append(confirmButton);
const rejectButton = $(_reject_return_button);
actionPanel.append(rejectButton);
confirmButton.click(() => {
confirmButton.remove();
rejectButton.remove();
accept_return_function(box);
});
rejectButton.click(() => {
confirmButton.remove();
rejectButton.remove();
reject_return_function(box);
});
} else {
actionPanel.append($(create_link_to_loan(loan, "Pending return request.")));
}
}
/**
* Add a button for confirming the loan.
*/
var add_confirm_loan_button = function(box, loan) {
const actionsPanel = get_actions_panel(box);
if (userIsAdministrator() === true || userHasRole("curator") === true) {
const but = $(_confirm_loan_button);
actionsPanel.append(but);
but.click(() => {
but.remove();
confirm_loan_function(box);
});
} else {
actionsPanel.append($(create_link_to_loan(loan, "Loan request accepted.")));
}
}
var return_function = async function(form, box) {
const wait = getPleaseWaitSpan();
const actions_panel = get_actions_panel(box);
$(actions_panel).append(wait).find('btn').hide()
$(form).hide();
const return_request = get_request_data(form);
const loan = (await get_active_loans(box.id))[0];
$.extend(return_request, {
box: getEntityID(box),
loan: getEntityID(loan),
});
const result = await run_script("loan_management/request_return.py", return_request);
$(wait).remove();
$(form).remove();
show_result(actions_panel, result, box);
}
var replace_loan_destination = async function(node) {
const config = {
type: "reference_drop_down",
name: "destination",
label: "",
required: true,
query: `FIND Record ${datamodel.location}`,
make_desc: getEntityName,
}
const dd = form_elements.make_form_field(config);
$(dd).find(".col-sm-9").removeClass("col-sm-9");
$(dd).find(".col-sm-3").removeClass("col-sm-3");
$(node).find("#loan-destination").replaceWith(dd);
}
var add_menu_entry = async function () {
if ($("#caosdb-f-bookmarks-borrow-all").length > 0) {
return;
}
var modal = $(await generate_form_borrow_checkout(undefined, "loan-forms/borrow_all_bookmarked")).find(".modal");
replace_loan_destination(modal);
$(modal).find("label").css("display", "block");
$('body').append(modal);
modal.on('show.bs.modal', function() {
modal.find(".modal-footer").show();
});
modal.find('.btn-primary').on('click', async function() {
var loan_request = get_request_data(modal.find("form")[0]);
// TODO this is a dependency that cannot be expressed by adding the module to the call of this module
// because the ext_bookmarks module is initialized too late
loan_request["box"] = ext_bookmarks.get_bookmarks();
const result = await run_script("loan_management/request_loan.py", loan_request);
show_result(modal.find(".modal-body"), result, {});
modal.find(".modal-footer").hide();
modal.find(".box-loan-btn").attr("data-bs-dismiss", "modal")
});
_init_validator(modal[0]);
const borrow_btn = $('<li class="disabled" id="caosdb-f-bookmarks-borrow-all" data-bs-toggle="modal" data-bs-target="#caosdb-f-borrow-all-form-modal" title="Borrow all bookmarked items"> <a class="dropdown-item">Borrow all</a></li>');
$("#caosdb-f-bookmarks-clear").parent().append(borrow_btn);
}
/**
* Add buttons for borrowing boxes.
*/
var add_buttons = function(boxes) {
$(boxes).find('.box-loan-btn').remove();
$(boxes).each(async function() {
const loan_state = await get_loan_state(this);
if (typeof loan_state.loan !== "undefined") {
if (loan_state.state == "loan_requested") {
add_borrow_accept_button(this, loan_state.loan);
} else if (loan_state.state == "loan_accepted") {
add_confirm_loan_button(this, loan_state.loan);
} else if (loan_state.state == "lent") {
add_return_button(this);
} else if (loan_state.state == "return_requested") {
add_confirm_reject_return(this, loan_state.loan);
} else if (loan_state.state == "return_accepted") {
add_manual_return_button(this, loan_state.loan);
}
} else {
add_borrow_button(this);
}
});
}
/**
* This function generates a form for borrowing boxes.
* selectform can be used to generate the form for multiple boxes.
* In case selectform is true, box will be ignored.
*/
var generate_form_borrow_checkout = async function(boxid, formhtml) {
const email = window.localStorage["borrower_email"];
const firstname = window.localStorage["borrower_first_name"];
const lastname = window.localStorage["borrower_last_name"];
const destination = window.localStorage["borrower_destination"];
var exp_return = window.localStorage["borrower_return_date"];
if (exp_return < Date.now()) {
exp_return = "";
}
formhtml = formhtml || 'loan-forms/borrow_checkout';
return getHTMLForm(formhtml, {
boxid: boxid,
first_name: firstname,
last_name: lastname,
email: email,
destination: destination,
expected_return_date: exp_return,
mindate: get_formatted_date("T"),
lentType: datamodel.box
});
}
/**
* Generate form for a new Return Request, with fields
*
* boxid (hidden)
* first_name (pre-filled)
* last_name (pre-filled)
* email (pre-filled)
* expectedreturn (pre-filled)
* currentlocation (pre-filled)
* comment
*/
var generate_return_form = async function(boxid) {
const loan = (await get_active_loans(boxid))[0];
if (typeof loan === "undefined") {
// no loan found
logger.error("No loan found for box", boxid);
return ;
}
const borrower = (await retrieve(getProperty(loan, "Borrower", case_sensitive = false)))[0];
if (typeof borrower === "undefined") {
// no loan found
logger.error("No borrower found for loan", loan);
return ;
}
var email = getProperty(borrower, datamodel.email, case_sensitive = false);
var first_name = getProperty(borrower, datamodel.firstName,
case_sensitive = false);
var last_name = getProperty(borrower, datamodel.lastName,
case_sensitive = false);
var exp_return = getProperty(loan, datamodel.expectedReturn,
case_sensitive = false);
var cur_loc = getProperty(loan, "destination", case_sensitive = false);
return getHTMLForm("loan-forms/return_box", {
boxid: boxid,
first_name: first_name,
last_name: last_name,
email: email,
expectedreturn: exp_return,
currentlocation: cur_loc,
mindate: get_formatted_date("T")
});
}
var init = function(boxes) {
const init_boxes = boxes || $(".caosdb-entity-panel")
.not("[data-version-successor]")
.filter(function() {
return $(this).find(".caosdb-parent-name").text().trim() === datamodel.box;
})
.toArray();
add_menu_entry();
add_buttons(init_boxes);
}
return {
init: init,
add_buttons: add_buttons,
get_active_loans: get_active_loans,
get_loan_state_string: get_loan_state_string,
};
}(log.getLogger("box_loan"), box_loan_config);
$(document).ready(function() {
if ("${BUILD_MODULE_BOX_LOAN}" == "ENABLED") {
caosdb_modules.register(box_loan);
}
});