diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb229b1ed7a1747e31374e16a0fa400b86259c9..6fb68fdc81cf6f6a86971be70c321eaea3750f1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features, dependecies etc.) * `form_panel` module for conveniently creating a panel for web forms. +* `restore_old_version` function to base functionality (caosdb.js) +* buttons to the version history modal that allow restoring older versions ### Changed (for changes in existing functionality) diff --git a/misc/entity_state_test_data.py b/misc/entity_state_test_data.py index 72ffe278d0ad90cb5223966bcd52c79cd420f160..400b73749011834fb5390be2e00eebdde1a91062 100755 --- a/misc/entity_state_test_data.py +++ b/misc/entity_state_test_data.py @@ -153,9 +153,8 @@ def setup_state_model(): "Transition").add_property("from", "under review").add_property("to", "unpublished").insert() # 1->1 - db.Record("Edit").add_parent( - "Transition", - description="Edit this entity. The changes are not publicly available until this entity will have been reviewed and published.").add_property( + db.Record("Edit", description="Edit this entity. The changes are not publicly available until this entity will have been reviewed and published.").add_parent( + "Transition").add_property( "from", "unpublished").add_property( "to", diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index 90d6bfd23b989bbff133a63d65e1e68d99f4b3bb..7c3c4300f971eae16d30748e6787843334708baa 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -1071,7 +1071,7 @@ function createFileXML(name, id, parents, * Update, Response, Delete. * * @param {string} root - The name of the newly created document root node. - * @param {(Document|XMLDocumentFragment)} xmls The xml documents. + * @param {Document[]|XMLDocumentFragment[]} xmls The xml documents. * @return {Document} A new xml document. */ function wrapXML(root, xmls) { @@ -1164,6 +1164,36 @@ async function update(xml) { return await transaction.updateEntitiesXml(wrapped); } + +/** + * Restore an old version of an entity using an xml representation. + * First, the old version is retrieved and the current version is set to the + * old one. + * @param versionid The version id (e.g. 123@abbabbaeff23322) of the version of + * the entity which shall be restored. + */ +async function restore_old_version(versionid){ + // retrieve entity + var ent = await transaction.retrieveEntityById(versionid); + if (ent === undefined){ + throw new Error(`Entity with version id ${versionid} could not be retrieved.`); + } + // remove unwanted tags (Version and Permissions) + ent.getElementsByTagName("Version")[0].remove(); + var permissions = ent.getElementsByTagName("Permissions"); + for (let i = permissions.length-1; i >=0 ; i--) { + permissions[i].remove(); + } + + // use XML to update entity/restore old version + const doc = _createDocument("Request"); + doc.firstElementChild.appendChild(ent); + reps = await transaction.updateEntitiesXml(doc); + if (reps.getElementsByTagName("Error").length>0) { + throw new Error(`Could not restore the Entity to the version ${versionid}.`); + } +} + /** * Insert an entity in xml representation. * diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index efa28c9b39921df3e02a25ec95d4601f752fa288..03cc7bfbb399e898a88cb99dc0a05cc90289f96a 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -359,6 +359,8 @@ this.caosdb_utils = new function () { * connection module contains all ajax calls. */ this.connection = new function () { + const logger = log.getLogger("connection"); + this._init = function () { /** * Send a get request. @@ -376,7 +378,7 @@ this.connection = new function () { if (error.status == 414) { throw new Error("UriTooLongException for GET " + uri); } else if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("GET " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -400,7 +402,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("PUT " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -435,7 +437,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error( "POST scripting returned with HTTP status " + error.status + @@ -461,7 +463,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("POST " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -486,7 +488,7 @@ this.connection = new function () { }); } catch (error) { if (error.status == 0) { - console.log(error); + logger.error(error); } else if (error.status != null) { throw new Error("DELETE " + "Entity/" + idline + " returned with HTTP status " + error.status + " - " + error.statusText); } else { @@ -985,6 +987,9 @@ this.transaction = new function () { */ var version_history = new function () { + const logger = log.getLogger("version_history"); + this.logger = logger; + this._get = connection.get; /** * Retrieve the version history of an entity and return a table with the @@ -1035,6 +1040,7 @@ var version_history = new function () { .retrieve_history(entity_id_version); sparse.replaceWith(history_table); version_history.init_export_history_buttons(entity); + version_history.init_restore_version_buttons(entity); }); } } @@ -1072,7 +1078,7 @@ var version_history = new function () { for (let version_info of $(entity) .find(".caosdb-f-entity-version-info")) { $(version_info).find(".caosdb-f-entity-version-export-history-btn") - .click(async () => { + .click(() => { const html_table = $(version_info).find("table")[0]; const history_tsv = this.get_history_tsv(html_table); version_history._download_tsv(history_tsv); @@ -1080,6 +1086,72 @@ var version_history = new function () { } } + /** + * Initialize the restore old version buttons of `entity`. + * + * The buttons are only visible when the user is allowed to update the + * entity. + * + * The causes a retrieve of the specified version of the entity and then an + * update that restores that version. + * + * @param {HTMLElement} [entity] - if undefined, the export buttons of all + * page entities are initialized. + */ + this.init_restore_version_buttons = function (entity) { + var entities = [entity] || $(".caosdb-entity-panel"); + + for (let _entity of entities) { + // initialize buttons only if the user is allowed to update the entity + if (hasEntityPermission(_entity, "UPDATE:*") || hasEntityPermission(_entity, "UPDATE:DESCRIPTION")) { + for (let version_info of + $(_entity).find(".caosdb-f-entity-version-info")) { + // find the restore button + $(version_info).find(".caosdb-f-entity-version-restore-btn") + .toggleClass("d-none", false) // show button + .click(async (eve) => { + // the version id is stored in the restore button's + // data-version-id attribute + const versionid = eve.delegateTarget.getAttribute("data-version-id") + const reload = () => { + window.location.reload(); + } + const _alert = form_elements.make_alert({ + title: "Warning", + message: "You are going to restore this version of the entity.", + proceed_callback: async () => { + try { + await restore_old_version(versionid); + $(_alert).remove(); + // reload after sucessful update + $(version_info).find(".modal-body").prepend( + $(`<div class="alert alert-success" role="alert">Restore successful! <p>You are being forwarded to the latest version of this entity or you can click <a href="#" onclick="window.location.reload()">here</a>.</p></div>`)); + setTimeout(reload, 5000); + } catch (e) { + logger.error(e); + // print errors in an alert div + $(version_info).find(".modal-body").prepend( + $(`<div class="alert alert-danger alert-dismissible " role="alert"> <button class="btn-close" data-bs-dismiss="alert" aria-label="close"></button> Restore failed! <p>${e.message}</p></div>`)); + + } + }, + cancel_callback: () => { + // do nothing + $(_alert).remove(); + $(version_info).find("table").show(); + }, + proceed_text: "Yes, restore!", + remember_my_decision_id: "restore_entity", + }); + + $(version_info).find("table").after(_alert).hide(); + $(_alert).addClass("text-end"); + }); + } + } + } + } + this._download_tsv = function (tsv_link) { window.location.href = tsv_link; } @@ -1088,6 +1160,7 @@ var version_history = new function () { this.init = function () { this.init_load_history_buttons(); this.init_export_history_buttons(); + this.init_restore_version_buttons(); } } diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 92f08ea70645a8282f97f364c9fc4143f37afd6a..d8ba00a9810e9861bfba741055ea113c7f2b88bc 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -644,11 +644,14 @@ <div class="modal-body"> <table class="table table-hover"> <thead> - <tr><th><div class="export-data">Entity ID</div></th> + <tr> + <th></th> + <th class="invisible"><div class="export-data">Entity ID</div></th> <th class="export-data">Version ID</th> <th class="export-data">Date</th> <th class="export-data">User</th> <th class="invisible"><div class="export-data">URI</div></th> + <th></th> </tr></thead> <tbody> <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> @@ -664,6 +667,13 @@ <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> </td> <td class="invisible"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td> + <td> + <xsl:if test="not(@head='true')"> + <button type="button" class="caosdb-f-entity-version-restore-btn btn btn-secondary d-none" title="Restore this version of the entity."> + <xsl:attribute name="data-version-id"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> + <i class="bi-arrow-counterclockwise"></i></button> + </xsl:if> + </td> </tr> <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> <xsl:with-param name="entityId" select="$entityId"/> @@ -672,7 +682,7 @@ </table> </div> <div class="modal-footer"> - <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-secondary">Export history</button> + <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-secondary" title="Export this history table as a CSV file.">Export history</button> </div> </xsl:template> @@ -745,6 +755,14 @@ <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> </td> <td class="invisible"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td> + <td> + <!-- include button if it is not head, i.e. Predecessors are always old and Successors if they do have a Successor Member --> + <xsl:if test="(name()='Predecessor' or Successor)"> + <button type="button" class="caosdb-f-entity-version-restore-btn btn btn-secondary d-none" title="Restore this version of the entity."> + <xsl:attribute name="data-version-id"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> + <i class="bi-arrow-counterclockwise"></i></button> + </xsl:if> + </td> </tr> </xsl:template> diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index d2ef27952e41142a62eb70e144571bc9d30c52d2..5f45c32adb9d171c5d362467d0bba0840f02836f 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -1827,6 +1827,7 @@ QUnit.test("available", function (assert) { assert.equal(typeof version_history.init, "function"); assert.equal(typeof version_history.get_history_tsv, "function"); assert.equal(typeof version_history.init_export_history_buttons, "function"); + assert.equal(typeof version_history.init_restore_version_buttons, "function"); assert.equal(typeof version_history.init_load_history_buttons, "function"); assert.equal(typeof version_history.retrieve_history, "function"); }) @@ -1887,6 +1888,76 @@ QUnit.test("init_load_history_buttons and init_load_history_buttons", async func $(html).remove(); }); +QUnit.test("available", function (assert) { + assert.equal(typeof restore_old_version, "function"); +}) + +QUnit.test("init_restore_version_buttons", async function (assert) { + var xml_str = `<Response username="user1" realm="Realm1" srid="bc2f8f6b-71d6-49ca-890c-eebea3e38e18" timestamp="1606253365632" baseuri="https://localhost:10443" count="1"> + <UserInfo username="user1" realm="Realm1"> + <Roles> + <Role>role1</Role> + </Roles> + </UserInfo> + <Record id="8610" name="TestRecord1-6thVersion" description="This is the 6th version."> + <Permissions> + <Permission name="RETRIEVE:HISTORY" /> + <Permission name="UPDATE:*" /> + </Permissions> + <Version id="efa5ac7126c722b3f43284e150d070d6deac0ba6" > + <Predecessor id="f09114b227d88f23d4e23645ae471d688b1e82f7" /> + <Successor id="5759d2bccec3662424db5bb005acea4456a299ef" /> + </Version> + <Parent id="8609" name="TestRT" /> + </Record> +</Response> +`; + var done = assert.async(1); + var xml = str2xml(xml_str); + version_history._get = async function (entity) { + assert.equal(entity, "Entity/8610@efa5ac7126c722b3f43284e150d070d6deac0ba6?H"); + done(); + $(xml).find("Version").attr("completeHistory", "true"); + return xml; + } + var html = await transformation.transformEntities(xml); + var load_button = $(html).find(".caosdb-f-entity-version-load-history-btn"); + $("body").append(html); + + assert.notOk(load_button.is(":visible"), "load_button hidden"); + load_button.click(); // nothing happens + + version_history.init_load_history_buttons(); + assert.ok(load_button.is(":visible"), "load_button is not hidden anymore"); + + //console.log(xml2str(restore_button[0])); + //assert.ok(restore_button.hasClass("d-none"), "restore_button is hidden"); + + + // load_button triggers retrieval of history + load_button.click(); + await sleep(500); + + //console.log(xml2str(restore_button[0])); + //version_history.init_restore_version_buttons(); + + var restore_button = $("body").find(".caosdb-f-entity-version-restore-btn"); + assert.ok(!restore_button.hasClass("d-none"), "restore_button is not hidden anymore"); + + // restore_button triggers retrieval of history + localStorage["form_elements.alert_decision.restore_entity"] = "proceed"; + restore_button.first().click(); + localStorage.removeItem("form_elements.alert_decision.restore_entity"); + await sleep(500); + + // restore is not possible in the unit test + alertdiv = $(html).find(".alert-danger"); + assert.equal(alertdiv.length, 1, "on alert div"); + assert.ok(alertdiv.text().indexOf("Restore failed") > 0, "Restore failed"); + + $(html).remove(); +}); + /* SETUP tests for user_management */ QUnit.module("webcaosdb.js - user_management", {