diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md new file mode 100644 index 0000000000000000000000000000000000000000..a8c5b719ad5f8e18c2fd68d2daa1e5c62f94d450 --- /dev/null +++ b/.gitlab/merge_request_templates/Default.md @@ -0,0 +1,48 @@ +# Summary + + Insert a meaningful description for this merge request here. What is the + new/changed behavior? Which bug has been fixed? Are there related Issues? + +# Focus + + Point the reviewer to the core of the code change. Where should they start + reading? What should they focus on (e.g. security, performance, + maintainability, user-friendliness, compliance with the specs, finding more + corner cases, concrete questions)? + +# Test Environment + + How to set up a test environment for manual testing? + +# Check List for the Author + +Please, prepare your MR for a review. Be sure to write a summary and a +focus and create gitlab comments for the reviewer. They should guide the +reviewer through the changes, explain your changes and also point out open +questions. For further good practices have a look at [our review +guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md) + +- [ ] All automated tests pass +- [ ] Reference related Issues +- [ ] Up-to-date CHANGELOG.md +- [ ] Annotations in code (Gitlab comments) + - Intent of new code + - Problems with old code + - Why this implementation? + + +# Check List for the Reviewer + + +- [ ] I understand the intent of this MR +- [ ] All automated tests pass +- [ ] Up-to-date CHANGELOG.md +- [ ] The test environment setup works and the intended behavior is + reproducible in the test environment +- [ ] In-code documentation and comments are up-to-date. +- [ ] Check: Are there spezifications? Are they satisfied? + +For further good practices have a look at [our review guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md). + + +/assign me diff --git a/src/caosadvancedtools/serverside/__init__.py b/src/caosadvancedtools/serverside/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/caosadvancedtools/serverside/helper.py b/src/caosadvancedtools/serverside/helper.py new file mode 100644 index 0000000000000000000000000000000000000000..360f287759e582e548808d22771ee296eeb785ca --- /dev/null +++ b/src/caosadvancedtools/serverside/helper.py @@ -0,0 +1,279 @@ +# encoding: utf-8 +# +# Copyright (C) 2019, 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2019, 2020 Henrik tom Wörden <h.tomwoerden@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/>. +# +# +from __future__ import absolute_import + +import argparse +import datetime +import json +import logging +import sys + +import caosdb as db + + +def wrap_bootstrap_alert(text, kind): + """ Wrap a text into a Bootstrap (3.3.7) DIV.alert. + + Parameters + ---------- + + text : str + The text body of the bootstrap alert. + kind : str + One of ["success", "info", "warning", "danger"] + + Returns + ------- + alert : str + A HTML str of a Bootstrap DIV.alert + """ + return ('<div class="alert alert-{kind} alert-dismissible" ' + 'role="alert">{text}</div>').format(kind=kind, text=text) + + +def print_bootstrap(text, kind, file=sys.stdout): + """ Wrap a text into a Bootstrap (3.3.7) DIV.alert and print it to a file. + + Parameters + ---------- + + text : str + The text body of the bootstrap alert. + kind : str + One of ["success", "info", "warning", "danger"] + file : file, optional + Print the alert to this file. Default: sys.stdout. + + Returns + ------- + None + """ + print(wrap_bootstrap_alert(text, kind), file=file) + + +def print_success(text): + """Shortcut for print_bootstrap(text, kine="success") + + The text body is also prefixed with "<b>Success:</b> ". + + Parameters + ---------- + + text : str + The text body of the bootstrap alert. + + Returns + ------- + None + """ + print_bootstrap("<b>Success:</b> " + text, kind="success") + + +def print_info(text): + """Shortcut for print_bootstrap(text, kine="info") + + The text body is also prefixed with "<b>Info:</b> ". + + Parameters + ---------- + + text : str + The text body of the bootstrap alert. + + Returns + ------- + None + """ + print_bootstrap("<b>Info:</b> " + text, kind="info") + + +def print_warning(text): + """Shortcut for print_bootstrap(text, kine="warning") + + The text body is also prefixed with "<b>Warning:</b> ". + + Parameters + ---------- + + text : str + The text body of the bootstrap alert. + + Returns + ------- + None + """ + print_bootstrap("<b>Warning:</b> " + text, kind="warning") + + +def print_error(text): + """Shortcut for print_bootstrap(text, kine="danger") + + The text body is also prefixed with "<b>ERROR:</b> ". + + Parameters + ---------- + + text : str + The text body of the bootstrap alert. + + Returns + ------- + None + """ + print_bootstrap("<b>ERROR:</b> " + text, kind="danger", file=sys.stderr) + + +class DataModelError(RuntimeError): + """DataModelError indicates that the server-side script cannot work as + intended due to missing datat model entities or an otherwise incompatible + data model.""" + + def __init__(self, rt): + super().__init__( + "This script expects certain RecordTypes and Properties to exist " + "in the data model. There is a problem with {} .".format(rt)) + + +def recordtype_is_child_of(rt, parent): + """Return True iff the RecordType is a child of another Entity. + + The parent Entity can be a direct or indirect parent. + + Parameters + ---------- + + rt : caosdb.Entity + The child RecordType. + parent : str or int + The parent's name or id. + + Returns + ------- + bool + True iff `rt` is a child of `parent` + """ + query = "COUNT RecordType {} with id={}".format(parent, rt.id) + + if db.execute_query(query) > 0: + return True + else: + return False + + +def init_data_model(entities): + """Return True if all entities exist. + + Parameters + ---------- + + entities : iterable of caosdb.Entity + The data model entities which are to be checked for existence. + + Raises + ------ + DataModelError + If any entity in `entities` does not exists. + + Returns + ------- + bool + True if all entities exist. + """ + try: + for e in entities: + e.retrieve() + except db.exceptions.EntityDoesNotExistError: + raise DataModelError(e.name) + + return True + + +def get_data(filename, default=None): + """Load data from a json file as a dict. + + Parameters + ---------- + + filename : str + The file's path, relative or absolute. + default : dict + Default data, which is overridden by the data in the file, if the keys + are defined in the file. + + Returns + ------- + dict + Data from the given file. + """ + result = default.copy() if default is not None else {} + with open(filename, 'r') as fi: + data = json.load(fi) + result.update(data) + + return result + + +def get_timestamp(): + """Return a ISO 8601 compliante timestamp (second precision)""" + return datetime.datetime.utcnow().isoformat(timespec='seconds') + + +def get_argument_parser(): + """Return a argparse.ArgumentParser for typical use-cases. + + The parser expects a file name as data input ('filename') and and an + optional auth-token ('--auth-token'). + + The parser can also be augmented for other use cases. + + Returns + ------- + argparse.ArgumentParser + """ + p = argparse.ArgumentParser() + # TODO: add better description. I do not know what json file is meant. + # TODO: use a prefix for this argument? using this in another parser is + # difficult otherwise + p.add_argument("filename", help="The json filename") + p.add_argument("--auth-token") + + return p + + +def parse_arguments(args): + """Use the standard parser and parse the arguments. + + Call with `parse_arguments(args=sys.argv)` to parse the command line + arguments. + + Parameters + ---------- + args : list of str + Arguments to parse. + + Returns + ------- + dict + Parsed arguments. + """ + p = get_argument_parser() + + return p.parse_args(args) diff --git a/src/caosadvancedtools/webui_formatter.py b/src/caosadvancedtools/webui_formatter.py new file mode 100644 index 0000000000000000000000000000000000000000..848f3e39b5607f828934a29a68b5bb94529a542d --- /dev/null +++ b/src/caosadvancedtools/webui_formatter.py @@ -0,0 +1,94 @@ +# encoding: utf-8 +# +# Copyright (C) 2019, 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2019, 2020 Henrik tom Wörden <h.tomwoerden@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/>. +# +# +import logging + +from .serverside.helper import prefix_bootstrap + + +class WebUI_Formatter(logging.Formatter): + """ allows to make logging to be nicely displayed in the WebUI + + You can enable this as follows: + logger = logging.getLogger("<LoggerName>") + formatter = WebUI_Formatter(full_file="path/to/file") + handler = logging.Handler() + handler.setFormatter(formatter) + logger.addHandler(handler) + """ + + def __init__(self, *args, full_file=None, **kwargs): + super().__init__(*args, **kwargs) + self.max_elements = 100 + self.counter = 0 + self.full_file = full_file + + def format(self, record): + """ Return the HTML formatted log record for display on a website. + + This essentially wraps the text formatted by the parent class in html. + + Parameters + ---------- + + record : + + Raises + ------ + RuntimeError + If the log level of the record is not supported. Supported log + levels include logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, and logging.CRITICAL. + + Returns + ------- + str + The formatted log record. + + """ + msg = super().format(record) + self.counter += 1 + + if self.counter == self.max_elements: + return prefix_bootstrap( + "<b>Warning:</b> Due to the large number of messages, the " + "output is stopped here. You can see the full log " + " <a href='{}'>here</a>.".format(self.full_file), + kind="warning") + + if self.counter > self.max_elements: + return "" + + text = msg.replace("\n", r"</br>") + text = text.replace("\t", r" "*4) + + if record.levelno == logging.DEBUG: + return prefix_bootstrap(msg, kind="info") + elif record.levelno == logging.INFO: + return prefix_bootstrap("<b>Info:</b> " + text, kind="info") + elif record.levelno == logging.WARNING: + return prefix_bootstrap("<b>Warning:</b> " + text, kind="warning") + elif record.levelno == logging.ERROR: + return prefix_bootstrap("<b>ERROR:</b> " + text, kind="danger") + elif record.levelno == logging.CRITICAL: + return prefix_bootstrap("<b>CRITICAL ERROR:</b> " + text, + kind="danger") + else: + raise Exception("unknown level")