diff --git a/CHANGELOG.md b/CHANGELOG.md index 985d1541eaeb597156b4ddb3de377b55d71e021c..b79c4b37a6c80bc921e2957e45a9eb7f01b20ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features, dependecies etc.) +* two new field types for the form_elements module, `file` and `select`. See + the module documentation for more information. + ### Changed (for changes in existing functionality) ### Deprecated (for soon-to-be removed features) @@ -16,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed edit mode for Safari 11. + ### Security (in case of vulnerabilities) ## [0.3.0] - 2021-02-10 diff --git a/misc/yaml_to_json.py b/misc/yaml_to_json.py index a7d5bd62a7a1ccc50766b797ef6710466e9bee11..e77e5efc56b2cea39b0a7b6f90236fb5b39da24e 100755 --- a/misc/yaml_to_json.py +++ b/misc/yaml_to_json.py @@ -6,4 +6,4 @@ import json import yaml with open(sys.argv[1], 'r') as infile: - print(json.dumps(yaml.load(infile))) + print(json.dumps(yaml.safe_load(infile))) diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 9df7505f1e81fd0234aaa4461444535846921885..5da4303fb7227e12b38ca2d23628e21a4c996c63 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -411,7 +411,7 @@ var edit_mode = new function() { * entity in XML representation. */ this.form_to_xml = function(entity_form) { - const obj = form_elements.form_to_object($(entity_form).find("form")[0]); + const obj = form_elements.form_to_object($(entity_form).find("form")[0])[0]; var entityRole = getEntityRole(entity_form); var file_path = undefined; var file_checksum = undefined; diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index d4cd4234a28953140fcc1f62104e2c43a3460cdb..8fe0af4ba633aaf257c48dd3e40ee22f4a8fc3c5 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -84,14 +84,17 @@ var form_elements = new function () { /** * The configuration for double, integer, date input elements. * - * There are specializations of this configuration object. See - * {@link ReferenceDropDownConfig} + * There are several specializations of this configuration object. + * {@link ReferenceDropDownConfig}, {@link RangeFieldConfig}, {@link SelectFieldConfig}, {@link FileFieldConfig} * * @typedef {object} FieldConfig + * * @property {string} name * @property {string} type - * @property {string} label - * @see {@link ReferenceDropDownConfig} + * @property {string} [label] + * @property {string} [help] + * @property {boolean} [required=false] + * @property {boolean} [cached=false] */ this.version = "0.1"; @@ -99,7 +102,6 @@ var form_elements = new function () { this.logger = log.getLogger("form_elements"); this.cancel_form_event = new Event("caosdb.form.cancel"); this.submit_form_event = new Event("caosdb.form.submit"); - this.form_ready_event = new Event("caosdb.form.ready"); this.field_changed_event = new Event("caosdb.field.changed"); this.field_enabled_event = new Event("caosdb.field.enabled"); this.field_disabled_event = new Event("caosdb.field.disabled"); @@ -208,7 +210,7 @@ var form_elements = new function () { this.make_alert = function (config) { caosdb_utils.assert_string(config.message, "config param `message`"); caosdb_utils.assert_type(config.proceed_callback, "function", - "config param `proceed_callback`"); + "config param `proceed_callback`"); // define some defaults. const title = config.title ? `<h4>${config.title}</h4>` : ""; @@ -219,12 +221,14 @@ var form_elements = new function () { // check if alert should be created at all if (remember) { - var result = this._get_alert_decision(config.remember_my_decision_id); - if (result == "proceed") { - // call callback asyncronously and return - (async function(){ config.proceed_callback(); })(); - return undefined; - } + var result = this._get_alert_decision(config.remember_my_decision_id); + if (result == "proceed") { + // call callback asyncronously and return + (async function () { + config.proceed_callback(); + })(); + return undefined; + } } // create the alert @@ -236,8 +240,8 @@ var form_elements = new function () { // create the "Don't ask me again" checkbox var checkbox = undefined; if (remember) { - const remember_my_decision_text = config.remember_my_decision_text - || "Don't ask me again."; + const remember_my_decision_text = config.remember_my_decision_text || + "Don't ask me again."; checkbox = $(`<p class="checkbox"><label> <input type="checkbox"/> ${remember_my_decision_text}</label></p>`); _alert.append(checkbox); @@ -293,12 +297,23 @@ var form_elements = new function () { if (typeof desc == "undefined") { desc = entity_id; } - var opt_str = '<option value="' + entity_id + '">' + desc + + return form_elements._make_option(entity_id, desc); + } + + /** + * Return an `option` element for a `select`. + * + * @param {string} value - the actual value of the option element. + * @param {string} label - the string which is shown for this option in the + * drop-down menu of the select input. + * @return {HTMLElement} + */ + this._make_option = function (value, label) { + const opt_str = '<option value="' + value + '">' + label + "</option>"; return $(opt_str)[0]; } - /** * (Re-)set this module's functions to standard implementation. */ @@ -320,10 +335,11 @@ var form_elements = new function () { * parameter which is an entity in HTML representation. * @param {boolean} [multiple] - whether the select allows multiple * options to be selected. + * @param {string} name - the name of the select element * @returns {HTMLElement} SELECT element with entity options. */ this.make_reference_select = async function (entities, make_desc, - make_value, multiple) { + make_value, name, multiple) { caosdb_utils.assert_array(entities, "param `entities`", false); if (typeof make_desc !== "undefined") { caosdb_utils.assert_type(make_desc, "function", @@ -333,12 +349,7 @@ var form_elements = new function () { caosdb_utils.assert_type(make_value, "function", "param `make_value`"); } - const ret = $('<select class="selectpicker form-control" title="Nothing selected"/>'); - if (multiple) { - ret.attr("multiple", ""); - } else { - ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>'); - } + const ret = $(form_elements._make_select(multiple, name)); for (let entity of entities) { this.logger.trace("add option", entity); let entity_id = getEntityID(entity); @@ -351,6 +362,30 @@ var form_elements = new function () { return ret[0]; } + /** + * Return a new select element. + * + * This function is mainly used by other factory functions, e.g. {@link + * make_reference_select} and {@link make_select_input}. + * + * @param {boolean} multiple - the `multiple` attribute of the select element. + * @param {string} name - the name of the select element. + * @return {HTMLElement} + */ + this._make_select = function (multiple, name) { + const ret = $(`<select class="selectpicker form-control" name="${name}" title="Nothing selected"/>`); + if (typeof name !== "undefined") { + caosdb_utils.assert_string(name, "param `name`"); + ret.attr("name", name); + } + if (multiple) { + ret.attr("multiple", ""); + } else { + ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>'); + } + return ret[0]; + } + /** * Configuration object for a drop down menu for selecting references. * `make_reference_drop_down` generates such a drop down menu using a @@ -369,10 +404,9 @@ var form_elements = new function () { * defined by `label`. If the `label` property is undefined, the `name` * is shown instead. * - * The ReferenceDropDownConfig is a specialisation of a - * {@link FieldConfig}. - * * @typedef {option} ReferenceDropDownConfig + * + * @augments FieldConfig * @property {string} name - The name of the select input. * @property {string} query - Query for entities. * @property {function} [make_value] - Call-back for the generation of @@ -386,8 +420,6 @@ var form_elements = new function () { * @property {string} [type] - This should be "reference_drop_down" or * undefined. This property is used by `make_form_field` to decide * which type of field is to be generated. - * - * @see {@link FieldConfig} */ this._query = async function (q) { @@ -396,8 +428,23 @@ var form_elements = new function () { return result; } + /** + * Call a server-side script with the content of the given form and + * return the results. + * + * Note that the form should be one generated by this form_elements + * module. Otherwise it cannot be guaranteed that the form will be + * serialized (to json) correctly. + * + * @param {string} script - the path of the script + * @param {HTMLElements} form - a form generated by this module. + * @return {ScriptingResult} the results of the call. + */ this._run_script = async function (script, form) { - const json_str = JSON.stringify(form_elements.form_to_object(form[0])); + const form_objects = form_elements.form_to_object(form[0]); + const json_str = JSON.stringify(form_objects[0]); + + // append non-file form fields to the request const params = { "-p0": { "filename": "form.json", @@ -406,6 +453,16 @@ var form_elements = new function () { }) } }; + + // append files to the request + const files = form_objects[1]; + for (let i = 0; i < files.length; i++) { + params[`file_${i}`] = { + "filename": `${files[i]["fieldname"]}_${files[i]["filename"]}`, + "blob": files[i]["blob"] + }; + } + const result = await connection.runScript(script, params); this.logger.debug("server-side script returned", result); return this.parse_script_result(result); @@ -422,7 +479,8 @@ var form_elements = new function () { */ /** - * Bla, TODO + * Convert the reponse of a server-side scripting call into a {@link + * ScriptingResult} object. * * @param {XMLDocument} result * @return {ScriptingResult} @@ -456,15 +514,15 @@ var form_elements = new function () { this.make_reference_drop_down = function (config) { let ret = $(this._make_field_wrapper(config.name)); let label = this._make_input_label_str(config); - let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); + let loading = $(createWaitingNotification("loading...")) + .addClass("caosdb-f-field-not-ready"); let input_col = $('<div class="col-sm-9"/>'); input_col.append(loading); this._query(config.query).then(async function (entities) { let select = $(await form_elements.make_reference_select( - entities, config.make_desc, config.make_value, config.multiple, + entities, config.make_desc, config.make_value, config.name, config.multiple, config.value)); - select.attr("name", config.name); loading.remove(); input_col.append(select); form_elements.init_select_picker(ret[0], config.value); @@ -574,16 +632,27 @@ var form_elements = new function () { } /** - * generate a java script object representation of a form + * Generate a java script object representation of a form and extract the + * files from the form. + * + * The property names (aka keys) are the names of the form fields and + * subforms. The values are single strings or arrays of strings. If the + * field was had a file-type input, the value is a string identifying the + * file blob which belongs to this key. * - * @function + * Subforms lead to nested objects of the same structure. + * + * @param {HTMLElement} form - a form generated by this module. + * @return {object[]} - an array of length 2. The first element is an + * object representing the fields of the form. The second contains a + * list of file blobs resulting from file inputs in the form. */ this.form_to_object = function (form) { this.logger.trace("entity form_to_json", form); caosdb_utils.assert_html_element(form, "parameter `form`"); - const _to_json = (element, data) => { - this.logger.trace("enter element_to_json", element, data); + const _to_json = (element, data, files) => { + this.logger.trace("enter element_to_json", element, data, files); for (const child of element.children) { // ignore disabled fields and subforms @@ -595,7 +664,7 @@ var form_elements = new function () { if (is_subform) { const subform = $(child).data("subform-name"); // recursive - var subform_obj = _to_json(child, {}); + var subform_obj = _to_json(child, {}, files)[0]; if (typeof data[subform] === "undefined") { data[subform] = subform_obj; } else if (Array.isArray(data[subform])) { @@ -606,7 +675,30 @@ var form_elements = new function () { } else if (name && name !== "") { // input elements const not_checkbox = !$(child).is(":checkbox"); - if (not_checkbox || $(child).is(":checked")) { + const is_file = $(child).is("input:file"); + if (is_file) { + var fileList = child.files; + if (fileList.length > 0) { + for (let i = 0; i < fileList.length; i++) { + // generate an identifyer for the file(s) of this input + value = name + "_" + fileList[i].name; + if (typeof data[name] === "undefined") { + // first and possibly only value + data[name] = value + } else if (Array.isArray(data[name])) { + data[name].push(value); + } else { + // there is a value present yet - convert to array. + data[name] = [data[name], value] + } + files.push({ + "fieldname": name, + "filename": fileList[i].name, + "blob": fileList[i] + }); + } + } + } else if (not_checkbox || $(child).is(":checked")) { // checked or not a checkbox var value = $(child).val(); if (typeof data[name] === "undefined") { @@ -621,15 +713,15 @@ var form_elements = new function () { } } else if (child.children.length > 0) { // recursive - _to_json(child, data); + _to_json(child, data, files); } } - this.logger.trace("leave element_to_json", element, data); - return data; + this.logger.trace("leave element_to_json", element, data, files); + return [data, files]; }; - const ret = _to_json(form, {}); + const ret = _to_json(form, {}, []); this.logger.trace("leave form_to_json", ret); return ret; } @@ -649,11 +741,19 @@ var form_elements = new function () { } /** - * TODO make syncronous + * Return a new form field (or a subform). + * + * This function is intended to be called by make_form and recursively by + * other make_* functions which create subforms or other deeper structured + * form fields. + * + * This function also configures the caching, whether a form field is + * 'required' or not, and the help for each field. * + * @param {FieldConfig} config - the configuration of the form field * @return {HTMLElement} */ - this.make_form_field = async function (config) { + this.make_form_field = function (config) { caosdb_utils.assert_type(config, "object", "param `config`"); caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); @@ -661,6 +761,8 @@ var form_elements = new function () { const type = config.type; if (type === "date") { field = this.make_date_input(config); + } else if (type === "file") { + field = this.make_file_input(config); } else if (type === "checkbox") { field = this.make_checkbox_input(config); } else if (type === "text") { @@ -670,12 +772,13 @@ var form_elements = new function () { } else if (type === "integer") { field = this.make_integer_input(config); } else if (type === "range") { - field = await this.make_range_input(config); + field = this.make_range_input(config); } else if (type === "reference_drop_down") { field = this.make_reference_drop_down(config); + } else if (type === "select") { + field = this.make_select_input(config); } else if (type === "subform") { - // TODO handle cache and required for subforms - return await this.make_subform(config); + return this.make_subform(config); } else { throw new TypeError("undefined field type `" + type + "`"); } @@ -732,31 +835,18 @@ var form_elements = new function () { this.make_form_wrapper = function (form, config) { var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); - var header = this.make_heading(config); - wrapper.append(header); - var loading = $('<div>loading...</div>'); - var logger = this.logger; var cancel = (e) => { - logger.trace("cancel form", e); + form_elements.logger.trace("cancel form", e); wrapper.remove(); }; - wrapper.append(loading); - - Promise.resolve(form).then(form => { - // form ready - loading.remove(); - wrapper.append(form); - wrapper[0].dispatchEvent(this.form_ready_event); + wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); - }).catch(err => { - logger.error("form loading error", err); - loading.remove(); - wrapper.append(err); - }); - wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); + var header = this.make_heading(config); + wrapper.append(header); + wrapper.append(form); return wrapper[0]; } @@ -782,9 +872,9 @@ var form_elements = new function () { /** * Create a form. * - * The returned element is a container which will eventually contain a HTML - * form element. The container emits a {@link form_ready_event} when the - * form is ready. + * The returned element is a container which contains a HTML form element. + * The fields are ready or they will emit a {@link field_ready_event} when + * they are. * * @param {FormConfig} config * @return {HTMLElement} @@ -802,9 +892,20 @@ var form_elements = new function () { } /** - * TODO make syncronous + * @typedef {object} SubFormConfig + * + * @augments FieldConfig + * @property {FieldConfig[]} fields - array of fields. The order is the + * order in which they appear in the resulting subform. */ - this.make_subform = async function (config) { + + /** + * Return a new subform. + * + * @param {SubFormConfig} config - the configuration of the subform. + * @return {HTMLElement} + */ + this.make_subform = function (config) { this.logger.trace("enter make_subform"); caosdb_utils.assert_type(config, "object", "param `config`"); caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); @@ -815,7 +916,7 @@ var form_elements = new function () { for (let field of config.fields) { this.logger.trace("add subform field", field); - let elem = await this.make_form_field(field); + let elem = this.make_form_field(field); form.append(elem); } @@ -865,6 +966,7 @@ var form_elements = new function () { this.disable_fields = function (fields) { $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); + $(fields).find(":input").prop("required", false); for (const field of $(fields)) { field.dispatchEvent(this.field_disabled_event); } @@ -872,6 +974,7 @@ var form_elements = new function () { this.enable_fields = function (fields) { $(fields).toggleClass("caosdb-f-field-disabled", false).show(); + $(fields).filter(".caosdb-f-form-field-required").find("input.caosdb-f-property-single-raw-value, select.selectpicker").prop("required", true); for (const field of $(fields)) { field.dispatchEvent(this.field_enabled_event); } @@ -885,7 +988,7 @@ var form_elements = new function () { this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } - this.make_script_form = async function (config, script) { + this.make_script_form = function (config, script) { this.logger.trace("enter make_script_form"); const submit_callback = async function (form) { @@ -914,7 +1017,7 @@ var form_elements = new function () { name: script, submit: submit_callback }, config); - return await this.make_generic_form(new_config); + return this.make_generic_form(new_config); } /** @@ -924,9 +1027,9 @@ var form_elements = new function () { * The `config.fields` array may contain `form_elements.field_config` * objects or HTMLElements. * - * TODO + * @return {HTMLElement} */ - this.make_generic_form = async function (config) { + this.make_generic_form = function (config) { this.logger.trace("enter make_generic_form"); caosdb_utils.assert_type(config, "object", "param `config`"); @@ -946,7 +1049,7 @@ var form_elements = new function () { if (field instanceof HTMLElement) { form.append(field); } else { - let elem = await this.make_form_field(field); + let elem = this.make_form_field(field); form.append(elem); } } @@ -1125,11 +1228,24 @@ var form_elements = new function () { } /** - * TODO make syncronous + * @typedef {object} RangeFieldConfig + * + * @augments FieldConfig + * @property {FieldConfig} from - the start point of the range. This is + * usually an integer or double input field. + * @property {FieldConfig] to - the end point of the range. This is + * usually an integer or a double input field. */ - this.make_range_input = async function (config) { - // TODO + /** + * Return a new form field representing a range of numbers. + * + * @param {RangeFieldConfig} config + * @return {HTMLElement} + */ + this.make_range_input = function (config) { + + // TODO // 1. wrapp both inputs to separate it from the label into a container // 2. make two rows for each input // 3. make inline-block for all included elements @@ -1144,8 +1260,8 @@ var form_elements = new function () { type: "double" }, config.to); - const from_input = await this.make_form_field(from_config); - const to_input = await this.make_form_field(to_config); + const from_input = this.make_form_field(from_config); + const to_input = this.make_form_field(to_config); const ret = $(this._make_field_wrapper(config.name)); if (config.label) { @@ -1178,13 +1294,27 @@ var form_elements = new function () { this._make_field_wrapper = function (name) { caosdb_utils.assert_string(name, "param `name`"); return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />') - .css({"padding": "0"})[0]; + .css({ + "padding": "0" + })[0]; } + /** + * Return a new date field. + * + * @param {FieldConfig} config + * @return {HTMLElement} + */ this.make_date_input = function (config) { return this._make_input(config); } + /** + * Return a new text field. + * + * @param {FieldConfig} config + * @return {HTMLElement} + */ this.make_text_input = function (config) { return this._make_input(config); } @@ -1195,14 +1325,20 @@ var form_elements = new function () { * * `config.type` is set to "number" and overrides any other type. * - * @param {form_elements.input_config} config. + * @param {FieldConfig} config. * @returns {HTMLElement} a double form field. */ this.make_double_input = function (config) { - var clone = $.extend({}, config, { + const _config = $.extend({}, config, { type: "number" }); - var ret = $(this._make_input(clone)) + const ret = $(this._make_input(_config)) + if (typeof config.min !== "undefined") { + ret.find("input").attr("min", config.min); + } + if (typeof config.max !== "undefined") { + ret.find("input").attr("max", config.max); + } ret.find("input").attr("step", "any"); return ret[0]; } @@ -1213,7 +1349,7 @@ var form_elements = new function () { * * `config.type` is set to "number" and overrides any other type. * - * @param {form_elements.input_config} config. + * @param {FieldConfig} config. * @returns {HTMLElement} an integer form field. */ this.make_integer_input = function (config) { @@ -1222,6 +1358,79 @@ var form_elements = new function () { return ret[0]; } + /** + * @typedef {object} FileFieldConfig + * + * @augments FieldConfig + * @property {boolean} [multiple=false] - whether to accept multiple files. + * @property {string} [accept] - a comma separated list of file extensions + * which are accepted (exclusively). + */ + + /** + * Return a new form field for a file upload. + * + * @param {FileFieldConfig} config - configuration for this form field. + * @return {HTMLElement} + */ + this.make_file_input = function (config) { + const ret = this._make_input(config); + $(ret) + .find(":input") + .prop("multiple", !!config.multiple) + .css({ + "display": "block" + }); + if (config.accept) { + $(ret) + .find(":input") + .attr("accept", config.accept); + } + + return ret; + } + + /** + * @typedef {object} SelectOptionConfig + * + * @property {string} value - the value of the select option. + * @property {string} [label] - a visible representation (think: + * description) of the value of the select option. defaults to the + * value itself. + */ + + /** + * @typedef {object} SelectFieldConfig + * + * @augments {FieldConfig} + * @property {SelectOptionConfig} - options + */ + + /** + * Return a select field. + * + * @param {SelectFieldConfig} config + * @returns {HTMLElement} a select field. + */ + this.make_select_input = function (config) { + const options = config.options; + const select = $(form_elements._make_select(config.multiple, config.name)); + + for (let option of options) { + select.append(form_elements._make_option(option.value, option.label)); + } + const ret = form_elements._make_input(config, select[0]); + // Here, the bootstrap-select features should be activated for the new + // select element. However, up until now, this only works when the + // select element is already a part of the dom tree - which is not the + // case when this method is called and is controlled by the client. So + // there is currently no other work-around than to call + // init_select_picker after the form creation explicitely :( + //form_elements.init_select_picker(select[0], config.value); + + return ret; + } + /** * Return a checkbox input field. @@ -1340,25 +1549,29 @@ var form_elements = new function () { * * @param {object} config - config object with `name`, `type` and * optional `label` + * @param {string} input - optional specification of the HTML input element. + * `<input class="form-control caosdb-f-property-single-raw-value" type="' + type + '" name="' + name + '" />` + * is used as default where `name` and `type` stem from the config + * object. * @returns {HTMLElement} a form field. */ - this._make_input = function (config) { + this._make_input = function (config, input) { caosdb_utils.assert_string(config.name, "the name of a form field"); let ret = $(this._make_field_wrapper(config.name)); let name = config.name; let label = this._make_input_label_str(config); let type = config.type || "text"; let value = config.value; - let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + - '" name="' + name + - '" />'); - input.change(function () { + const _input = $(input || + '<input class="form-control caosdb-f-property-single-raw-value" type="' + + type + '" name="' + name + '" />'); + _input.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); }); let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); - input_col.append(input); + input_col.append(_input); if (value) { - input.val(value); + _input.val(value); } return ret.append(label, input_col)[0]; } diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index 55337a36a97d6a7ad42aadfe673e429d6d942a0a..b7ad95f4d907104210eb04771b95f03d3f41d5bd 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -1683,11 +1683,17 @@ function getXSLScriptClone(source) { /** * TODO */ -function injectTemplate(orig_xsl, template) { +function injectTemplate(orig_xsl, templateStr) { var xsl = getXSLScriptClone(orig_xsl); - var entry_t = xsl.createElement("xsl:template"); - xsl.firstElementChild.appendChild(entry_t); - entry_t.outerHTML = template; + // var entry_t = xsl.createElement("xsl:template"); + // xsl.firstElementChild.appendChild(entry_t); + // entry_t.outerHTML = template; // Does not work in templates in Safari 11 + // Workaround follows, remove after Safari also has the behaviour of Firefox and current WebKit + var temp = xsl.documentElement.cloneNode(false) + temp.innerHTML = templateStr; + var entry_t = temp.firstChild; + xsl.documentElement.appendChild(entry_t); + // End of workaround return xsl; } diff --git a/src/doc/administration/comments.rst b/src/doc/administration/comments.rst new file mode 100644 index 0000000000000000000000000000000000000000..f44561d12c247ae5c1c42d9ac1c912d13ab768e2 --- /dev/null +++ b/src/doc/administration/comments.rst @@ -0,0 +1,85 @@ +The comments feature of the caosdb webui +======================================== + +WebUI contains a feature that allows users to add comments to existing +records. + +The feature is not enabled by default. + +You can manually activate it using the following steps: - Add a new +RecordType (e.g. using the Edit Mode) called “Annotation” - Add a new +RecordType called “CommentAnnotation” with parent “Annotation” - Add a +new TEXT Property called “comment” - Add a new REFERENCE Property called +“annotationOf” + +or using the following XML: + +.. code:: xml + + <Property id="-1" name="comment" description="A comment on something." datatype="TEXT"> + </Property> + + <Property id="-2" name="annotationOf" description="The core property of the [Annotation] denoting which entity the annotation is annotating." datatype="REFERENCE"> + </Property> + + <RecordType id="-3" name="Annotation" description="Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs."> + <Property id="-2" name="annotationOf" description="The core property of the [Annotation] denoting which entity the annotation is annotating." datatype="REFERENCE" importance="OBLIGATORY"> + </Property> + </RecordType> + + <RecordType name="CommentAnnotation" description="CommentAnnotations represent user comments on other entities. As they are entities themselves they can be 'responded' by just annotating them with another CommentAnnotation."> + <Parent id="-3" name="Annotation" description="Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs." /> + <Property id="-1" name="comment" description="A comment on something." datatype="TEXT" importance="OBLIGATORY"> + </Property> + </RecordType> + +Additionally, on some servers the comment button might be disabled using +CSS. + +E.g. on the demo server you would have to comment out the following +lines in ``demoserver.css``: + +.. code:: css + + .caosdb-new-comment-button { + visibility: hidden; + } + +Using the YAML-Datamodel-Interface +---------------------------------- + +It’s even easier to add the model using the yaml interface. Use the +following yaml file: + +.. code:: yaml + + + Annotation: + description: Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs. + obligatory_properties: + annotationOf: + description: The core property of the [Annotation] denoting which entity the annotation is annotating. + datatype: REFERENCE + + CommentAnnotation: + description: CommentAnnotations represent user comments on other entities. As they are entities themselves they can be 'responded' by just annotating them with another CommentAnnotation. + inherit_from_obligatory: + - Annotation + obligatory_properties: + comment: + description: A comment on something. + datatype: TEXT + +Save this file under “datamodel.yaml”. + +Make sure you have installed caosdb-models. + +Then sync the model: + +.. code:: python + + import caosdb as db + from caosmodels.parser import parse_model_from_yaml + + model = parse_model_from_yaml("datamodel.yaml") + model.sync_data_model(noquestion=True) diff --git a/src/doc/administration/index.rst b/src/doc/administration/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..e04e1dea523f1d9d8aa83231429a9347ecbfb4de --- /dev/null +++ b/src/doc/administration/index.rst @@ -0,0 +1,10 @@ +Administration +============== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + comments + static-snapshots diff --git a/src/doc/administration/static-snapshots.md b/src/doc/administration/static-snapshots.md new file mode 100644 index 0000000000000000000000000000000000000000..b7f292ad37d4a35f63aed406600190e988bf84a6 --- /dev/null +++ b/src/doc/administration/static-snapshots.md @@ -0,0 +1,50 @@ +# Creating Static WebUI Snapshots + +It can be helpful to generate static snapshots of WebUI contents, e.g. for reviewing layouts or for presentation purposes. This is possible with a little bit of effort. Excitingly not only the layout can be exported, but also a lot of the javascript functionality can be maintained in the static pages. + +**NOTE: This manual page is currently work in progress.** + +## Create the static webui folder in the docker container + +We need a static version of the caosdb-webui. In principle it can be simply copied from e.g. a running docker container or from the public-directory. As it contains self-referencing (cyclic) symlinks a little bit of care has to be taken. + +### Using Docker + +Login to the caosdb/linkahead docker container as root: +```bash +docker exec -u 0 -ti linkahead /bin/bash +``` + +We need to be root (`-u 0`) in order to be able to create a copy of caosdb-webui within the container. + +Create the copy using `cp` and the option for following symlinks `-L`: + +```bash +cp -L git/caosdb-server/caosdb-webui/public/ webui-copy +``` + +It will warn you that two symlinks (which are cyclic) cannot be created. That's fine, we will create these two symlinks later. + +``` +cp: cannot copy cyclic symbolic link 'git/caosdb-server/caosdb-webui/public/1602145811' <- The number here is a "unique" build number +cp: cannot copy cyclic symbolic link 'git/caosdb-server/caosdb-webui/public/webinterface' +``` + +**Please copy the build number somewhere, or make sure your terminal history does not get wiped.** + +Copy webui-copy from the docker container to the location where you want to store the snapshots: +`docker cp linkahead:/opt/caosdb/webui-copy/ .` + +Create the two missing symlinks in webui-copy/public: +``` +ln -s webui-copy/public webui-copy/public/1602145811 +ln -s webui-copy/public webui-copy/public/webinterface +``` + +You can now use the included xslt stylesheet to convert xml files to html using: +```bash +xsltproc webui-copy/public/webcaosdb.xsl test.xml > test.html +``` + +As the generated html file still contains invalid references to `/webinterface/1602145811` +you have to replace all occurences of `/webinterface` with webui-copy/public`. diff --git a/src/doc/extension/module.md b/src/doc/extension/module.md new file mode 100644 index 0000000000000000000000000000000000000000..d4f9b56db508d7c3be9dc0a52953de5c82bf310d --- /dev/null +++ b/src/doc/extension/module.md @@ -0,0 +1,72 @@ +# How to add a module to CaosDB WebUI +The CaosDB WebUI is organized in modules which can easily be added and on a module basis enabled or disabled. + +There are a few steps necessary to create a new module. + +## Create the module file +Create a new file in `src/core/js` starting with `ext_`. E.g. `ext_flight_preview.js`. This file should define one function that wraps every thing and which is enabled at the bottom of the file: + +```js +/* + * ** header with license infoc + * ... + */ + +'use strict'; + +/** + * description of the module ... + * + * @module ext_flight_preview + * @version 0.1 + * + * @requires somelibrary + * (pass the dependencies as arguments) + */ +var ext_flight_preview = function (somelibrary) { + + var init = function (toolbox) { + /* initialization of the module */ + } + + /** + * doc string + */ + var some_function = function (arg1, arg2) { + } + + /* the main function must return the initialization of the module */ + return { + init: init, + }; +//pass the dependencies as arguments here as well +}(somelibrary); + +// this will be replaced by require.js in the future. +$(document).ready(function() { + // use a variable starting with `BUILD_MODULE_` to enable your module + // the build variable has to be enabled in the `build.properties.d/` directory. + // Otherwise the module will not be activated. + if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") { + caosdb_modules.register(ext_flight_preview); + } +}); +``` +## Update xml +Add a section to `src/core/xsl/main.xsl` to include your new file. + +```xsl +<xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_sss_markdown.js')"/> + </xsl:attribute> +</xsl:element> +``` + +## Add to index.html in test +If you have unittests (and you should), you need to add a line in : +`test/core/index.html`. + +## Update the changelog + +## Create a merge request \ No newline at end of file diff --git a/src/doc/extension/xslt-debugging.md b/src/doc/extension/xslt-debugging.md new file mode 100644 index 0000000000000000000000000000000000000000..80443dd8656da5645a5315f83f5ce26dfbbebce0 --- /dev/null +++ b/src/doc/extension/xslt-debugging.md @@ -0,0 +1,33 @@ +# XSLT Debugging + +The CaosDB WebUI uses [XSLT](https://en.wikipedia.org/wiki/XSLT) to transform the servers response into a web page. +In the webui-repository these XSLT stylesheets can be found in `src/core/` and `src/core/xsl`. + +The XSLT stylesheet is typically interpreted on the client side, e.g. in Mozilla Firefox. Error output of the browser regarding XSLT problems are typically hard to debug. For example, Firefox typically does not print detailed information about the location of an exception in the sourcecode. + +So what options do we have to debug xslt stylesheets? + +* So called "printf-style" debugging +* Using a different xslt processor + +I found this thread on Stack Overflow very helpful: +https://stackoverflow.com/questions/218522/tools-for-debugging-xslt + +# "printf-style" debugging + +As mentioned in the Stack Overflow thread referenced above, `<xsl:message>` can be used to output debugging messages during XSLT processing. + +# Using different XSLT processors + +## xsltproc from libxslt + +`xsltproc` is a tool from libxslt that allows transforming XML using XSLT stylesheets on the command line. It is called using: +```bash +xsltproc <stylesheet> <xmlfile> +``` + +So a possible workflow for debugging an xslt script could be: +* Save the test response from the server as `test.xml`. +* Run `make` in repository `caosdb-webui` +* Go to folder `public` in `caosdb-webui` +* Run: `xsltproc webcaosdb.xsl test.xml` diff --git a/src/doc/index.rst b/src/doc/index.rst index 107c9052fd6cdafecd201eb17118d8e56f3da440..24c394349a045bf276c4252a2fde47feae6f533c 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -10,6 +10,7 @@ Welcome to the documentation of CaosDB's web UI! Getting started <getting_started> Tutorials <tutorials/index> Concepts <concepts> + administration/index.rst Extending the UI <extension> API <api/index> diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index ae11b04a380f162018de70a53409e34b4e6990c6..2d7a605fd29788d189d2af0d828266e7e3a35f84 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -255,7 +255,7 @@ QUnit.test("make_datatype_input", function (assert) { const no_dt_input = edit_mode.make_datatype_input(undefined); no_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(no_dt_input)[0]); + .form_to_object($(form_wrapper).append(no_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "TEXT", "reference_scope": null, @@ -266,7 +266,7 @@ QUnit.test("make_datatype_input", function (assert) { const text_dt_input = edit_mode.make_datatype_input("TEXT"); text_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(text_dt_input)[0]); + .form_to_object($(form_wrapper).append(text_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "TEXT", "reference_scope": null, @@ -277,7 +277,7 @@ QUnit.test("make_datatype_input", function (assert) { const ref_dt_input = edit_mode.make_datatype_input("REFERENCE"); ref_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(ref_dt_input)[0]); + .form_to_object($(form_wrapper).append(ref_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": null, @@ -288,7 +288,7 @@ QUnit.test("make_datatype_input", function (assert) { const file_dt_input = edit_mode.make_datatype_input("FILE"); file_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(file_dt_input)[0]); + .form_to_object($(form_wrapper).append(file_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "FILE", "reference_scope": null, @@ -299,7 +299,7 @@ QUnit.test("make_datatype_input", function (assert) { const person_dt_input = edit_mode.make_datatype_input("Person"); person_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(person_dt_input)[0]); + .form_to_object($(form_wrapper).append(person_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": "Person", @@ -310,7 +310,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_text_dt_input = edit_mode.make_datatype_input("LIST<TEXT>"); list_text_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_text_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_text_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "TEXT", "reference_scope": null, @@ -322,7 +322,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_ref_dt_input = edit_mode.make_datatype_input("LIST<REFERENCE>"); list_ref_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_ref_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_ref_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": null, @@ -334,7 +334,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_file_dt_input = edit_mode.make_datatype_input("LIST<FILE>"); list_file_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_file_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_file_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "FILE", "reference_scope": null, @@ -346,7 +346,7 @@ QUnit.test("make_datatype_input", function (assert) { const list_per_dt_input = edit_mode.make_datatype_input("LIST<Person>"); list_per_dt_input.addEventListener("caosdb.field.ready", function (e) { var obj = form_elements - .form_to_object($(form_wrapper).append(list_per_dt_input)[0]); + .form_to_object($(form_wrapper).append(list_per_dt_input)[0])[0]; assert.propEqual(obj, { "atomic_datatype": "REFERENCE", "reference_scope": "Person", @@ -486,9 +486,6 @@ QUnit.test("remove_delete_button", function (assert) { { - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } const datamodel = ` <div><div class=\"btn-group-vertical\"><button type=\"button\" class=\"btn btn-default caosdb-f-edit-panel-new-button new-property\">Create new Property</button><button type=\"button\" class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype\">Create new RecordType</button></div><div title=\"Drag and drop Properties from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing Properties</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('properties');\" id=\"caosdb-f-filter-properties\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input\" title=\"Create this Property.\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-20\">name</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-21\">unit</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-24\">description</li></ul></div></div><div title=\"Drag and drop RecordTypes from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing RecordTypes</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('recordtypes');\" id=\"caosdb-f-filter-recordtypes\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input\" title=\"Create this RecordType\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-30992\">Test</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-31015\">Test2</li></ul></div></div></div>`; diff --git a/test/core/js/modules/ext_bottom_line.js.js b/test/core/js/modules/ext_bottom_line.js.js index d4add1a8997b86dbf9f39566aa5f14b0b6721df1..e684c8da430bef39668388d6f2f520229bbe7014 100644 --- a/test/core/js/modules/ext_bottom_line.js.js +++ b/test/core/js/modules/ext_bottom_line.js.js @@ -24,10 +24,6 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { - const sleep = (ms) => { - return new Promise(res => setTimeout(res, ms)) - } - var test_config = { "version": 0.1, "fallback": "blablabla", "creators": [ diff --git a/test/core/js/modules/ext_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js index 5426580436933b942cec6750b521747ab62f2d00..195e9c825d9601dc08f29c8f1b2171ff13eeef47 100644 --- a/test/core/js/modules/ext_xls_download.js.js +++ b/test/core/js/modules/ext_xls_download.js.js @@ -64,40 +64,35 @@ QUnit.module("ext_xls_download", { }); -{ - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - QUnit.test("call downloadXLS", async function(assert) { - var done = assert.async(2); +QUnit.test("call downloadXLS", async function(assert) { + var done = assert.async(2); - // mock server response (successful) - connection.runScript = async function(exec, param){ - assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py"); - done(); - return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); - } + // mock server response (successful) + connection.runScript = async function(exec, param){ + assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py"); + done(); + return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); + } - caosdb_table_export.go_to_script_results = function(filename) { - assert.equal(filename, "bla", "filename correct"); - done(); - } + caosdb_table_export.go_to_script_results = function(filename) { + assert.equal(filename, "bla", "filename correct"); + done(); + } - var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />'); - var modal = $('<div id="downloadModal"><div>'); - $(document.body).append([tsv_data, modal]); + var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />'); + var modal = $('<div id="downloadModal"><div>'); + $(document.body).append([tsv_data, modal]); - var xsl_link = $("<a/>"); - downloadXLS(xsl_link[0]); + var xsl_link = $("<a/>"); + downloadXLS(xsl_link[0]); - await sleep(500); + await sleep(500); - tsv_data.remove(); - modal.remove(); - }); -} + tsv_data.remove(); + modal.remove(); +}); QUnit.test("_clean_cell", function(assert) { assert.equal(caosdb_table_export._clean_cell("\n\t\n\t"), " ", "No valid content"); diff --git a/test/core/js/modules/form_elements.js.js b/test/core/js/modules/form_elements.js.js index f8bf1ed1ac1495a8a3688aeb7b0cce387c8b69fb..aa2e281c7d25f03ce8ed109fb5728919635abec6 100644 --- a/test/core/js/modules/form_elements.js.js +++ b/test/core/js/modules/form_elements.js.js @@ -23,7 +23,7 @@ 'use strict'; QUnit.module("form_elements.js", { - before: function(assert) { + before: function (assert) { markdown.init(); var entities = [ $('<div><div class="caosdb-id" data-entity-name="name12">id12</div></div>')[0], @@ -32,27 +32,27 @@ QUnit.module("form_elements.js", { $('<div><div class="caosdb-id" data-entity-name="name15">id15</div></div>')[0], ]; - form_elements._query = async function(query) { + form_elements._query = async function (query) { return entities; }; - this.get_example_1 = async function() { + this.get_example_1 = async function () { return $(await $.ajax("html/form_elements_example_1.html"))[0]; }; }, - after: function(assert) { + after: function (assert) { form_elements._init_functions(); } }); -QUnit.test("availability", function(assert) { +QUnit.test("availability", function (assert) { assert.equal(form_elements.version, "0.1", "test version"); assert.ok(form_elements.init, "init available"); assert.ok(form_elements.version, "version available"); }); -QUnit.test("make_reference_option", function(assert) { +QUnit.test("make_reference_option", function (assert) { assert.equal(typeof form_elements.make_reference_option, "function", "function available"); - assert.throws(()=>form_elements.make_reference_option(), /is expected to be a string/, "noargs throws"); + assert.throws(() => form_elements.make_reference_option(), /is expected to be a string/, "noargs throws"); var option = form_elements.make_reference_option("id15"); assert.equal($(option).val(), "id15", "value"); assert.equal($(option).text(), "id15", "text"); @@ -62,14 +62,17 @@ QUnit.test("make_reference_option", function(assert) { }); -QUnit.test("make_reference_select", async function(assert) { +QUnit.test("make_reference_select", async function (assert) { assert.equal(typeof form_elements.make_reference_select, "function", "function available"); - //assert.throws(()=> unasync(form_elements.make_reference_select), /param `entities` is expected to be an array/, "undefined entities throws"); - //assert.throws(()=> unasync(form_elements.make_reference_select, "test"), /param `entities` is expected to be an array/, "string entities throws"); - var select = await form_elements.make_reference_select([ - {dataset: {entityId : "id17"}}, - {dataset: {entityId : "id18"}}, - ]); + var select = await form_elements.make_reference_select([{ + dataset: { + entityId: "id17" + } + }, { + dataset: { + entityId: "id18" + } + }, ]); assert.ok($(select).hasClass("selectpicker"), "selectpicker class from bootstrap-select"); assert.notOk($(select).val(), "unselected"); $(select).val(["id18"]); @@ -86,7 +89,7 @@ QUnit.test("make_reference_select", async function(assert) { }); -QUnit.test("make_script_form", async function(assert) { +QUnit.test("make_script_form", async function (assert) { assert.equal(typeof form_elements.make_script_form, "function", "function available"); // TODO @@ -96,12 +99,31 @@ QUnit.test("make_script_form", async function(assert) { var done = assert.async(3); var config = { - groups: [ - { name: "group1", fields: ["date"], enabled: false }, - ], - fields: [ - {type: "date", name: "baldate"}, - ], + groups: [{ + name: "group1", + fields: ["date"], + enabled: false + }, ], + fields: [{ + type: "date", + name: "baldate" + }, { + type: "select", + name: "Sex", + label: "Sex", + value: "female", + required: true, + options: [{ + value: "female", + label: "female" + }, { + value: "diverse", + label: "diverse" + }, { + value: "male", + label: "male" + }] + }], }; var script_form = await form_elements.make_script_form(config, "test_script"); @@ -115,24 +137,29 @@ QUnit.test("make_script_form", async function(assert) { assert.equal(cancel_button.length, 1, "has cancel button"); var field = $(script_form).find(".caosdb-f-field"); - assert.equal(field.length, 1, "has one field"); + assert.equal(field.length, 2, "has two field"); assert.equal(field.find("input[type='date']").length, 1, "has date input"); + assert.equal(field.find("select").length, 1, "has select input"); - script_form.addEventListener("caosdb.form.cancel", function(e) { + script_form.addEventListener("caosdb.form.cancel", function (e) { done(); }, true); cancel_button.click(); - script_form.addEventListener("caosdb.form.error", function(e) { + script_form.addEventListener("caosdb.form.error", function (e) { assert.equal($(script_form).find(".caosdb-f-form-elements-message-error").length, 2, "error message there (call and stderr)"); done(); script_form.remove(); }); - form_elements._run_script = async function(script, params) { + form_elements._run_script = async function (script, params) { done(); - return {code: "1", stderr: "Autsch!", call: "none"}; + return { + code: "1", + stderr: "Autsch!", + call: "none" + }; }; assert.equal($(script_form).find(".caosdb-f-form-error-message").length, 0, "no error message"); @@ -144,7 +171,7 @@ QUnit.test("make_script_form", async function(assert) { }); -QUnit.test("make_date_input", function(assert) { +QUnit.test("make_date_input", function (assert) { assert.equal(typeof form_elements.make_date_input, "function", "function available"); var config = { @@ -164,7 +191,7 @@ QUnit.test("make_date_input", function(assert) { }); -QUnit.test("make_range_input", async function(assert) { +QUnit.test("make_range_input", async function (assert) { assert.equal(typeof form_elements.make_range_input, "function", "function available"); var config = { @@ -192,20 +219,27 @@ QUnit.test("make_range_input", async function(assert) { }); -QUnit.test("make_form_field", async function(assert) { +QUnit.test("make_form_field", async function (assert) { assert.equal(typeof form_elements.make_form_field, "function", "function available"); - var cached = false; - for ( var t of ["date", "range", "reference_drop_down", "subform", "checkbox", "double", "integer"] ) { + var cached = false; + for (var t of ["date", "range", "reference_drop_down", "subform", "checkbox", "double", "integer"]) { cached = !cached; var config = { - help: {title: "HELP", content: "help me, help me, help me-e-e!"}, + help: { + title: "HELP", + content: "help me, help me, help me-e-e!" + }, type: t, cached: cached, name: "a name", label: "a label", - from: {name: "from_bla"}, - to: {name: "to_bla"}, + from: { + name: "from_bla" + }, + to: { + name: "to_bla" + }, query: "FIND something", make_desc: getEntityName, fields: [], @@ -220,8 +254,8 @@ QUnit.test("make_form_field", async function(assert) { }); -QUnit.test("make_subform", async function(assert) { - assert.equal(typeof form_elements.make_reference_drop_down, "function", "function available"); +QUnit.test("make_subform", async function (assert) { + assert.equal(typeof form_elements.make_subform, "function", "function available"); const config = { type: "subform", @@ -239,7 +273,7 @@ QUnit.test("make_subform", async function(assert) { }); -QUnit.test("make_reference_drop_down", async function(assert) { +QUnit.test("make_reference_drop_down", async function (assert) { assert.equal(typeof form_elements.make_reference_drop_down, "function", "function available"); var config = { @@ -257,7 +291,7 @@ QUnit.test("make_reference_drop_down", async function(assert) { assert.equal(label.text(), "IceCore", "label has text"); }); -QUnit.test("make_checkbox_input", function(assert) { +QUnit.test("make_checkbox_input", function (assert) { assert.equal(typeof form_elements.make_checkbox_input, "function", "function available"); @@ -274,7 +308,7 @@ QUnit.test("make_checkbox_input", function(assert) { assert.equal(input.attr("name"), "approved", "input has name"); assert.ok(input.is(":checked"), "input is checked"); - var obj = form_elements.form_to_object(field); + var obj = form_elements.form_to_object(field)[0]; assert.equal(obj["approved"], "yes!!!", "checked value"); @@ -286,36 +320,72 @@ QUnit.test("make_checkbox_input", function(assert) { assert.equal(input.attr("name"), "approved", "input has name"); assert.notOk(input.is(":checked"), "input is not checked"); - obj = form_elements.form_to_object(field); + obj = form_elements.form_to_object(field)[0]; assert.equal(typeof obj["approved"], "undefined", "no checked value"); }); -QUnit.test("form_to_object", async function(assert) { +QUnit.test("form_to_object", async function (assert) { assert.equal(typeof form_elements.form_to_object, "function", "function available"); var config = { - fields: [ - { type: "date", name: "the-date" }, - { type: "reference_drop_down", name: "icecore", query: "FIND Record IceCore"}, - { type: "range", name: "the-range", from: {name: "fromblla"}, to: {name: "toblla"}}, - { type: "subform", name: "subform1", fields: [ - { type: "date", name: "the-other-date", }, - { type: "checkbox", name: "rectangular", }, - ],}, - ], + fields: [{ + type: "date", + name: "the-date" + }, { + type: "reference_drop_down", + name: "icecore", + query: "FIND Record IceCore" + }, { + type: "range", + name: "the-range", + from: { + name: "fromblla" + }, + to: { + name: "toblla" + } + }, { + type: "subform", + name: "subform1", + fields: [{ + type: "date", + name: "the-other-date", + }, { + type: "checkbox", + name: "rectangular", + }, ], + }, { + type: "select", + required: true, + cached: true, + name: "sex", + label: "Sex", + value: "d", + options: [{ + "value": "f", + "label": "female" + }, { + "value": "d", + "label": "diverse" + }, { + "value": "m", + "label": "male" + }, ], + }, ], }; var form = await form_elements.make_script_form(config, "bla"); - var json = form_elements.form_to_object(form); + var json = form_elements.form_to_object(form)[0]; assert.equal(typeof json["cancel"], "undefined", "cancel button not serialized"); assert.equal(json["the-date"], "", "date"); assert.equal(json["icecore"], null, "reference_drop_down"); assert.equal(json["fromblla"], "", "range from"); assert.equal(json["toblla"], "", "range to"); + assert.equal(json["sex"], "d", "select"); assert.equal(typeof json["the-other-date"], "undefined", "subform element not on root level"); var subform = json["subform1"]; @@ -327,10 +397,13 @@ QUnit.test("form_to_object", async function(assert) { }); -QUnit.test("make_double_input", function(assert) { +QUnit.test("make_double_input", function (assert) { assert.equal(typeof form_elements.make_double_input, "function", "function available"); - var config = {type: "double", name: "d"}; + var config = { + type: "double", + name: "d" + }; var input = $(form_elements.make_double_input(config)).find("input"); assert.ok(input.is("[type='number'][step='any']"), "double input"); @@ -340,10 +413,13 @@ QUnit.test("make_double_input", function(assert) { }); -QUnit.test("make_integer_input", function(assert) { +QUnit.test("make_integer_input", function (assert) { assert.equal(typeof form_elements.make_integer_input, "function", "function available"); - var config = {type: "integer", name: "i"}; + var config = { + type: "integer", + name: "i" + }; var input = $(form_elements.make_integer_input(config)).find("input"); assert.ok(input.is("[type='number'][step='1']"), "integer input"); @@ -352,21 +428,26 @@ QUnit.test("make_integer_input", function(assert) { assert.equal(input.val("abc").val(), "", "abc not valid"); }); -QUnit.test("make_form", function(assert) { +QUnit.test("make_form", function (assert) { assert.equal(typeof form_elements.make_form, "function", "function available"); - var form1 = form_elements.make_form({fields: []}); + var form1 = form_elements.make_form({ + fields: [] + }); assert.equal(form1.tagName, "DIV", "wrapper is div"); assert.ok($(form1).hasClass("caosdb-f-form-wrapper"), "div has caosdb-f-form-wrapper class"); assert.equal($(form1).find(".h3").length, 0, "no header"); - var form2 = form_elements.make_form({fields: [], header: "bla"}); + var form2 = form_elements.make_form({ + fields: [], + header: "bla" + }); assert.equal(form2.tagName, "DIV", "wrapper is div"); assert.equal($(form2).find(".h3").length, 1, "one header"); assert.equal($(form2).find(".h3").text(), "bla", "header text set"); }); -QUnit.test("enable/disable_group", function(assert) { +QUnit.test("enable/disable_group", function (assert) { assert.equal(typeof form_elements.disable_group, "function", "function available"); assert.equal(typeof form_elements.enable_group, "function", "function available"); @@ -413,11 +494,11 @@ QUnit.test("enable/disable_group", function(assert) { }); -QUnit.test("parse_script_result", function(assert) { +QUnit.test("parse_script_result", function (assert) { assert.equal(typeof form_elements.parse_script_result, "function", "function available"); var result = str2xml( -`<?xml version="1.0" encoding="UTF-8"?> + `<?xml version="1.0" encoding="UTF-8"?> <?xml-stylesheet type="text/xsl" href="https://localhost:10443/webinterface/webcaosdb.xsl" ?> <Response username="admin" realm="PAM" srid="256c14970dac2b2b5649973d52e4c06a" timestamp="1570785591824" baseuri="https://localhost:10443"> <UserInfo username="admin" realm="PAM"> @@ -443,15 +524,15 @@ QUnit.test("parse_script_result", function(assert) { }); -QUnit.test("disable_name", function(assert) { +QUnit.test("disable_name", function (assert) { assert.equal(typeof form_elements.disable_name, "function", "function available"); }); -QUnit.test("enable_name", function(assert) { +QUnit.test("enable_name", function (assert) { assert.equal(typeof form_elements.enable_name, "function", "function available"); }); -QUnit.test("add_field_to_group", function(assert) { +QUnit.test("add_field_to_group", function (assert) { assert.equal(typeof form_elements.add_field_to_group, "function", "function available"); var field = $(form_elements._make_field_wrapper("field1"))[0]; @@ -462,7 +543,7 @@ QUnit.test("add_field_to_group", function(assert) { }); -QUnit.test("cache_form", async function(assert) { +QUnit.test("cache_form", async function (assert) { var form = await this.get_example_1(); assert.equal($(form).find("form").length, 1, "example form available"); @@ -476,7 +557,7 @@ QUnit.test("cache_form", async function(assert) { }); -QUnit.test("load_cached", async function(assert) { +QUnit.test("load_cached", async function (assert) { var done = assert.async(); var form = await this.get_example_1(); assert.equal($(form).find("form").length, 1, "example form available"); @@ -498,7 +579,7 @@ QUnit.test("load_cached", async function(assert) { }); -QUnit.test("field_ready", function(assert) { +QUnit.test("field_ready", function (assert) { var done = assert.async(3); var field1 = $('<div id="f1"><div class="caosdb-f-field-not-ready"/></div>')[0]; var field2 = $('<div id="f2" class="caosdb-f-field-not-ready"/>')[0]; @@ -541,16 +622,13 @@ QUnit.test("field_ready", function(assert) { }); }); -{ -const sleep = (ms) => { - return new Promise(res => setTimeout(res, ms)) -} - -QUnit.test("make_alert - cancel", async function(assert) { +QUnit.test("make_alert - cancel", async function (assert) { var cancel_callback = assert.async() var _alert = form_elements.make_alert({ message: "message", - proceed_callback: () => {assert.ok(false, "this should not be called");}, + proceed_callback: () => { + assert.ok(false, "this should not be called"); + }, cancel_callback: cancel_callback, }); $("body").append(_alert); @@ -564,7 +642,7 @@ QUnit.test("make_alert - cancel", async function(assert) { }); -QUnit.test("make_alert - proceed", async function(assert) { +QUnit.test("make_alert - proceed", async function (assert) { var proceed_callback = assert.async(); var _alert = form_elements.make_alert({ message: "message", @@ -581,7 +659,7 @@ QUnit.test("make_alert - proceed", async function(assert) { }); -QUnit.test("make_alert - remember", async function(assert) { +QUnit.test("make_alert - remember", async function (assert) { form_elements._set_alert_decision("unittests", ""); var proceed_callback = assert.async(3); @@ -628,4 +706,83 @@ QUnit.test("make_alert - remember", async function(assert) { assert.equal(typeof _alert, "undefined", "alert was not created, proceed callback was called third time"); }); -} +QUnit.test("make_select_input", function (assert) { + const config = { + name: "sex", + label: "Sex", + multiple: true, + options: [{ + "value": "f", + "label": "female" + }, { + "value": "d", + "label": "diverse" + }, { + "value": "m", + "label": "male" + }, ], + } + const select = $(form_elements.make_select_input(config)); + assert.equal(select.find("select").length, 1, "select input there"); + assert.equal(select.find("select").attr("name"), "sex", "has select with correct name"); + assert.equal(select.find("select option").length, 3, "three options there"); +}); + +QUnit.test("select_input caching", async function (assert) { + const config = { + "name": "test-form", + "fields": [{ + type: "select", + required: true, + cached: true, + name: "sex", + label: "Sex", + options: [{ + "value": "f", + "label": "female" + }, { + "value": "d", + "label": "diverse" + }, { + "value": "m", + "label": "male" + }, ], + }, ], + } + const form_wrapper = $(form_elements.make_form(config)); + await sleep(200); + const form = form_wrapper.find("form"); + assert.equal(form.find("select").length, 1); + + + // write to cache + const cache = {}; + const field = $(form_elements.get_fields(form[0], "sex")); + field.find("select").val("f"); + assert.equal(form_elements.get_cache_value(field[0]), "f", "initial value set"); + assert.equal(form_elements.get_cache_key(form[0], field[0]), "form_elements.cache.test-form.sex", "cache key correct"); + + form_elements.cache_form(cache, form[0]); + assert.equal(cache[form_elements.get_cache_key(form[0], field[0])], "f"); + + // read from cache and set the value + field.find("select").val("m"); + assert.equal(form_elements.get_cache_value(field[0]), "m", "different value set"); + + form_elements.load_cached(cache, form[0]); + await sleep(200); + assert.equal(form_elements.get_cache_value(field[0]), "f", "value back to value from cache"); +}); + +QUnit.test("make_file_input", function (assert) { + const config = { + name: "some_file", + multiple: true, + accept: ".tsv, .csv", + } + const file_input = $(form_elements.make_file_input(config)); + assert.equal(file_input.find(":input").length, 1, "file input there"); + assert.equal(file_input.find(":input").attr("name"), "some_file", "has file input with correct name"); + assert.ok(file_input.find(":input").prop("multiple"), "is multiple"); + assert.equal(file_input.find(":input").attr("accept"), ".tsv, .csv", "accept there"); +}); \ No newline at end of file diff --git a/test/core/js/modules/query_shortcuts.js.js b/test/core/js/modules/query_shortcuts.js.js index f088b729001daac8a42ed276507e2370f3e08679..3798c5fe81ea6e860cd176797a9a6959a9a64895 100644 --- a/test/core/js/modules/query_shortcuts.js.js +++ b/test/core/js/modules/query_shortcuts.js.js @@ -141,7 +141,9 @@ QUnit.test("init_delete_shortcut_form", function(assert) { assert.equal(panel.find(".caosdb-f-form-wrapper").length, 1, "panel has form after"); // test cancel button - panel[0].addEventListener("caosdb.form.cancel", function(e) { + var done = assert.async(); + panel[0].addEventListener("caosdb.form.cancel", async function(e) { + await sleep(200); assert.equal(panel.find("form").length, 0, "form is gone"); done(); }, true); @@ -187,23 +189,17 @@ QUnit.test("make_delete_form", function(assert) { } var form = query_shortcuts.make_delete_form(panel[0], delete_callback); + $('body').append(form); - // wait for form - form.addEventListener("caosdb.form.ready", function(e) { - - assert.equal($(form).hasClass("caosdb-f-form-wrapper"), true, "is form"); - assert.equal($(form).find("input[type='checkbox']").length, 3, "three checkboxes (for three user-defined shortcuts)"); - - // check two - $(form).find(":checkbox[name='id28']").click(); - $(form).find(":checkbox[name='id29']").click(); - $(form).find("[type='submit']").click(); - - $(form).find("button.caosdb-f-form-elements-cancel-button").click(); + assert.equal($(form).hasClass("caosdb-f-form-wrapper"), true, "is form"); + assert.equal($(form).find("input[type='checkbox']").length, 3, "three checkboxes (for three user-defined shortcuts)"); - }, true); + // check two + $(form).find(":checkbox[name='id28']").click(); + $(form).find(":checkbox[name='id29']").click(); + $(form).find("[type='submit']").click(); - $('body').append(form); + $(form).find("button.caosdb-f-form-elements-cancel-button").click(); }); @@ -228,31 +224,24 @@ QUnit.test("transform_entities", async function(assert) { }); QUnit.test("make_create_form", function(assert) { - - var done = assert.async(); var panel = $('<div/>'); var form = query_shortcuts.make_create_form(panel[0], () => {}); assert.ok($(form).hasClass("caosdb-f-form-wrapper"), "form created"); $('body').append(form); - form.addEventListener(form_elements.form_ready_event.type, function(e) { - $(form).find(":input[name='templateDescription']").val("NEW DESC"); - $(form).find(":input[name='Query']").val("NEW QUERY"); + $(form).find(":input[name='templateDescription']").val("NEW DESC"); + $(form).find(":input[name='Query']").val("NEW QUERY"); - var entity = getEntityXML(form); - assert.equal(xml2str(entity), "<Record><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); + var entity = getEntityXML(form); + assert.equal(xml2str(entity), "<Record><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); - form_elements.dismiss_form(form); - done(); + form_elements.dismiss_form(form); - }, true); }); QUnit.test("make_update_form", function(assert) { - - var done = assert.async(); var panel = $('<div/>'); var header = $('<span class="h3">Shortcuts</span>'); var userTemplate1 = query_shortcuts.generate_user_shortcut("the_description", "FIND nothing", "id28"); @@ -271,16 +260,12 @@ QUnit.test("make_update_form", function(assert) { $('body').append(form); - form.addEventListener(form_elements.form_ready_event.type, function(e) { - $(form).find(":input[name='templateDescription']").val("NEW DESC"); - $(form).find(":input[name='Query']").val("NEW QUERY"); - - var entity = getEntityXML(form); - assert.equal(xml2str(entity), "<Record id=\"id28\"><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); + $(form).find(":input[name='templateDescription']").val("NEW DESC"); + $(form).find(":input[name='Query']").val("NEW QUERY"); - form_elements.dismiss_form(form); - done(); + var entity = getEntityXML(form); + assert.equal(xml2str(entity), "<Record id=\"id28\"><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted"); - }, true); + form_elements.dismiss_form(form); }); diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 7b3f7abf404f261668c18690df337b73794dd8bd..95c4f62d983077374343a099d4d4bcb2680e25d5 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -2008,7 +2008,3 @@ QUnit.test("init_load_history_buttons and init_load_history_buttons", async func $(html).remove(); }); - -const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/test/core/js/setup.js b/test/core/js/setup.js index 9894827988999e89a6388206f10bdd815098e81e..fac4fd78d0c867cf213917b16d874513d1871a09 100644 --- a/test/core/js/setup.js +++ b/test/core/js/setup.js @@ -46,3 +46,6 @@ QUnit.done(function( details ) { $.post("/done", report); }); +const sleep = function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 026887097ac3b7dc13e6e429bf73c363e3adcbf3..92889dc526229475463ddcdfe6a2080a669b9d25 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -13,7 +13,7 @@ RUN apt-get update \ && apt-get install -f RUN pip3 install pylint pytest -RUN pip3 install caosdb +RUN pip3 install caosdb==0.5.1 RUN pip3 install pandas xlrd==1.2.0 RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev # For automatic documentation diff --git a/test/server_side_scripting/ext_table_preview/data/bad.csv b/test/server_side_scripting/ext_table_preview/data/bad.csv deleted file mode 100644 index d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa..0000000000000000000000000000000000000000 Binary files a/test/server_side_scripting/ext_table_preview/data/bad.csv and /dev/null differ diff --git a/test/server_side_scripting/ext_table_preview/data/bad.tsv b/test/server_side_scripting/ext_table_preview/data/bad.tsv deleted file mode 100644 index d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa..0000000000000000000000000000000000000000 Binary files a/test/server_side_scripting/ext_table_preview/data/bad.tsv and /dev/null differ diff --git a/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py index 00d1c7f38746abe437abc76cd51b29600adcd049..c7c0cd3bc22c62ad4f1a214d4ed777718cdbf74a 100644 --- a/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py +++ b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py @@ -67,8 +67,9 @@ class PreviewTest(unittest.TestCase): assert (table == searchkey).any(axis=None) badfiles = [os.path.join(os.path.dirname(__file__), "data", f) - for f in ["bad.csv", "bad.tsv", "bad.xls", "bad.xlsx"]] + for f in ["bad.xls", "bad.xlsx"]] for bfi in badfiles: + print("bfi: ", bfi) self.assertRaises(ValueError, read_file, bfi, "."+bfi.split(".")[-1])