diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..97b8fdf2b5ae43dc96726e16ea21a2c6a1883fdb --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,37 @@ +FROM debian:bookworm +RUN apt-get update && \ + apt-get install \ + curl \ + git \ + openjdk-17-jdk-headless \ + python3-autopep8 \ + python3-pip \ + python3-pytest \ + python3-sphinx \ + -y +RUN pip3 install --break-system-packages pylint recommonmark sphinx-rtd-theme tox +COPY .docker/wait-for-it.sh /wait-for-it.sh +ARG PYLIB +ADD https://gitlab.indiscale.com/api/v4/projects/97/repository/commits/${PYLIB} \ + pylib_version.json +RUN git clone https://gitlab.indiscale.com/caosdb/src/caosdb-pylib.git && \ + cd caosdb-pylib && git checkout ${PYLIB} && pip3 install --break-system-packages . +ARG ADVANCED +ADD https://gitlab.indiscale.com/api/v4/projects/104/repository/commits/${ADVANCED} \ + advanced_version.json +RUN git clone https://gitlab.indiscale.com/caosdb/src/caosdb-advanced-user-tools.git && \ + cd caosdb-advanced-user-tools && git checkout ${ADVANCED} && pip3 install --break-system-packages .[h5-crawler] +COPY . /git + +# Delete .git because it is huge. +RUN rm -r /git/.git + +RUN cd /git/ && pip3 install --break-system-packages . + +WORKDIR /git/integrationtests +# wait for server, +CMD /wait-for-it.sh caosdb-server:10443 -t 500 -- \ + # ... install pycaosdb.ini the server-side scripts + cp /git/.docker/sss_pycaosdb.ini /scripting/home/.pycaosdb.ini && \ + # ... and run tests + pytest-3 . diff --git a/.docker/cert.sh b/.docker/cert.sh new file mode 100755 index 0000000000000000000000000000000000000000..628ba8dd9cc19f85a515a75cebd03b8981337bfd --- /dev/null +++ b/.docker/cert.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2019 Daniel Hornung, Göttingen +# +# 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/>. +# +# ** end header + + +# Creates a directory `cert` and certificates in this directory. +# +# The hostname for which the certificate is created can be changed by setting +# the environment variable CAOSHOSTNAME. +# +# ## Overview of variables ## +# +# - CAOSHOSTNAME :: Hostname for the key (localhost) +# - KEYPW :: Password for the key (default ist CaosDBSecret) +# - KEYSTOREPW :: Password for the key store (same as KEYPW) +function cert() { + mkdir -p cert + cd cert + KEYPW="${KEYPW:-CaosDBSecret}" + CAOSHOSTNAME="${CAOSHOSTNAME:-localhost}" + KEYSTOREPW="${KEYPW:-}" + # NOTE: KEYPW and KEYSTOREPW are the same, due to Java limitations. + KEYPW="${KEYPW}" openssl genrsa -aes256 -out caosdb.key.pem \ + -passout env:KEYPW 2048 + # Certificate is for localhost + KEYPW="${KEYPW}" openssl req -new -x509 -key caosdb.key.pem \ + -out caosdb.cert.pem -passin env:KEYPW \ + -subj "/C=/ST=/L=/O=example/OU=example/CN=${CAOSHOSTNAME}" \ + -days 365 \ + -addext "subjectAltName = DNS:${CAOSHOSTNAME}" \ + -addext "certificatePolicies = 1.2.3.4" + KEYPW="${KEYPW}" KEYSTOREPW="$KEYSTOREPW" openssl pkcs12 -export \ + -inkey caosdb.key.pem -in caosdb.cert.pem -out all-certs.pkcs12 \ + -passin env:KEYPW -passout env:KEYPW + + keytool -importkeystore -srckeystore all-certs.pkcs12 -srcstoretype PKCS12 \ + -deststoretype pkcs12 -destkeystore caosdb.jks \ + -srcstorepass "${KEYPW}" \ + -destkeypass "${KEYPW}" -deststorepass "$KEYSTOREPW" + echo "Certificates successfuly created." +} + +cert diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..02ccac5c48e039a3374a0d169f3b355f897e45fc --- /dev/null +++ b/.docker/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.7' +services: + sqldb: + image: mariadb:10.4 + environment: + MYSQL_ROOT_PASSWORD: caosdb1234 + networks: + - caosnet + caosdb-server: + image: "$CI_REGISTRY/caosdb/src/caosdb-deploy:$CAOSDB_TAG" + user: 999:999 + depends_on: + - sqldb + networks: + - caosnet + volumes: + - type: bind + source: ./cert + target: /opt/caosdb/cert + - type: bind + source: "../integrationtests/test_data/extroot" + target: /opt/caosdb/mnt/extroot + - type: volume + source: scripting + target: /opt/caosdb/git/caosdb-server/scripting + - type: volume + source: authtoken + target: /opt/caosdb/git/caosdb-server/authtoken + ports: + # - "from_outside:from_inside" + - "10443:10443" + - "10080:10080" + environment: + DEBUG: 1 + CAOSDB_CONFIG_AUTHTOKEN_CONFIG: "conf/core/authtoken.example.yaml" + CAOSDB_CONFIG_TRANSACTION_BENCHMARK_ENABLED: "TRUE" + CAOSDB_CONFIG__CAOSDB_INTEGRATION_TEST_SUITE_KEY: 10b128cf8a1372f30aa3697466bb55e76974e0c16a599bb44ace88f19c8f61e2 +volumes: + scripting: + authtoken: +networks: + caosnet: + driver: bridge diff --git a/.docker/run.sh b/.docker/run.sh new file mode 100755 index 0000000000000000000000000000000000000000..b0e1a716f28516b83043fb3fdb6594515a0bafd4 --- /dev/null +++ b/.docker/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +docker-compose -f tester.yml run tester +rv=$? +echo $rv > result diff --git a/.docker/sss_pycaosdb.ini b/.docker/sss_pycaosdb.ini new file mode 100644 index 0000000000000000000000000000000000000000..de2867f8dc66b3e81f10f35e40c36f9cb8591604 --- /dev/null +++ b/.docker/sss_pycaosdb.ini @@ -0,0 +1,9 @@ +; this is the pycaosdb.ini for the server-side-scripting home. +[Connection] +url = https://caosdb-server:10443 +cacert = /opt/caosdb/cert/caosdb.cert.pem +debug = 0 +timeout = 5000 + +[Misc] +sendmail = /usr/local/bin/sendmail_to_file diff --git a/.docker/tester.yml b/.docker/tester.yml new file mode 100644 index 0000000000000000000000000000000000000000..83db879c6072bfdea7b3212c833116b96bb54d0c --- /dev/null +++ b/.docker/tester.yml @@ -0,0 +1,26 @@ +version: '3.7' +services: + tester: + image: "$CI_REGISTRY_IMAGE" + networks: + - docker_caosnet + volumes: + - type: bind + source: ./cert + target: /cert + - type: volume + source: extroot + target: /extroot + - type: volume + source: scripting + target: /scripting + - type: volume + source: authtoken + target: /authtoken +networks: + docker_caosnet: + external: true +volumes: + scripting: + extroot: + authtoken: diff --git a/.docker/tester_pycaosdb.ini b/.docker/tester_pycaosdb.ini new file mode 100644 index 0000000000000000000000000000000000000000..2159dec250b3dcb2f16043d12bdbe73675e4d75c --- /dev/null +++ b/.docker/tester_pycaosdb.ini @@ -0,0 +1,31 @@ +; pycaosdb.ini for pytest test suites. + +[IntegrationTests] +; location of the scripting bin dir which is used for the test scripts from the +; server's perspective. +test_server_side_scripting.bin_dir.server = scripting/bin-debug/ +; location of the scripting bin dir which is used for the test scripts from the +; pyinttest's perspective. +test_server_side_scripting.bin_dir.local = /scripting/bin-debug/ + +; location of the files from the pyinttest perspective +test_files.test_insert_files_in_dir.local = /extroot/test_insert_files_in_dir/ +; location of the files from the caosdb_servers perspective +test_files.test_insert_files_in_dir.server = /opt/caosdb/mnt/extroot/test_insert_files_in_dir/ + +; location of the one-time tokens from the pyinttest's perspective +test_authentication.admin_token_crud = /authtoken/admin_token_crud.txt +test_authentication.admin_token_expired = /authtoken/admin_token_expired.txt +test_authentication.admin_token_3_attempts = /authtoken/admin_token_3_attempts.txt + + +[Connection] +url = https://caosdb-server:10443/ +username = admin +cacert = /cert/caosdb.cert.pem +debug = 0 + +password_method = plain +password = caosdb + +timeout = 500 diff --git a/.docker/wait-for-it.sh b/.docker/wait-for-it.sh new file mode 100755 index 0000000000000000000000000000000000000000..d69e99f1f13257b559dce2433de0515379663efa --- /dev/null +++ b/.docker/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# License: +# From https://github.com/vishnubob/wait-for-it +# The MIT License (MIT) +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + WAITFORIT_BUSYTIMEFLAG="-t" + +else + WAITFORIT_ISBUSY=0 + WAITFORIT_BUSYTIMEFLAG="" +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..43301651fac17dc7a71dd736f9f54259bd56cafc --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*~ +.coverage +__pycache__ +.tox +*.egg-info +venv/ +build +/.env/ +/src/doc/_apidoc/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1944b7062da4fab02dffe185215115e039514d92 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,216 @@ +# +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# Copyright (C) 2019 Henrik tom Wörden +# +# 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/>. + +variables: + CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/linkahead-python-package-template/testenv:$CI_COMMIT_REF_NAME + CI_REGISTRY_IMAGE_BASE: $CI_REGISTRY/caosdb/src/caosdb-pyinttest/base:latest + +stages: + - info + - setup + - cert + - style + - test + - deploy + +.env: &env + - echo "Pipeline triggered by $TRIGGERED_BY_REPO@$TRIGGERED_BY_REF ($TRIGGERED_BY_HASH)" + - echo "CI_REGISTRY_IMAGE_BASE = $CI_REGISTRY_IMAGE_BASE" + - echo "CI_REGISTRY_IMAGE = $CI_REGISTRY_IMAGE" + - echo "CAOSDB_TAG = $CAOSDB_TAG" + - echo "REFTAG = $REFTAG" + - echo "F_BRANCH = $F_BRANCH" + - echo "CI_COMMIT_REF_NAME = $CI_COMMIT_REF_NAME" + - ls -lah /image-cache/ + + - F_BRANCH=${F_BRANCH:-$CI_COMMIT_REF_NAME} + - echo $F_BRANCH + - if [[ "$REFTAG" == "" ]] ; then + if [[ "$F_BRANCH" == "dev" ]] ; then + REFTAG=dev; + fi; + fi + - REFTAG=${REFTAG:-dev_F_${F_BRANCH}} + + - echo $F_BRANCH + + - if [[ "$CAOSDB_TAG" == "" ]]; then + CAOSDB_TAG=${REFTAG}; + fi + - echo $CAOSDB_TAG + +info: + tags: [cached-dind] + image: docker:20.10 + stage: info + needs: [] + script: + - *env + +unittest_py3.11: + tags: [cached-dind] + stage: test + image: $CI_REGISTRY_IMAGE + script: + - python3 -c "import sys; assert sys.version.startswith('3.11')" + - tox + +unittest_py3.8: + tags: [cached-dind] + stage: test + image: python:3.8 + script: &python_test_script + # install dependencies + - pip install pytest pytest-cov + - pip install . + # actual test + - pytest --cov=linkahead_python_package_template -vv ./unittests + +unittest_py3.9: + tags: [cached-dind] + stage: test + image: python:3.11 + script: *python_test_script + +unittest_py3.10: + tags: [cached-dind] + stage: test + image: python:3.10 + script: *python_test_script + +unittest_py3.12: + tags: [cached-dind] + stage: test + image: python:3.12 + script: *python_test_script + +unittest_py3.13: + allow_failure: true + tags: [cached-dind] + stage: test + image: python:3.13-rc + script: *python_test_script + +build-testenv: + tags: [cached-dind] + image: docker:20.10 + stage: setup + timeout: 2h + only: + - schedules + - web + - pushes + needs: [] + script: + - df -h + - command -v wget + - if [ -z "$PYLIB" ]; then + if echo "$CI_COMMIT_REF_NAME" | grep -c "^f-" ; then + echo "Check if pylib has branch $CI_COMMIT_REF_NAME" ; + if wget https://gitlab.indiscale.com/api/v4/projects/97/repository/branches/${CI_COMMIT_REF_NAME} ; then + PYLIB=$CI_COMMIT_REF_NAME ; + fi; + fi; + fi; + - PYLIB=${PYLIB:-dev} + - echo $PYLIB + + - if [ -z "$ADVANCED" ]; then + if echo "$CI_COMMIT_REF_NAME" | grep -c "^f-" ; then + echo "Check if advanced user tools have branch $CI_COMMIT_REF_NAME" ; + if wget https://gitlab.indiscale.com/api/v4/projects/104/repository/branches/${CI_COMMIT_REF_NAME} ; then + ADVANCED=$CI_COMMIT_REF_NAME ; + fi; + fi; + fi; + - ADVANCED=${ADVANCED:-dev} + - echo $ADVANCED + + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + # use here general latest or specific branch latest... + - docker build + --build-arg PYLIB=${PYLIB} + --build-arg ADVANCED=${ADVANCED:dev} + --file .docker/Dockerfile + -t $CI_REGISTRY_IMAGE . + - docker push $CI_REGISTRY_IMAGE + - docker save $CI_REGISTRY_IMAGE > /image-cache/linkahead-python-package-template-testenv-${CI_COMMIT_REF_NAME}.tar + +cert: + tags: [docker] + stage: cert + image: $CI_REGISTRY_IMAGE + needs: + - job: build-testenv + optional: true + artifacts: + paths: + - .docker/cert/ + expire_in: 1 week + script: + - cd .docker + - CAOSHOSTNAME=caosdb-server ./cert.sh + +code-style: + tags: [docker] + stage: style + image: $CI_REGISTRY_IMAGE + needs: + - job: build-testenv + optional: true + script: + - autopep8 -r --diff --exit-code . + allow_failure: true + +pylint: + tags: [docker] + stage: style + image: $CI_REGISTRY_IMAGE + needs: + - job: build-testenv + optional: true + allow_failure: true + script: + - pylint --unsafe-load-any-extension=y -d all -e E,F src/linkahead_python_package_template + +# Build the sphinx documentation and make it ready for deployment by Gitlab Pages +# Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages +# Based on: https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/ci/editor?branch_name=main +pages_prepare: &pages_prepare + tags: [ cached-dind ] + stage: deploy + needs: + - job: build-testenv + image: $CI_REGISTRY_IMAGE + only: + refs: + - /^release-.*$/i + script: + - echo "Deploying documentation" + - make doc + - cp -r build/doc/html public + artifacts: + paths: + - public +pages: + <<: *pages_prepare + only: + refs: + - main diff --git a/README.md b/README.md index ae38611452e0cb2543250ffa398f5cf09e181de9..c37d2e63b2ad4e65ba5524a3fed7116f011e6c39 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,22 @@ This repo contains all the custom code related to the loan management in LinkAhe It is to be checked out as a custom-directory and needs to be specified in your profile.yml. +## Usage +Mount the custom folder by adding it to the `profile.yml`. E.g. +``` + custom: + # Customizations for the demo server are stored here; e.g. the tour + - "./custom" # typical folder for instance specific customization + - "./loan/loan-custom" # adds the loan module +``` + +Also, you need to add the python package to the scripting interface in the +profile: +``` +TODO +``` + + ## Integration tests Run `pytest .` in the integration tests folder diff --git a/integrationtests/box_loan.py b/integrationtests/box_loan.py deleted file mode 120000 index 59146e98ae906b78e437917af8e4e22b868ed636..0000000000000000000000000000000000000000 --- a/integrationtests/box_loan.py +++ /dev/null @@ -1 +0,0 @@ -../loan-custom/caosdb-server/scripting/bin/loan_management/box_loan.py \ No newline at end of file diff --git a/integrationtests/conf.py b/integrationtests/conf.py deleted file mode 120000 index ff726da6bb485ce5add33969b229ddce13950b31..0000000000000000000000000000000000000000 --- a/integrationtests/conf.py +++ /dev/null @@ -1 +0,0 @@ -../loan-custom/caosdb-server/scripting/bin/loan_management/conf.py \ No newline at end of file diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/accept_loan_request.py b/loan-custom/caosdb-server/scripting/bin/loan_management/accept_loan_request.py index c4bc70401eb526130bac037756c350246d2b26a8..55678576e624bc31bef164a23695ccad47195ad1 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/accept_loan_request.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/accept_loan_request.py @@ -2,6 +2,8 @@ # encoding: utf-8 # # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2024 Henrik tom Wörden (h.tomwoerden@indiscale.com) +# Copyright (C) 2024 IndiScale GmbH (info@indiscale.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -20,45 +22,8 @@ """ Accept a loan request. """ -from __future__ import absolute_import -import caosdb as db -from caosadvancedtools.serverside.helper import print_success, get_timestamp -from box_loan import (main, get_loan, F_LOAN, assert_loan_state, - LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED, - get_borrower_names, set_property) - - -def _accept_loan_request(data): - """Update a loan Record and add the `accepted` Property.""" - loan = get_loan(data[F_LOAN]) - assert_loan_state(loan, S_LOAN_REQUESTED) - - # This changes the state from "loan_requested" to "loan_accepted". - set_property(loan, LOAN_ACCEPTED, get_timestamp()) - - # To be sure that it worked: - assert_loan_state(loan, S_LOAN_ACCEPTED) - - db.Container().extend([ - loan - ]).update() - - return loan - - -def accept_loan_request(data): - """Accept a loan request. - - I.e. update the `Loan` Record and add the `accepted` Property. - """ - loan = _accept_loan_request(data) - fn, ln = get_borrower_names(loan) - - print_success('Thank you for accepting the loan request by {fn} {ln}. See ' - '<a href="{loan}" title="Go to the accepted loan ' - 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id)) - return 0 - +from loan.box_loan import main +from loan.accept_loan_request import accept_loan_request if __name__ == "__main__": main(accept_loan_request) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/accept_return_request.py b/loan-custom/caosdb-server/scripting/bin/loan_management/accept_return_request.py index b28a7d4c0aa2a61c0794566d01cb38f8a6a827cd..e3f2826baa63733d72ec2547a9df93b2ef6d04d0 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/accept_return_request.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/accept_return_request.py @@ -2,6 +2,8 @@ # encoding: utf-8 # # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2024 Henrik tom Wörden (h.tomwoerden@indiscale.com) +# Copyright (C) 2024 IndiScale GmbH (info@indiscale.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -20,60 +22,8 @@ """ Accept a return request. """ -# @review Timm Fitschen 2022-03-16 -from __future__ import absolute_import -import caosdb as db -from caosadvancedtools.serverside.helper import print_success, get_timestamp -from box_loan import (BOX_BORROWED, CONTENT, main, get_loan, set_property, - F_LOAN, assert_loan_state, RETURN_ACCEPTED, - RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURN_REQUESTED, - get_borrower_names, set_location) - - -def _accept_return_request(data): - """Update a loan Record and add the `accepted` Property.""" - loan = get_loan(data[F_LOAN]) - assert_loan_state(loan, S_RETURN_REQUESTED) - - # This changes the state from "return_requested" to "return_accepted". - set_property(loan, RETURN_ACCEPTED, get_timestamp()) - - # To be sure that it worked: - assert_loan_state(loan, S_RETURN_ACCEPTED) - box = set_location(loan, RETURNLOCATION, update=False) - if loan.get_property(CONTENT) is not None and loan.get_property(CONTENT).value: - if box.get_property(CONTENT) is not None: - box.get_property(CONTENT).value = loan.get_property(CONTENT).value - else: - box.add_property(id=CONTENT.retrieve().id, - value=loan.get_property(CONTENT).value) - - db.Container().extend([ - box, - loan - ]).update() - - return loan - - -def accept_return_request(data): - """Accept a return request. - - I.e. update the `Loan` Record and add the `returnAccepted` Property. - """ - loan = _accept_return_request(data) - fn, ln = get_borrower_names(loan) - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - - print_success('Thank you for accepting the return request by {fn} {ln}.<br>' - 'If necessary, location and content of the box have been ' - 'updated.<br>' - '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' - 'to view the new Location.<br>' - 'See <a href="{loan}" title="Go to the accepted loan ' - 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, bid=box_id)) - return 0 - +from loan.box_loan import main +from loan.accept_return_request import accept_return_request if __name__ == "__main__": main(accept_return_request) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/confirm_loan.py b/loan-custom/caosdb-server/scripting/bin/loan_management/confirm_loan.py index 4b463b7cad7435d2484e8c9ce5d63585b7dfc339..ae4c05176027115f21f59b413cffe479f5475f0b 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/confirm_loan.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/confirm_loan.py @@ -2,6 +2,8 @@ # encoding: utf-8 # # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2024 Henrik tom Wörden (h.tomwoerden@indiscale.com) +# Copyright (C) 2024 IndiScale GmbH (info@indiscale.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -20,73 +22,8 @@ """ Confirm a loan. """ -from __future__ import absolute_import - -import caosdb as db -from caosadvancedtools.serverside.helper import get_timestamp, print_success - -from box_loan import (BOX, BOX_BORROWED, DESTINATION, F_LOAN, LENT, S_LENT, - S_LOAN_ACCEPTED, assert_loan_state, get_borrower_names, - get_loan, main, set_location, set_property) - - -def _set_lent_box(loan): - """Set the box version to HEAD. - - This stores the version of the box when it was delivered to the borrower. - """ - box_prop = loan.get_property(BOX) - box_prop.name = BOX_BORROWED - box_prop.value = str(box_prop.value) + "@HEAD" - - -def set_loan_location(loan): - """Set the location of the box to the return location of the loan. """ - set_location(loan, DESTINATION) - - -def _confirm_loan(data): - """Update a loan Record and add the `lent` Property.""" - loan = get_loan(data[F_LOAN]) - assert_loan_state(loan, S_LOAN_ACCEPTED) - - # This changes the state from "loan_accepted" to "lent". - set_property(loan, LENT, get_timestamp()) - _set_lent_box(loan) - - # updates the box location - set_loan_location(loan) - - # To be sure that it worked: - assert_loan_state(loan, S_LENT) - - db.Container().extend([ - loan - ]).update() - - return loan - - -def confirm_loan(data): - """Confirm a loan. - - I.e. update the `Loan` Record and add the `lent` Property. - """ - loan = _confirm_loan(data) - fn, ln = get_borrower_names(loan) - loan = get_loan(data[F_LOAN]) - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - print_success('Thank you for confirming the loan request by {fn} {ln}.<br>' - 'The Location of the Box was updated. ' - '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' - 'to view the new Location.<br>' - 'You can also checkout the new loan state ' - '<a href="{loan}" title="Go to the confirmed loan ' - 'record.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, - bid=box_id)) - - return 0 - +from loan.box_loan import main +from loan.confirm_loan import confirm_loan if __name__ == "__main__": main(confirm_loan) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/manual_return.py b/loan-custom/caosdb-server/scripting/bin/loan_management/manual_return.py index 1f6e8e634b6c45198373ea8b1cd08f0613630cf4..f1ed64b7a5cd66e83b4d1871ac6c25f9f643d00c 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/manual_return.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/manual_return.py @@ -2,6 +2,8 @@ # encoding: utf-8 # # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2024 Henrik tom Wörden (h.tomwoerden@indiscale.com) +# Copyright (C) 2024 IndiScale GmbH (info@indiscale.com) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -20,86 +22,8 @@ """ Manually return a box. """ -from __future__ import absolute_import - -import caosdb as db -from caosadvancedtools.serverside.helper import get_timestamp, print_success - -from box_loan import (BOX, BOX_BORROWED, BOX_RETURNED, CONTENT, F_LOAN, - RETURNED, RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURNED, - assert_loan_state, get_borrower_names, get_loan, main, - set_location, set_property) - - -def _set_returned_box(loan): - """Add a `Box (returned)` property to the loan. - - This stores the version of the box that was returned by a borrower. - """ - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - loan.add_property(property=BOX, - name=BOX_RETURNED, - value=box_id + "@HEAD") - - -def set_return_location(loan): - """Set the location of the box to the return location of the loan. """ - set_location(loan, RETURNLOCATION) - - -def set_content(loan): - """Set the content to the box to the one given in the loan. """ - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - box = db.Record(id=box_id).retrieve() - set_property(box, CONTENT, loan.get_property(CONTENT).value) - box.update() - - -def _manual_return(data): - """Update a loan Record and add the `returned` Property.""" - loan = get_loan(data[F_LOAN]) - assert_loan_state(loan, S_RETURN_ACCEPTED) - - # This changes the state from "return_accepted" to "returned". - # TODO why twice???? - set_property(loan, RETURNED, get_timestamp()) - set_property(loan, RETURNED, get_timestamp()) - - # updates the box location and content - # *currently this is not wanted* - # set_return_location(loan) - # set_content(loan) - - # To be sure that it worked: - assert_loan_state(loan, S_RETURNED) - - # add returned box - _set_returned_box(loan) - - db.Container().extend([ - loan - ]).update() - - return loan - - -def manual_return(data): - """Return a box from a borrower. - - I.e. update the `Loan` Record and add the `returned` Property. - """ - loan = _manual_return(data) - fn, ln = get_borrower_names(loan) - loan = get_loan(data[F_LOAN]) - box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] - - print_success('The box borrowed by {fn} {ln} '.format(fn=fn, ln=ln) - + 'has been returned and the Box Location has been updated.' - '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' - 'to view the new Location.<br>'.format(bid=box_id)) - - return 0 - +from loan.box_loan import main +from loan.manual_return import manual_return if __name__ == "__main__": main(manual_return) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/reject_return_request.py b/loan-custom/caosdb-server/scripting/bin/loan_management/reject_return_request.py index 0d8227edc96ec1482d30d44448dfa1788fafac7a..fc7ac40c822b6bd3b7c2c307fdf4a574658506ca 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/reject_return_request.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/reject_return_request.py @@ -20,45 +20,8 @@ """ Reject a return request. """ -from __future__ import absolute_import -import caosdb as db -from caosadvancedtools.serverside.helper import print_success -from box_loan import (main, get_loan, F_LOAN, assert_loan_state, - S_RETURN_REQUESTED, RETURN_REQUESTED, S_LENT, - get_borrower_names) - - -def _reject_return_request(data): - """Update a `Loan` Record and remove the `returnRequested` Property.""" - loan = get_loan(data[F_LOAN]) - assert_loan_state(loan, S_RETURN_REQUESTED) - - # This changes the state from "return_requested" back to "lent". - loan.remove_property(RETURN_REQUESTED) - - # To be sure that it worked: - assert_loan_state(loan, S_LENT) - - db.Container().extend([ - loan - ]).update() - - return loan - - -def reject_return_request(data): - """Reject a return request. - - I.e. update the `Loan` Record and remove the `returnRequested` Property. - """ - loan = _reject_return_request(data) - fn, ln = get_borrower_names(loan) - - print_success('You rejected return request by {fn} {ln}. See ' - '<a href="{loan}" title="Go to the loan record.">' - 'here</a>'.format(fn=fn, ln=ln, loan=loan.id)) - return 0 - +from loan.box_loan import main +from loan.reject_return_request import reject_return_request if __name__ == "__main__": main(reject_return_request) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/request_loan.py b/loan-custom/caosdb-server/scripting/bin/loan_management/request_loan.py index 765eec94f2d047c9aa91178b868b118376703e39..1457a15599d7796a21f6f5b83fcbb19d6834f343 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/request_loan.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/request_loan.py @@ -20,107 +20,8 @@ """ Creates a loan request using information provided by a web formular. """ -from __future__ import absolute_import - -import linkahead as db -from caosadvancedtools.serverside.helper import get_timestamp, print_success - -from box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, - EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION, - F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE, - F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LOAN, - LOAN_REQUESTED, assert_date_in_future, - assert_key_in_data, get_box, insert_or_update_person, main, - send_loan_request_mail) - - -def create_loan(box, borrower, expected_return, exhaust_contents, comment, - destination): - """ Create a new loan record. """ - - loan = db.Record().add_parent(LOAN) - - loan.add_property(BOX, box) - loan.add_property(BORROWER, borrower) - loan.add_property(EXPECTED_RETURN, expected_return) - loan.add_property(EXHAUST_CONTENTS, exhaust_contents) - loan.add_property(COMMENT, comment) - loan.add_property(DESTINATION, destination) - loan.add_property(LOAN_REQUESTED, get_timestamp()) - - return loan - - -_OBLIGATORY = [ - F_EXPECTED_RETURN_DATE, - F_BOX, - F_COMMENT, - F_DESTINATION, - F_EMAIL, - F_LAST_NAME, - F_FIRST_NAME, -] - - -def _check_data(data): - if F_EXHAUST_CONTENTS not in data: - data[F_EXHAUST_CONTENTS] = False - - for field in _OBLIGATORY: - assert_key_in_data(data, field) - assert_date_in_future( - data[F_EXPECTED_RETURN_DATE], - ("The expected return date needs to be in the future." - " You submitted '{}'.").format( - data[F_EXPECTED_RETURN_DATE])) - - return data - - -def _issue_loan_request(data): - """ Insert a loan record a insert/update a person record. """ - data = _check_data(data) - borrower = insert_or_update_person(firstname=data[F_FIRST_NAME], - lastname=data[F_LAST_NAME], - email=data[F_EMAIL]) - loan = create_loan(box=data[F_BOX], - borrower=borrower, - expected_return=data[F_EXPECTED_RETURN_DATE], - exhaust_contents=data[F_EXHAUST_CONTENTS], - comment=data[F_COMMENT], - destination=data[F_DESTINATION]) - loan.insert() - - return borrower, loan - - -def issue_loan_request(data): - """ Issue a loan request. - - I.e. inserting Record with parent `Loan` which has the following - properties: - * borrower - * expectedReturn - * exhaustContents - * Box - * comment - * destination - - The borrower is a Person Record, with firstName, lastName and email, - identified by either email, oder firstName+lastName. - """ - borrower, loan = _issue_loan_request(data) - - fn = borrower.get_property(FIRST_NAME.name).value - ln = borrower.get_property(LAST_NAME.name).value - print_success('Thank you for your loan request, {fn} {ln}. See ' - '<a href="{loan}" title="Go to the newly created loan ' - 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id)) - - send_loan_request_mail(data, borrower, loan) - - return 0 - +from loan.box_loan import main +from loan.request_loan import issue_loan_request if __name__ == "__main__": main(issue_loan_request) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/request_return.py b/loan-custom/caosdb-server/scripting/bin/loan_management/request_return.py index 5c78ff5a26d06fb1b2fd988e52b001c806f87eaa..55bccb08508d85fe180eb7b4350d03fe13c81e8f 100755 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/request_return.py +++ b/loan-custom/caosdb-server/scripting/bin/loan_management/request_return.py @@ -22,72 +22,8 @@ Creates a return request using information provided by a web formular. A return request is a loan record with a returnRequested property. """ - - -from __future__ import absolute_import - -from caosadvancedtools.serverside.helper import get_timestamp, print_success - -from box_loan import (BORROWER, COMMENT, CONTENT, EXPECTED_RETURN, F_COMMENT, - F_CURRENT_LOCATION, F_EMAIL, F_EXPECTED_RETURN_DATE, - F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, LAST_NAME, - RETURN_REQUESTED, RETURNLOCATION, S_LENT, - assert_date_in_future, assert_key_in_data, - assert_loan_state, get_loan, insert_or_update_person, main, - send_return_request_mail, set_property) - - -def _check_data(data): - assert_key_in_data(data, F_EXPECTED_RETURN_DATE) - assert_key_in_data(data, F_LOAN) - assert_key_in_data(data, F_COMMENT) - assert_key_in_data(data, F_CURRENT_LOCATION) - - assert_date_in_future( - data[F_EXPECTED_RETURN_DATE], - ("The expected return date needs to be in the future." - " You submitted '{}'.").format( - data[F_EXPECTED_RETURN_DATE])) - - -def _issue_return_request(data): - _check_data(data) - loan = get_loan(data[F_LOAN]) - assert_loan_state(loan, S_LENT) - - returner = insert_or_update_person(data[F_FIRST_NAME], data[F_LAST_NAME], data[F_EMAIL]) - loan.add_property(RETURN_REQUESTED, get_timestamp()) - - set_property(loan, BORROWER, returner.id) - set_property(loan, EXPECTED_RETURN, data[F_EXPECTED_RETURN_DATE]) - set_property(loan, CONTENT, data[F_COMMENT]) - set_property(loan, RETURNLOCATION, data[F_CURRENT_LOCATION]) - - loan.update() - - return returner, loan - - -def issue_return_request(data): - """ Issue a return request. - - A return request is a Loan record where a returnRequested property is being - added. - """ - returner, loan = _issue_return_request(data) - - # print - fn = returner.get_property(FIRST_NAME.name).value - ln = returner.get_property(LAST_NAME.name).value - print_success( - 'Thank you for your return request, {fn} {ln}.'.format( - fn=fn, ln=ln)) - - # send an email and inform the curator about the return request - send_return_request_mail(data, returner, loan) - - return 0 - +from loan.box_loan import main +from loan.request_return import issue_return_request if __name__ == "__main__": main(issue_return_request) diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/box_loan.py b/loan-custom/caosdb-server/scripting/bin/test/loan_management/box_loan.py deleted file mode 120000 index 034b4c7dfca3b7d5c0e6ac80ad830c176658934a..0000000000000000000000000000000000000000 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/box_loan.py +++ /dev/null @@ -1 +0,0 @@ -../../loan_management/box_loan.py \ No newline at end of file diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/conf.py b/loan-custom/caosdb-server/scripting/bin/test/loan_management/conf.py deleted file mode 120000 index dfe16033970e733d38f4eeb25837bdfdb6b4a66d..0000000000000000000000000000000000000000 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/conf.py +++ /dev/null @@ -1 +0,0 @@ -../../loan_management/conf.py \ No newline at end of file diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/manual_return.py b/loan-custom/caosdb-server/scripting/bin/test/loan_management/manual_return.py deleted file mode 120000 index f0eab4f92842389daf8a57e71480f8238d93e421..0000000000000000000000000000000000000000 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/manual_return.py +++ /dev/null @@ -1 +0,0 @@ -../../loan_management/manual_return.py \ No newline at end of file diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_loan.py b/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_loan.py deleted file mode 120000 index 0c060509fbb552f9e92d8b890acbbdb1db3e2e7c..0000000000000000000000000000000000000000 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_loan.py +++ /dev/null @@ -1 +0,0 @@ -../../loan_management/request_loan.py \ No newline at end of file diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_return.py b/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_return.py deleted file mode 120000 index b080e7193350a231c118aa9d493966ca5ffe45d8..0000000000000000000000000000000000000000 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_return.py +++ /dev/null @@ -1 +0,0 @@ -../../loan_management/request_return.py \ No newline at end of file diff --git a/loanpy/CHANGELOG.md b/loanpy/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..dd4609543d8988a0a2bf0f35846a1cc80beb55f1 --- /dev/null +++ b/loanpy/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] ## + +### Added ### + +### Changed ### + +### Deprecated ### + +### Removed ### + +### Fixed ### + +### Security ### + +### Documentation ### diff --git a/loanpy/LICENSE b/loanpy/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ce8d9f20bb5ddc2a66a21364acadb90e1c5abcea --- /dev/null +++ b/loanpy/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Check SFS + Copyright (C) 2020 Alexander Schlemmer + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<http://www.gnu.org/licenses/>. diff --git a/loanpy/Makefile b/loanpy/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..b8bc521c7f9046dbcbea56c08a26903ba924acab --- /dev/null +++ b/loanpy/Makefile @@ -0,0 +1,48 @@ +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Daniel Hornung <d.hornung@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/>. +# +# ** end header + +# This Makefile is a wrapper for several other scripts. + +.PHONY: help + +help: + @echo 'Type `make doc` for documentation, or `make install` for (local) installation.' + +doc: + $(MAKE) -C src/doc html + +install: + @echo "Not implemented yet, use pip for installation." + +check: style lint +.PHONY: check + +style: + pycodestyle --count src unittests +.PHONY: style + +lint: + pylint --unsafe-load-any-extension=y -d all -e E,F src/linkahead_python_package_template +.PHONY: lint + +unittest: + tox -r +.PHONY: unittest diff --git a/loanpy/README.md b/loanpy/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4e457007fa2dc3a3b3d241183ef7d2e7e5b69e79 --- /dev/null +++ b/loanpy/README.md @@ -0,0 +1,50 @@ +# LinkAhead Python Package Template + +This Repo serves as a template for LinkAhead related Python packages, e.g., +custom crawler converters. + +## Usage + +To create a new Python package, fork this repo and change the names in the `src` +directory, `pyproject.toml`, `Makefile`, `src/doc/Makefile`, `src/doc/conf.py` +and `tox.ini`. Also edit the `authors` field therein. If applicable, create a +`citation.cff` for the new project. + +Also update this `README.md` to contain actual package information including +license, copyright, conttributors, etc. See [Pylib's +README.md](https://gitlab.com/linkahead/linkahead-pylib/-/blob/main/README.md?ref_type=heads) +as an example. + +Then, write your Python code in `/src/<package_name>`, and add unit tests in +`unittests`. Documentation goes to `/src/doc`. + +If you want to publish the result on PyPi, there is a release.sh helper +script. Make sure that your `pyproject.toml` contains the correct and complete +metadata and check the `RELEASE_GUIDELINES.md` + +### Unit Tests + +Run `tox` or alternatively `pytest unittests/`. + +### Code style and liniting + +Run `make style lint` after installing the dependencies listed below. + +### Documentation + +Run `make doc` after installing the dependencies listed below. + +## Dependencies + +Package and optional dependencies are declared in the `pyproject.toml`; +additional dependencies for testing are listed in the `tox.ini`. + +For linting and code-style we additionally require + +- `pylint` + +For building the documentation we require + +- `sphinx` +- `recommonmark` +- `sphinx-rtd-theme` diff --git a/loanpy/RELEASE_GUIDELINES.md b/loanpy/RELEASE_GUIDELINES.md new file mode 100644 index 0000000000000000000000000000000000000000..46ef6a8d31643281e46ee7139b2fb81e1e221215 --- /dev/null +++ b/loanpy/RELEASE_GUIDELINES.md @@ -0,0 +1,47 @@ +# Release Guidelines for the CaosDB Python Client Library + +This document specifies release guidelines in addition to the general release +guidelines of the CaosDB Project +([RELEASE_GUIDELINES.md](https://gitlab.com/caosdb/caosdb/blob/dev/RELEASE_GUIDELINES.md)) + +## General Prerequisites + +* All tests are passing. +* FEATURES.md is up-to-date and a public API is being declared in that document. +* CHANGELOG.md is up-to-date. +* dependencies in `setup.cfg` are up-to-date. + +## Steps + +1. Create a release branch from the dev branch. This prevents further changes + to the code base and a never ending release process. Naming: `release-<VERSION>` + +2. Update CHANGELOG.md + +3. Check all general prerequisites. + +4. Update the version: + - `version` variables in `src/doc/conf.py` + - Version in [pyproject.toml](./pyproject.toml). + - `CITATION.cff` (update version and date) + +5. Merge the release branch into the main branch. + +6. Tag the latest commit of the main branch with `v<VERSION>`. + +7. Delete the release branch. + +8. Remove possibly existing `./dist` directory with old release. + +9. Publish the release by executing `./release.sh` with uploads the caosdb + module to the Python Package Index [pypi.org](https://pypi.org). + +10. Merge the main branch back into the dev branch. + +11. After the merge of main to dev, start a new development version by + increasing at least the micro version in [pyproject.toml](./pyproject.toml) + and preparing CHANGELOG.md. + +12. Create releases on gitlab.com and gitlab.indiscale.com that contain (at + least) the most recent section of the CHANGELOG as the description and link + to the PyPi package. diff --git a/integrationtests/basic_test.py b/loanpy/integrationtests/basic_test.py similarity index 99% rename from integrationtests/basic_test.py rename to loanpy/integrationtests/basic_test.py index 511c1876987a428a660cbb6f3bb79dd300b4d677..5fda6aa0eb00c47e8124fe79476f6463776a510a 100644 --- a/integrationtests/basic_test.py +++ b/loanpy/integrationtests/basic_test.py @@ -26,7 +26,7 @@ import linkahead as db import re from tempfile import NamedTemporaryFile from linkahead.utils.server_side_scripting import run_server_side_script -from box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, LENT, +from loan.box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, LENT, F_CURRENT_LOCATION, EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION,PERSON, LOAN_ACCEPTED, F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE, EMAIL, F_LOAN, diff --git a/loanpy/pyproject.toml b/loanpy/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..75f26a64a344fcea92503b789abdf74b909b593d --- /dev/null +++ b/loanpy/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "linkahead-sss-loanpy" +description = "Server side scripting part of the loan management in LinkAhead" +version = "0.0.1" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name = "Henrik tom Wörden", email="h.tomwoerden@indiscale.com"}, + {name = "Florian Spreckelsen", email= "f.spreckelsen@indiscale.com"} +] +maintainers = [ + {name = "Henrik tom Wörden", email="h.tomwoerden@indiscale.com"} +] +keywords = ["Data management", "Research data management"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Operating System :: OS Independent", +] +requires-python = ">= 3.8" +dependencies = [ + "linkahead" +] + +[project.urls] +Homepage = "https://getlinkahead.com" +Documentation = "https://docs.indiscale.com" +Repository = "https://gitlab.indiscale.com/caosdb/src/linkahead-loan" +Issues = "https://gitlab.indiscale.com/caosdb/src/linkahead-loan/-/issues" +Changelog = "https://gitlab.indiscale.com/caosdb/src/linkahead-loan/-/blob/main/CHANGELOG.md?ref_type=heads" diff --git a/loanpy/release.sh b/loanpy/release.sh new file mode 100755 index 0000000000000000000000000000000000000000..f6335ae20d0c29e760b508aac831a35460a59ef3 --- /dev/null +++ b/loanpy/release.sh @@ -0,0 +1,4 @@ +#!/bin/bash +rm -rf dist/ build/ .eggs/ +python setup.py sdist bdist_wheel +python -m twine upload dist/* diff --git a/loanpy/src/doc/Makefile b/loanpy/src/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..6f4670d0e922026e200840034b8df2d9cb5ab9c2 --- /dev/null +++ b/loanpy/src/doc/Makefile @@ -0,0 +1,47 @@ +# This file is a part of the LinkAhead Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Daniel Hornung <d.hornung@indiscale.com> +# Copyright (C) 2021 Alexander Schlemmer <alexander.schlemmer@ds.mpg.de> +# +# 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/>. +# + +# This Makefile is a wrapper for sphinx scripts. +# +# It is based upon the autocreated makefile for Sphinx documentation. + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -a +SPHINXBUILD ?= sphinx-build +SPHINXAPIDOC ?= sphinx-apidoc +PY_BASEDIR = ../linkahead_python_package_template +SOURCEDIR = . +BUILDDIR = ../../build/doc + + +.PHONY: doc-help Makefile + +# Put it first so that "make" without argument is like "make help". +doc-help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile apidoc + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +apidoc: + @$(SPHINXAPIDOC) -o _apidoc --separate $(PY_BASEDIR) diff --git a/loanpy/src/doc/conf.py b/loanpy/src/doc/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..cfd8a066f58516d6c7df8d580ab582cff68e94f5 --- /dev/null +++ b/loanpy/src/doc/conf.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# Based on the configuration for caosdb-pylib. +# +# # Copyright (C) 2021 Alexander Schlemmer <alexander.schlemmer@ds.mpg.de> +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, add these +# directories to sys.path here. This is particularly necessary if this package is installed at a +# different version, for example via `pip install`. +# +# If the directory is relative to the documentation root, use os.path.abspath to make it absolute, +# like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +import sphinx_rtd_theme # noqa: E402 + + +# -- Project information ----------------------------------------------------- + +project = 'caosdb-caoscrawler' +copyright = '2024, IndiScale' +author = 'Your Name' + +# The short X.Y version +version = '0.0.1' +# The full version, including alpha/beta/rc tags +# release = '0.5.2-rc2' +release = '0.0.1-dev' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosectionlabel', + 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', # For Google style docstrings + "recommonmark", # For markdown files. + "sphinx_rtd_theme", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# + +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] # ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'caosdb-caoscrawlerdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'caosdb-caoscrawler.tex', 'caosdb-caoscrawler Documentation', + 'MPIDS', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'caosdb-caoscrawler', 'caosdb-caoscrawler documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'caosdb-caoscrawler', 'caosdb-caoscrawler documentation', + author, 'caosdb-caoscrawler', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# True to prefix each section label with the name of the document it is in, followed by a colon. For +# example, index:Introduction for a section called Introduction that appears in document +# index.rst. Useful for avoiding ambiguity when the same section heading appears in different +# documents. +# +# Note: This stops "normal" links from working, so it should be kept at False. +# autosectionlabel_prefix_document = True + +# -- Options for intersphinx ------------------------------------------------- + +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping +intersphinx_mapping = { + "python": ("https://docs.python.org/", None), + "caosdb-mysqlbackend": ("https://docs.indiscale.com/caosdb-mysqlbackend/", + None), + "caosdb-server": ("https://docs.indiscale.com/caosdb-server/", None), + "caosdb-pylib": ("https://docs.indiscale.com/caosdb-pylib/", None), + "caosdb-advanced-user-tools": ("https://docs.indiscale.com/caosdb-advanced-user-tools/", None), +} + + +# TODO Which options do we want? +autodoc_default_options = { + 'members': None, + 'undoc-members': None, +} diff --git a/loanpy/src/doc/index.rst b/loanpy/src/doc/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..6756f30810e3f4a3aaedc9b6b9a2852bdb4161c6 --- /dev/null +++ b/loanpy/src/doc/index.rst @@ -0,0 +1,4 @@ +LinkkAhead Python Package Template +================================== + +**TODO**: Write your documentation here. diff --git a/loanpy/src/loan/__init__.py b/loanpy/src/loan/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..566fc50b34bb879f43543ee8ecc5415bbe86a8d4 --- /dev/null +++ b/loanpy/src/loan/__init__.py @@ -0,0 +1,18 @@ +# +# This file is a part of the LinkAhead Project. +# +# Copyright (C) 2024 IndiScale GmbH <info@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# diff --git a/loanpy/src/loan/accept_loan_request.py b/loanpy/src/loan/accept_loan_request.py new file mode 100755 index 0000000000000000000000000000000000000000..7e57f94c979e6d439531f2a8d0c44d01500969a4 --- /dev/null +++ b/loanpy/src/loan/accept_loan_request.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Accept a loan request. +""" +from __future__ import absolute_import +import caosdb as db +from caosadvancedtools.serverside.helper import print_success, get_timestamp +from .box_loan import (main, get_loan, F_LOAN, assert_loan_state, + LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED, + get_borrower_names, set_property) + + +def _accept_loan_request(data): + """Update a loan Record and add the `accepted` Property.""" + loan = get_loan(data[F_LOAN]) + assert_loan_state(loan, S_LOAN_REQUESTED) + + # This changes the state from "loan_requested" to "loan_accepted". + set_property(loan, LOAN_ACCEPTED, get_timestamp()) + + # To be sure that it worked: + assert_loan_state(loan, S_LOAN_ACCEPTED) + + db.Container().extend([ + loan + ]).update() + + return loan + + +def accept_loan_request(data): + """Accept a loan request. + + I.e. update the `Loan` Record and add the `accepted` Property. + """ + loan = _accept_loan_request(data) + fn, ln = get_borrower_names(loan) + + print_success('Thank you for accepting the loan request by {fn} {ln}. See ' + '<a href="{loan}" title="Go to the accepted loan ' + 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id)) + return 0 + + +if __name__ == "__main__": + main(accept_loan_request) diff --git a/loanpy/src/loan/accept_return_request.py b/loanpy/src/loan/accept_return_request.py new file mode 100755 index 0000000000000000000000000000000000000000..05619d9d5d6b524f15e981b4742cbeb72750c1e9 --- /dev/null +++ b/loanpy/src/loan/accept_return_request.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Accept a return request. +""" +# @review Timm Fitschen 2022-03-16 +from __future__ import absolute_import +import caosdb as db +from caosadvancedtools.serverside.helper import print_success, get_timestamp +from .box_loan import (BOX_BORROWED, CONTENT, main, get_loan, set_property, + F_LOAN, assert_loan_state, RETURN_ACCEPTED, + RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURN_REQUESTED, + get_borrower_names, set_location) + + +def _accept_return_request(data): + """Update a loan Record and add the `accepted` Property.""" + loan = get_loan(data[F_LOAN]) + assert_loan_state(loan, S_RETURN_REQUESTED) + + # This changes the state from "return_requested" to "return_accepted". + set_property(loan, RETURN_ACCEPTED, get_timestamp()) + + # To be sure that it worked: + assert_loan_state(loan, S_RETURN_ACCEPTED) + box = set_location(loan, RETURNLOCATION, update=False) + if loan.get_property(CONTENT) is not None and loan.get_property(CONTENT).value: + if box.get_property(CONTENT) is not None: + box.get_property(CONTENT).value = loan.get_property(CONTENT).value + else: + box.add_property(id=CONTENT.retrieve().id, + value=loan.get_property(CONTENT).value) + + db.Container().extend([ + box, + loan + ]).update() + + return loan + + +def accept_return_request(data): + """Accept a return request. + + I.e. update the `Loan` Record and add the `returnAccepted` Property. + """ + loan = _accept_return_request(data) + fn, ln = get_borrower_names(loan) + box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + + print_success('Thank you for accepting the return request by {fn} {ln}.<br>' + 'If necessary, location and content of the box have been ' + 'updated.<br>' + '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' + 'to view the new Location.<br>' + 'See <a href="{loan}" title="Go to the accepted loan ' + 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, bid=box_id)) + return 0 + + +if __name__ == "__main__": + main(accept_return_request) diff --git a/loan-custom/caosdb-server/scripting/bin/loan_management/box_loan.py b/loanpy/src/loan/box_loan.py similarity index 99% rename from loan-custom/caosdb-server/scripting/bin/loan_management/box_loan.py rename to loanpy/src/loan/box_loan.py index 7027e98e969a688bbec139ba5a6f5bbef0ef1f1c..b6dbad3e2760915d29c44eb0dac9d089c18163a6 100644 --- a/loan-custom/caosdb-server/scripting/bin/loan_management/box_loan.py +++ b/loanpy/src/loan/box_loan.py @@ -36,7 +36,7 @@ from caosadvancedtools.serverside.helper import (DataModelError, get_data, from caosadvancedtools.serverside.logging import configure_server_side_logging from linkahead.exceptions import EmptyUniqueQueryError from validate_email import validate_email -from conf import * +from .conf import * LOGGER_NAME = "box_loan" LOGGER = logging.getLogger(LOGGER_NAME) diff --git a/loanpy/src/loan/conf.py b/loanpy/src/loan/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..fcfb1654f3df9bcfa8270ea3cc6d910860e5e383 --- /dev/null +++ b/loanpy/src/loan/conf.py @@ -0,0 +1,28 @@ +import linkahead as db +# RecordTypes +BOX = db.RecordType(name="Box") +PERSON = db.RecordType(name="Person") +LOAN = db.RecordType(name="Loan") +# Properties +FIRST_NAME = db.Property(name="firstName", datatype=db.TEXT) +LAST_NAME = db.Property(name="lastName", datatype=db.TEXT) +EMAIL = db.Property(name="email", datatype=db.TEXT) +LOCATION = db.RecordType(name="Location") +DESTINATION = db.Property(name="LoanLocation", datatype="Location") +RETURNLOCATION = db.Property(name="ReturnLocation", datatype="Location") +COMMENT = db.Property(name="comment", datatype=db.TEXT) +EXHAUST_CONTENTS = db.Property(name="exhaustContents", datatype=db.BOOLEAN) +BORROWER = db.Property(name="Borrower", datatype=PERSON.name) +CONTENT = db.Property(name="Content", datatype=db.TEXT) +LOAN_REQUESTED = db.Property(name="loanRequested", datatype=db.DATETIME) +EXPECTED_RETURN = db.Property(name="expectedReturn", datatype=db.DATETIME) +LOAN_ACCEPTED = db.Property(name="loanAccepted", datatype=db.DATETIME) +LENT = db.Property(name="lent", datatype=db.DATETIME) +RETURN_REQUESTED = db.Property(name="returnRequested", datatype=db.DATETIME) +RETURN_ACCEPTED = db.Property(name="returnAccepted", datatype=db.DATETIME) +RETURNED = db.Property(name="returned", datatype=db.DATETIME) +BOX_NUMBER = db.Property(name="Number", datatype=db.TEXT) + +# Other Strings +BOX_RETURNED = "Box (returned)" +BOX_BORROWED = "Box (borrowed)" diff --git a/loanpy/src/loan/confirm_loan.py b/loanpy/src/loan/confirm_loan.py new file mode 100755 index 0000000000000000000000000000000000000000..cf4fab52dba066cd0856fb225736f02e517d0043 --- /dev/null +++ b/loanpy/src/loan/confirm_loan.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Confirm a loan. +""" +from __future__ import absolute_import + +import caosdb as db +from caosadvancedtools.serverside.helper import get_timestamp, print_success + +from .box_loan import (BOX, BOX_BORROWED, DESTINATION, F_LOAN, LENT, S_LENT, + S_LOAN_ACCEPTED, assert_loan_state, get_borrower_names, + get_loan, main, set_location, set_property) + + +def _set_lent_box(loan): + """Set the box version to HEAD. + + This stores the version of the box when it was delivered to the borrower. + """ + box_prop = loan.get_property(BOX) + box_prop.name = BOX_BORROWED + box_prop.value = str(box_prop.value) + "@HEAD" + + +def set_loan_location(loan): + """Set the location of the box to the return location of the loan. """ + set_location(loan, DESTINATION) + + +def _confirm_loan(data): + """Update a loan Record and add the `lent` Property.""" + loan = get_loan(data[F_LOAN]) + assert_loan_state(loan, S_LOAN_ACCEPTED) + + # This changes the state from "loan_accepted" to "lent". + set_property(loan, LENT, get_timestamp()) + _set_lent_box(loan) + + # updates the box location + set_loan_location(loan) + + # To be sure that it worked: + assert_loan_state(loan, S_LENT) + + db.Container().extend([ + loan + ]).update() + + return loan + + +def confirm_loan(data): + """Confirm a loan. + + I.e. update the `Loan` Record and add the `lent` Property. + """ + loan = _confirm_loan(data) + fn, ln = get_borrower_names(loan) + loan = get_loan(data[F_LOAN]) + box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + print_success('Thank you for confirming the loan request by {fn} {ln}.<br>' + 'The Location of the Box was updated. ' + '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' + 'to view the new Location.<br>' + 'You can also checkout the new loan state ' + '<a href="{loan}" title="Go to the confirmed loan ' + 'record.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, + bid=box_id)) + + return 0 + + +if __name__ == "__main__": + main(confirm_loan) diff --git a/loanpy/src/loan/manual_return.py b/loanpy/src/loan/manual_return.py new file mode 100755 index 0000000000000000000000000000000000000000..0505088f44f5a33a6b8c6c81d3baf14629da4729 --- /dev/null +++ b/loanpy/src/loan/manual_return.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Manually return a box. +""" +from __future__ import absolute_import + +import caosdb as db +from caosadvancedtools.serverside.helper import get_timestamp, print_success + +from .box_loan import (BOX, BOX_BORROWED, BOX_RETURNED, CONTENT, F_LOAN, + RETURNED, RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURNED, + assert_loan_state, get_borrower_names, get_loan, main, + set_location, set_property) + + +def _set_returned_box(loan): + """Add a `Box (returned)` property to the loan. + + This stores the version of the box that was returned by a borrower. + """ + box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + loan.add_property(property=BOX, + name=BOX_RETURNED, + value=box_id + "@HEAD") + + +def set_return_location(loan): + """Set the location of the box to the return location of the loan. """ + set_location(loan, RETURNLOCATION) + + +def set_content(loan): + """Set the content to the box to the one given in the loan. """ + box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + box = db.Record(id=box_id).retrieve() + set_property(box, CONTENT, loan.get_property(CONTENT).value) + box.update() + + +def _manual_return(data): + """Update a loan Record and add the `returned` Property.""" + loan = get_loan(data[F_LOAN]) + assert_loan_state(loan, S_RETURN_ACCEPTED) + + # This changes the state from "return_accepted" to "returned". + # TODO why twice???? + set_property(loan, RETURNED, get_timestamp()) + set_property(loan, RETURNED, get_timestamp()) + + # updates the box location and content + # *currently this is not wanted* + # set_return_location(loan) + # set_content(loan) + + # To be sure that it worked: + assert_loan_state(loan, S_RETURNED) + + # add returned box + _set_returned_box(loan) + + db.Container().extend([ + loan + ]).update() + + return loan + + +def manual_return(data): + """Return a box from a borrower. + + I.e. update the `Loan` Record and add the `returned` Property. + """ + loan = _manual_return(data) + fn, ln = get_borrower_names(loan) + loan = get_loan(data[F_LOAN]) + box_id = loan.get_property(BOX_BORROWED).value.split("@")[0] + + print_success('The box borrowed by {fn} {ln} '.format(fn=fn, ln=ln) + + 'has been returned and the Box Location has been updated.' + '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> ' + 'to view the new Location.<br>'.format(bid=box_id)) + + return 0 + + +if __name__ == "__main__": + main(manual_return) diff --git a/loanpy/src/loan/reject_return_request.py b/loanpy/src/loan/reject_return_request.py new file mode 100755 index 0000000000000000000000000000000000000000..58d2349055bf0870de257b6a1a250e18da24a65a --- /dev/null +++ b/loanpy/src/loan/reject_return_request.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Reject a return request. +""" +from __future__ import absolute_import +import caosdb as db +from caosadvancedtools.serverside.helper import print_success +from .box_loan import (main, get_loan, F_LOAN, assert_loan_state, + S_RETURN_REQUESTED, RETURN_REQUESTED, S_LENT, + get_borrower_names) + + +def _reject_return_request(data): + """Update a `Loan` Record and remove the `returnRequested` Property.""" + loan = get_loan(data[F_LOAN]) + assert_loan_state(loan, S_RETURN_REQUESTED) + + # This changes the state from "return_requested" back to "lent". + loan.remove_property(RETURN_REQUESTED) + + # To be sure that it worked: + assert_loan_state(loan, S_LENT) + + db.Container().extend([ + loan + ]).update() + + return loan + + +def reject_return_request(data): + """Reject a return request. + + I.e. update the `Loan` Record and remove the `returnRequested` Property. + """ + loan = _reject_return_request(data) + fn, ln = get_borrower_names(loan) + + print_success('You rejected return request by {fn} {ln}. See ' + '<a href="{loan}" title="Go to the loan record.">' + 'here</a>'.format(fn=fn, ln=ln, loan=loan.id)) + return 0 + + +if __name__ == "__main__": + main(reject_return_request) diff --git a/loanpy/src/loan/request_loan.py b/loanpy/src/loan/request_loan.py new file mode 100755 index 0000000000000000000000000000000000000000..723f569c784f4af9b54ea3dce131ba3f09b58971 --- /dev/null +++ b/loanpy/src/loan/request_loan.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Creates a loan request using information provided by a web formular. +""" +from __future__ import absolute_import + +import linkahead as db +from caosadvancedtools.serverside.helper import get_timestamp, print_success + +from .box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, + EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION, + F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE, + F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LOAN, + LOAN_REQUESTED, assert_date_in_future, + assert_key_in_data, get_box, insert_or_update_person, main, + send_loan_request_mail) + + +def create_loan(box, borrower, expected_return, exhaust_contents, comment, + destination): + """ Create a new loan record. """ + + loan = db.Record().add_parent(LOAN) + + loan.add_property(BOX, box) + loan.add_property(BORROWER, borrower) + loan.add_property(EXPECTED_RETURN, expected_return) + loan.add_property(EXHAUST_CONTENTS, exhaust_contents) + loan.add_property(COMMENT, comment) + loan.add_property(DESTINATION, destination) + loan.add_property(LOAN_REQUESTED, get_timestamp()) + + return loan + + +_OBLIGATORY = [ + F_EXPECTED_RETURN_DATE, + F_BOX, + F_COMMENT, + F_DESTINATION, + F_EMAIL, + F_LAST_NAME, + F_FIRST_NAME, +] + + +def _check_data(data): + if F_EXHAUST_CONTENTS not in data: + data[F_EXHAUST_CONTENTS] = False + + for field in _OBLIGATORY: + assert_key_in_data(data, field) + assert_date_in_future( + data[F_EXPECTED_RETURN_DATE], + ("The expected return date needs to be in the future." + " You submitted '{}'.").format( + data[F_EXPECTED_RETURN_DATE])) + + return data + + +def _issue_loan_request(data): + """ Insert a loan record a insert/update a person record. """ + data = _check_data(data) + borrower = insert_or_update_person(firstname=data[F_FIRST_NAME], + lastname=data[F_LAST_NAME], + email=data[F_EMAIL]) + loan = create_loan(box=data[F_BOX], + borrower=borrower, + expected_return=data[F_EXPECTED_RETURN_DATE], + exhaust_contents=data[F_EXHAUST_CONTENTS], + comment=data[F_COMMENT], + destination=data[F_DESTINATION]) + loan.insert() + + return borrower, loan + + +def issue_loan_request(data): + """ Issue a loan request. + + I.e. inserting Record with parent `Loan` which has the following + properties: + * borrower + * expectedReturn + * exhaustContents + * Box + * comment + * destination + + The borrower is a Person Record, with firstName, lastName and email, + identified by either email, oder firstName+lastName. + """ + borrower, loan = _issue_loan_request(data) + + fn = borrower.get_property(FIRST_NAME.name).value + ln = borrower.get_property(LAST_NAME.name).value + print_success('Thank you for your loan request, {fn} {ln}. See ' + '<a href="{loan}" title="Go to the newly created loan ' + 'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id)) + + send_loan_request_mail(data, borrower, loan) + + return 0 + + +if __name__ == "__main__": + main(issue_loan_request) diff --git a/loanpy/src/loan/request_return.py b/loanpy/src/loan/request_return.py new file mode 100755 index 0000000000000000000000000000000000000000..011f85aa206395a7c9a844a56e3184ca5157b081 --- /dev/null +++ b/loanpy/src/loan/request_return.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@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/>. +# +# +""" +Creates a return request using information provided by a web formular. + +A return request is a loan record with a returnRequested property. +""" + + +from __future__ import absolute_import + +from caosadvancedtools.serverside.helper import get_timestamp, print_success + +from .box_loan import (BORROWER, COMMENT, CONTENT, EXPECTED_RETURN, F_COMMENT, + F_CURRENT_LOCATION, F_EMAIL, F_EXPECTED_RETURN_DATE, + F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, LAST_NAME, + RETURN_REQUESTED, RETURNLOCATION, S_LENT, + assert_date_in_future, assert_key_in_data, + assert_loan_state, get_loan, insert_or_update_person, main, + send_return_request_mail, set_property) + + +def _check_data(data): + assert_key_in_data(data, F_EXPECTED_RETURN_DATE) + assert_key_in_data(data, F_LOAN) + assert_key_in_data(data, F_COMMENT) + assert_key_in_data(data, F_CURRENT_LOCATION) + + assert_date_in_future( + data[F_EXPECTED_RETURN_DATE], + ("The expected return date needs to be in the future." + " You submitted '{}'.").format( + data[F_EXPECTED_RETURN_DATE])) + + +def _issue_return_request(data): + _check_data(data) + loan = get_loan(data[F_LOAN]) + assert_loan_state(loan, S_LENT) + + returner = insert_or_update_person(data[F_FIRST_NAME], data[F_LAST_NAME], data[F_EMAIL]) + loan.add_property(RETURN_REQUESTED, get_timestamp()) + + set_property(loan, BORROWER, returner.id) + set_property(loan, EXPECTED_RETURN, data[F_EXPECTED_RETURN_DATE]) + set_property(loan, CONTENT, data[F_COMMENT]) + set_property(loan, RETURNLOCATION, data[F_CURRENT_LOCATION]) + + loan.update() + + return returner, loan + + +def issue_return_request(data): + """ Issue a return request. + + A return request is a Loan record where a returnRequested property is being + added. + """ + returner, loan = _issue_return_request(data) + + # print + fn = returner.get_property(FIRST_NAME.name).value + ln = returner.get_property(LAST_NAME.name).value + print_success( + 'Thank you for your return request, {fn} {ln}.'.format( + fn=fn, ln=ln)) + + # send an email and inform the curator about the return request + send_return_request_mail(data, returner, loan) + + return 0 + + +if __name__ == "__main__": + main(issue_return_request) diff --git a/loanpy/tox.ini b/loanpy/tox.ini new file mode 100644 index 0000000000000000000000000000000000000000..d72772249f9487943e771fe0012bdbb9b62eef8d --- /dev/null +++ b/loanpy/tox.ini @@ -0,0 +1,21 @@ +[tox] +envlist = py38, py39, py310, py311, py312, py313 +skip_missing_interpreters = true + +[testenv] +deps = . + pytest + pytest-cov + +commands = + py.test --cov=linkahead_python_package_template -vv {posargs} + +[flake8] +max-line-length = 100 + +[pycodestyle] +max-line-length = 100 + +[pytest] +testpaths = unittests +xfail_strict = True diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/request_loan_form.json b/loanpy/unittests/request_loan_form.json similarity index 100% rename from loan-custom/caosdb-server/scripting/bin/test/loan_management/request_loan_form.json rename to loanpy/unittests/request_loan_form.json diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_box_loan.py b/loanpy/unittests/test_box_loan.py similarity index 96% rename from loan-custom/caosdb-server/scripting/bin/test/loan_management/test_box_loan.py rename to loanpy/unittests/test_box_loan.py index ff40b78cbe29cec66699b9513bfd48ffda43ce63..9e7631d1c172511c040b676df48269e996570131 100644 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_box_loan.py +++ b/loanpy/unittests/test_box_loan.py @@ -2,7 +2,7 @@ from os.path import abspath, dirname, join from pytest import raises from linkahead import get_connection, configure_connection from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse) -from box_loan import (_caller, create_person, +from loan.box_loan import (_caller, create_person, EMAIL, FIRST_NAME, LAST_NAME, PERSON, assert_date_in_future, DataError, EmailPatternError, assert_email_pattern) diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_manual_return.py b/loanpy/unittests/test_manual_return.py similarity index 82% rename from loan-custom/caosdb-server/scripting/bin/test/loan_management/test_manual_return.py rename to loanpy/unittests/test_manual_return.py index a058ba6ab21f7fac5f972d8b2b80303861b4e557..f1753dde016d3659a9a06e5746a1fcfd060f212b 100644 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_manual_return.py +++ b/loanpy/unittests/test_manual_return.py @@ -1,6 +1,6 @@ from linkahead import Record -from box_loan import BOX, BOX_RETURNED, BOX_BORROWED -from manual_return import _set_returned_box +from loan.box_loan import BOX, BOX_RETURNED, BOX_BORROWED +from loan.manual_return import _set_returned_box def test_set_returned_box(): diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_request_loan.py b/loanpy/unittests/test_request_loan.py similarity index 91% rename from loan-custom/caosdb-server/scripting/bin/test/loan_management/test_request_loan.py rename to loanpy/unittests/test_request_loan.py index 721ef1e131560739a359b99b17a8d4d2b3ffed85..2379f7be473864a56e52bb617e0e8333199dcda5 100644 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_request_loan.py +++ b/loanpy/unittests/test_request_loan.py @@ -3,11 +3,11 @@ from pytest import raises from linkahead import get_connection, configure_connection, Record from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse) from caosadvancedtools.serverside.helper import get_data -from box_loan import (PERSON, FIRST_NAME, EMAIL, BOX, BORROWER, +from loan.box_loan import (PERSON, FIRST_NAME, EMAIL, BOX, BORROWER, EXPECTED_RETURN, EXHAUST_CONTENTS, COMMENT, DESTINATION, F_FIRST_NAME, F_EMAIL, F_EXPECTED_RETURN_DATE, DataError, BOX_NUMBER) -from request_loan import (create_loan, _issue_loan_request) +from loan.request_loan import (create_loan, _issue_loan_request) diff --git a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_request_return.py b/loanpy/unittests/test_request_return.py similarity index 89% rename from loan-custom/caosdb-server/scripting/bin/test/loan_management/test_request_return.py rename to loanpy/unittests/test_request_return.py index 5d01289831098e90c686c64aeba7ddd5c19b8bb2..7dd55be61a75769cef479e60401209b9267347a4 100644 --- a/loan-custom/caosdb-server/scripting/bin/test/loan_management/test_request_return.py +++ b/loanpy/unittests/test_request_return.py @@ -3,12 +3,12 @@ from pytest import raises from linkahead import get_connection, configure_connection from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse) from caosadvancedtools.serverside.helper import get_data -from box_loan import (FIRST_NAME, EMAIL, LAST_NAME, F_FIRST_NAME, +from loan.box_loan import (FIRST_NAME, EMAIL, LAST_NAME, F_FIRST_NAME, F_LAST_NAME, F_EMAIL, EXPECTED_RETURN, LENT, RETURN_REQUESTED, PERSON, BORROWER, BOX, LOAN_REQUESTED, StateError, F_EXPECTED_RETURN_DATE, DataError, LOAN) -from request_return import get_loan, _issue_return_request +from loan.request_return import get_loan, _issue_return_request