diff --git a/.Rhistory b/.Rhistory deleted file mode 100644 index 0a3960f21..000000000 --- a/.Rhistory +++ /dev/null @@ -1 +0,0 @@ -r -v diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..25b5269f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,53 @@ +# Keep .git: install/build diagnostics use git metadata inside the image. + +# Local Python environments and caches +.venv/ +venv/ +env/ +ENV/ +virtualenv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.hypothesis/ +.coverage +.coverage.* +htmlcov/ +.tox/ + +# Local editor and OS files +.vscode/ +.idea/ +.DS_Store +*.swp +*.swo + +# Local runtime/generated deployment files +.env +.env.* +!.env.example +.env.prod.file +install_settings.txt +wetlab/logging_config.ini +logs/ +tmp/ +documents/ +outputs/ +catboost_info/ + +# Build, package, and generated archives +build/ +dist/ +*.egg-info/ +.eggs/ +*.egg +*.log +*.gz +*.zip +*.tar +*.tgz + +# Generated Django/runtime files at repo root +/manage.py +/static/ diff --git a/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md b/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..45bdb2a4a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +# Pull Request description + +## Changes + +- +- +- + +## Related Issues + +Closes # + +## PR Checklist + +Before submitting this PR, please ensure the following: + +- [ ] Code has been linted (`flake8`, `black`). +- [ ] Changes are documented in the `CHANGELOG.md`. +- [ ] Tests have been added or updated (if applicable). +- [ ] The feature/fix has been tested locally. +- [ ] The PR is targeted at the correct branch (`develop`, unless otherwise specified). + +## Notes + \ No newline at end of file diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml index adf58c6ce..ba39bb598 100644 --- a/.github/workflows/python_lint.yml +++ b/.github/workflows/python_lint.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.9.x + python-version: '3.11' architecture: x64 - name: Checkout PyTorch uses: actions/checkout@master @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install black in jupyter run: pip install black[jupyter] - name: Check code lints with Black diff --git a/.gitignore b/.gitignore index 64b77f46c..3ba26fc9a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ *.bin *.gz *_BAK -migrations/ tmp/ logs/ /static/ @@ -17,6 +16,7 @@ documents/ manage.py logs/ logs +.vscode/ # Byte-compiled / optimized / DLL files @@ -102,6 +102,7 @@ celerybeat-schedule # dotenv .env +.env.prod.file # virtualenv .venv diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..185f49603 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,151 @@ +# iSkyLIMS 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.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.1.0] - 2026-05-21 : + +### Credits + +- [Sara Monzón](https://github.com/saramonzon) +- [Luis Chapado](https://github.com/luissian) +- [Daniel Valle](https://github.com/Daniel-VM) +- [Pablo Mata](https://github.com/Shettland) +- [Sergio Olmos](https://github.com/OPSergio) + +#### Added Enhancements + +- Added setting for HTTPS forwarding [#257](https://github.com/BU-ISCIII/iskylims/pull/257) +- Allow switching to Git SHA or Version Tag and restore initial state [#270](https://github.com/BU-ISCIII/iskylims/pull/270) +- Created graphics for the services that were re-analyzed [#290](https://github.com/BU-ISCIII/iskylims/pull/290) +- Enhance both `install.sh` and `docker_install.sh`, fix data loading issues [#327](https://github.com/BU-ISCIII/iskylims/pull/327) +- Included thorough description for wetlab API's update_lab() method [#361](https://github.com/BU-ISCIII/iskylims/pull/361) +- Included new API function lab-request-mapping to get LabRequest fields ontology map [#377](https://github.com/BU-ISCIII/iskylims/pull/377) +- Improved responses in API create-sample-data by adding ERROR messages and data [#377](https://github.com/BU-ISCIII/iskylims/pull/377) +- Added support for `script-before` and `script-after` hooks in install script. [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Committed baseline migration files and added migrations for develop changes [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Added developer notes on how to create and manage migration files [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Added documentation describing migration scripts and their related versions [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Enabled Docker internal networking for local test installation [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Opened Docker network to allow localhost MySQL connection when required [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Added test fixtures and installation updates for the new sequencer and SampleSheet v2 support, including admin test-group assignment and new sequencer bootstrap data. Closes [#388](https://github.com/BU-ISCIII/iskylims/issues/388) [#392](https://github.com/BU-ISCIII/iskylims/pull/392) +- Refactored container installation flow to separate staged application install from runtime bootstrap tasks [#393](https://github.com/BU-ISCIII/iskylims/pull/393) +- Updated Docker image build to stage application files at build time and run bootstrap tasks on container start/upgrade [#393](https://github.com/BU-ISCIII/iskylims/pull/393) +- Replaced container cron runtime with supercronic and improved multi-container install configuration [#391](https://github.com/BU-ISCIII/iskylims/pull/391) +- Added production container support for bind-mounted Django settings and generated Apache configuration files. +- Added production compose environment file generation during container installation. +- Added configurable ServerName and Apache log naming from installation settings. +- Added dedicated Docker network configuration to the production compose file. +- Added configuration examples for container bind mounts in installation settings templates. + +#### Fixes + +- Fixed minor bugs and improved README [#252](https://github.com/BU-ISCIII/iskylims/pull/252) +- Fixed issue #283: Error in Services Statistics per classification area [#286](https://github.com/BU-ISCIII/iskylims/pull/286) +- Fixed issue #289 [#291](https://github.com/BU-ISCIII/iskylims/pull/291) +- Fixed installation script issue where logs symbolic link was not created if it already existed (#256) [#262](https://github.com/BU-ISCIII/iskylims/pull/262) +- Fixed excessive email notifications during crontab process (#266) [#262](https://github.com/BU-ISCIII/iskylims/pull/262) +- Fixed issue where sample names could not be repeated, making SampleID unique (#26) [#262](https://github.com/BU-ISCIII/iskylims/pull/262) +- Prevent underscores in sample names (#73) [#262](https://github.com/BU-ISCIII/iskylims/pull/262) +- Fixed incorrect ordering of service states in `first_install_tables.json` (#265) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed incorrect confirmation email text after resolution (#261) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed issue where users couldn't search service/project by sample name (#264) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed AttributeError when no username is found in wetlab project (#250) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed issue where barcode count conversion to integer failed (#158) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed deletion issue where removing a run also deleted pools and library preparations (#180) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed deprecated `STATUS_CHOICES` usage in Django versions higher than 3.1.x (#263) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed issue where services could not be searched by service type (#78) [#267](https://github.com/BU-ISCIII/iskylims/pull/267) +- Fixed issue [#338](https://github.com/BU-ISCIII/iskylims/issues/338): Removed unnecessary hidden input passing a large JSON object, now using session storage [#344](https://github.com/BU-ISCIII/iskylims/pull/344) +- Fixed email error manage for multiple notification types. [#346](https://github.com/BU-ISCIII/iskylims/pull/346) +- Replaced all references to `Molecule Code ID` with `Extraction Code ID` for consistency. +- Improved message display in Manage Library Preparation. +- Fixed incomplete code execution when storing protocol values. (#349) [#352](https://github.com/BU-ISCIII/iskylims/pull/352) +- Increased maximum length for `prefix_protocol` to prevent data errors. (#350) [#352](https://github.com/BU-ISCIII/iskylims/pull/352) +- Corrected exception manage, replacing incorrect exception type with `AttributeError`. (#351) [#352](https://github.com/BU-ISCIII/iskylims/pull/352) +- Fixed DataError - Value Too Long for prefix_protocol #350: Increased lenght for field prefix_protocol [#352](https://github.com/BU-ISCIII/iskylims/pull/352) +- Removed --no-cache from docker_install.sh as it only worked with deprecated docker-compose [#356](https://github.com/BU-ISCIII/iskylims/pull/356) +- Removed unused field sample_project_searchable that was leading to errors during migration [#356](https://github.com/BU-ISCIII/iskylims/pull/356) +- Fixed small spacing errors in docker-compose.yml [#356](https://github.com/BU-ISCIII/iskylims/pull/356) +- Fixed KeyError in project schema loading by using default value for missing 'Downloadable' field.[#358](https://github.com/BU-ISCIII/iskylims/pull/358) +- Wetlab api create_sample_data() also creates lab based on submitting_institution data if present [#360](https://github.com/BU-ISCIII/iskylims/pull/360) +- Wetlab api update_lab() now creates new lab if 'create_if_missing' in request.data [#360](https://github.com/BU-ISCIII/iskylims/pull/360) +- Fixed wetlab API's labrequest.serializer update method to work properly [#361](https://github.com/BU-ISCIII/iskylims/pull/361) +- Adapted update_lab serializer call to new serializer update method [#361](https://github.com/BU-ISCIII/iskylims/pull/361) +- Leave missing submitting_fields as empty string instead of crashing in wetlab.api.create_sample_data [#363](https://github.com/BU-ISCIII/iskylims/pull/363) +- Fixed wetlab API create-sample-data error when submitting_institution fields were not provided [#377](https://github.com/BU-ISCIII/iskylims/pull/377) +- Fixed incorrect git revision propagation to Dockerfile during build [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Fixed configuration file accessibility from within Docker container [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Fixed disk utilization check to correctly resolve application folder path [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Fixed incorrect application folder path resolution in crontab scripts [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Fixed samplesheet parsing error [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Fixed logger inconsistency that prevented exceptions and error messages from being written to the update crontab log. Closes [#390](https://github.com/BU-ISCIII/iskylims/issues/390) [#392](https://github.com/BU-ISCIII/iskylims/pull/392) +- Fixed wetlab crontab processing for the new sequencer, including run discovery, completion checks, RunInfo/RunParameters parsing updates, and SampleSheet v2 handling. Closes [#387](https://github.com/BU-ISCIII/iskylims/issues/387) [#392](https://github.com/BU-ISCIII/iskylims/pull/392) +- Improved Podman compatibility and adjusted SELinux bind mount handling in production compose setup [#393](https://github.com/BU-ISCIII/iskylims/pull/393) +- Fixed production container Django settings rendering when settings are provided through bind mounts. +- Fixed duplicate wetlab run configuration test logging. +- Fixed wetlab run configuration tests, including run discovery and completion checks. +- Fixed skipped wetlab run test states so they are displayed in the configuration test view. +- Fixed Samba cron path validation in wetlab configuration checks. +- Fixed Docker build cleanup so DNF cache cleanup only runs in the final DNF step. + +#### Changed + +- Updated installation script with variable modules for more flexibility [#269](https://github.com/BU-ISCIII/iskylims/pull/269) +- Updated installation script to remove commas in values of `rawtobunbarcode` table [#277](https://github.com/BU-ISCIII/iskylims/pull/277) +- Updated installation documentation and script, fixing small issues [#284](https://github.com/BU-ISCIII/iskylims/pull/284) +- Unify main and develop branches [#334](https://github.com/BU-ISCIII/iskylims/pull/334) +- Increased max upload memory size. (#328) +- Renamed method `get_delivery_date` to `get_delivered_date` for clarity. +- Improved query performance and excluded rejected/archived services from ongoing list. (#299) +- Updated sample metadata fields with standardized ontology mappings and schema alignment.[#358](https://github.com/BU-ISCIII/iskylims/pull/358) +- API create-sample-data Lab data mapping moved to core_config.LAB_REQUEST_ONTOLOGY_MAP [#377](https://github.com/BU-ISCIII/iskylims/pull/377) +- Renamed `docker-compose.yml` to `docker-compose.test.yml` and `docker-compose.prod.yml` for test clarity [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Refactored Docker runtime handling when path is outside repository [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Updated `docker_install.sh` to `container_install.sh` with multiple reliability improvements [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Updated upgrade scripts documentation to include execution order information and docker upgrade clarifications [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Improved wetlab API stats-info aggregation queries. +- Improved supercronic download resilience in the Docker build. +- Refreshed install fixtures during Docker upgrades. +- Updated production container documentation for bind mounts and generated configuration files. +- Normalized the Django settings bind path used by container installation. +- Updated installation configuration templates with clearer container configuration guidance. + +#### Removed + +- Dummy fix in usage line [#271](https://github.com/BU-ISCIII/iskylims/pull/271) +- Removed migrations from `.gitignore` and app-level `.gitignore` to ensure version control of schema changes [#389](https://github.com/BU-ISCIII/iskylims/pull/389) +- Removed runtime ownership fixes from container installation. + +#### Requirements + +| Package | Last release Version | New release Version | +|:--------------------|:---------------------|:----------------------------| +| wheel | 0.37.1 | 0.46.2 | +| asn1crypto | 1.5.0 | 1.5.1 | +| bcrypt | 4.0.1 | 4.2.0 | +| biopython | 1.79 | 1.84 | +| cryptography | 38.0.3 | 44.0.3 | +| Django | 4.2 | 4.2.28 | +| django-crispy-forms | 2.0 | 2.3 | +| crispy-bootstrap5 | | 0.7 | +| django-crontab | | 0.7.1 | +| django-js-asset | 2.0.0 | 2.2.0 | +| django-mptt | 0.14.0 | 0.16.0 | +| django-mptt-admin | 2.4.1 | 2.6.2 | +| django-cleanup | 7.0.0 | 8.1.0 | +| interop | | >1.1.22 | +| mod_wsgi | 4.9.4 | 5.0.0 | +| gunicorn | | 22.0.0 | +| mysqlclient | 2.0.3 | 2.2.6 | +| paramiko | 3.1.0 | 3.4.1 | +| jsonschema | 4.17.3 | 4.23.0 | +| pysmb | | 1.2.9.1 | +| django_extensions | 3.2.1 | 3.2.3 | +| djangorestframework | 3.14.0 | 3.15.2 | +| drf-yasg | 1.21.5 | 1.21.7 | +| xlrd | | 2.0.1 | +| pandas | 1.5.3 | 2.2.2 | +| numpy | | 1.26.4 | +| openpyxl | 3.1.1 | 3.1.5 | +| setuptools | | 78.1.1 | diff --git a/Dockerfile b/Dockerfile index 36e103c6e..149e68ba0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,104 @@ -FROM ubuntu:22.04 +FROM registry.access.redhat.com/ubi9/ubi ENV TZ=Europe/Madrid RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# Runtime user (override with build args if needed) +ARG APP_UID=1212 +ARG APP_GID=1212 +ARG APP_SHELL=/sbin/nologin +ARG APP_INSTALL_PATH=/opt/iskylims +ENV APP_INSTALL_PATH=${APP_INSTALL_PATH} +ENV PIP_NO_CACHE_DIR=1 + # Updates -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get upgrade -y +RUN dnf -y update + +# Add EPEL for packages not available in default UBI repositories +RUN dnf -y install --setopt=install_weak_deps=False --nodocs \ + https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm # Essential software -RUN apt-get install -y \ - git wget lsb-core lsb-release \ - libmysqlclient-dev \ - python3-pip libpq-dev \ - python3-wheel apache2-dev \ - gnuplot pkg-config - -RUN git clone https://github.com/bu-isciii/iskylims.git /srv/iskylims +RUN dnf -y install --setopt=install_weak_deps=False --nodocs \ + git wget \ + python3.11 python3.11-pip python3.11-devel python3.11-wheel \ + gcc gcc-c++ make \ + openssl-devel libffi-devel \ + mariadb mariadb-connector-c-devel \ + httpd-devel cronie \ + rsync tzdata \ + pkgconf-pkg-config \ + gnuplot-minimal \ + && dnf clean all \ + && rm -rf /var/cache/dnf /tmp/* /var/tmp/* + +# Install supercronic (rootless-friendly cron runner) +RUN set -eux; \ + SUPERCRONIC_VERSION="v0.2.38"; \ + arch="$(uname -m)"; \ + case "$arch" in \ + x86_64) supercronic_arch="amd64" ;; \ + aarch64) supercronic_arch="arm64" ;; \ + *) echo "Unsupported architecture for supercronic: $arch" >&2; exit 1 ;; \ + esac; \ + supercronic_url="https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-${supercronic_arch}"; \ + if wget --tries=3 --waitretry=2 --retry-connrefused -q -O /usr/local/bin/supercronic "${supercronic_url}"; then \ + chmod +x /usr/local/bin/supercronic; \ + else \ + rm -f /usr/local/bin/supercronic; \ + echo "supercronic download failed from ${supercronic_url}; continuing without cron support"; \ + fi; \ + rm -rf /tmp/* /var/tmp/* + +# Ensure python3 points to the desired version +RUN ln -sf /usr/bin/python3.11 /usr/bin/python3 + +# Install Illumina InterOp CLI used to generate run metric plots +RUN set -eux; \ + cd /opt; \ + wget -q https://github.com/Illumina/interop/releases/download/v1.1.15/InterOp-1.1.15-Linux-GNU.tar.gz; \ + tar -xf InterOp-1.1.15-Linux-GNU.tar.gz; \ + ln -s InterOp-1.1.15-Linux-GNU interop; \ + rm InterOp-1.1.15-Linux-GNU.tar.gz; \ + rm -rf /tmp/* /var/tmp/* + +# Set git repository +RUN mkdir /srv/iskylims WORKDIR /srv/iskylims -RUN pip install -r conf/requirements.txt -RUN bash install.sh --install app --conf conf/docker_install_settings.txt --docker +# Copy the local git repository to docker image directory +COPY --chown=${APP_UID}:${APP_GID} . /srv/iskylims -WORKDIR /opt/iskylims +ENV PATH="/usr/sbin/cron:$PATH" +RUN chmod +x /srv/iskylims/scripts/container_start.sh + +# Set default install type +ARG INSTALL_TYPE=dep +ARG GIT_REVISION=main +ARG INSTALL_CONF=conf/docker_test_settings.txt + +# Prepare dependencies and stage the application tree in the image so the +# container can restart without rerunning install-time file generation. +ENV SKIP_SYSTEM_PACKAGES=1 +RUN /bin/bash install.sh --install dep --git_revision $GIT_REVISION --conf $INSTALL_CONF --skip_apache_restart \ + && rm -rf /root/.cache/pip /tmp/* /var/tmp/* +RUN /bin/bash install.sh --stage install --git_revision $GIT_REVISION --conf $INSTALL_CONF --skip_apache_restart \ + && rm -rf /root/.cache/pip /tmp/* /var/tmp/* +# Use the virtualenv created by install.sh +ENV PATH="${APP_INSTALL_PATH}/virtualenv/bin:${PATH}" + +WORKDIR ${APP_INSTALL_PATH} + +# Create non-root user and set ownership +RUN groupadd -g ${APP_GID} iskylims && \ + useradd -m -u ${APP_UID} -g ${APP_GID} -s ${APP_SHELL} iskylims && \ + mkdir -p ${APP_INSTALL_PATH}/cron ${APP_INSTALL_PATH}/tmp && \ + chown -R ${APP_UID}:${APP_GID} ${APP_INSTALL_PATH} /srv/iskylims && \ + chmod 700 ${APP_INSTALL_PATH}/cron ${APP_INSTALL_PATH}/tmp && \ + git config --system --add safe.directory /srv/iskylims # Expose EXPOSE 8001 -# Start the application -CMD ["python3", "/opt/iskylims/manage.py", "runserver", "0:8001"] \ No newline at end of file + +# Start the application once install.sh has populated /opt/iskylims. +USER iskylims +CMD ["/srv/iskylims/scripts/container_start.sh"] diff --git a/LEAME.md b/LEAME.md index c68cf701a..fc8d24d9a 100644 --- a/LEAME.md +++ b/LEAME.md @@ -1,288 +1,723 @@ -# iSkyLIMS - -[![Django](https://img.shields.io/static/v1?label=Django&message=4.2&color=azul?style=plastic&logo=django)](https://github.com/django/django) -[![Python](https://img.shields.io/static/v1?label=Python&message=3.8.10&color=verde?style=plastic&logo=Python)](https://www.python.org/) -[![Bootstrap](https://img.shields.io/badge/Bootstrap-v5.0-azulvioleta?style=plastic&logo=Bootstrap)](https://getbootstrap.com) -[![versión](https://img.shields.io/badge/versión-3.0.0-naranja?style=plastic&logo=GitHub)](https://github.com/BU-ISCIII/iskylims.git) - -La introducción de la secuenciación masiva (MS) en las instalaciones de genómica ha significado un crecimiento exponencial en la generación de datos, lo que requiere un sistema de seguimiento preciso, desde la preparación de la biblioteca hasta la generación de archivos fastq, el análisis y la entrega al investigador. El software diseñado para manejar esas tareas se llama Sistemas de Gestión de Información de Laboratorio (LIMS), y su software debe adaptarse a las necesidades particulares de su laboratorio de genómica. iSkyLIMS nace con el objetivo de ayudar con las tareas de laboratorio húmedo e implementar un flujo de trabajo que guíe a los laboratorios de genómica en sus actividades, desde la preparación de la biblioteca hasta la producción de datos, reduciendo los posibles errores asociados a la tecnología de alto rendimiento y facilitando el control de calidad de la secuenciación. Además, iSkyLIMS conecta el laboratorio húmedo con el laboratorio seco, facilitando el análisis de datos por parte de bioinformáticos. - -![Imagen](img/iskylims_scheme.png) - -De acuerdo con la infraestructura existente, la secuenciación se realiza en un instrumento Illumina NextSeq. Los datos se almacenan en un dispositivo de almacenamiento masivo NetApp y los archivos fastq se generan (bcl2fastq) en un clúster de cómputo de alto rendimiento Sun Grid Engine (SGE-HPC). Los servidores de aplicaciones ejecutan aplicaciones web para el análisis bioinformático (GALAXY), la aplicación iSkyLIMS y alojan la capa de información de MySQL. El flujo de trabajo de iSkyLIMS WetLab se ocupa del seguimiento y las estadísticas de la ejecución de la secuenciación. El seguimiento de la ejecución pasa por cinco estados: "registrado", el usuario de genómica registra la nueva ejecución de la secuenciación en el sistema, el proceso esperará hasta que la ejecución se complete en la máquina y los datos se transfieran al dispositivo de almacenamiento masivo; "Envío de hoja de muestra", el archivo de hoja de muestra con la información de la ejecución de la secuenciación se copiará en la carpeta de ejecución para el proceso de bcl2fastq; "Procesamiento de datos", se procesan los archivos de parámetros de ejecución y los datos se almacenan en la base de datos; "Estadísticas en ejecución", los datos de desmultiplexación generados en el proceso de bcl2fastq se procesan y almacenan en la base de datos, "Completado", todos los datos se procesan y almacenan correctamente. Se proporcionan estadísticas por muestra, por proyecto, por ejecución y por investigación, así como informes anuales y mensuales. El flujo de trabajo de iSkyLIMS DryLab se encarga de la solicitud de servicios de bioinformática y estadísticas. El usuario solicita servicios que pueden estar asociados con una ejecución de secuenciación. Se proporciona seguimiento de estadísticas y servicios. - -- [iSkyLIMS](#iskylims) - - [Instalación](#instalación) - - [Requisitos previos](#requisitos-previos) - - [Instalación de iSkyLIMS en Docker](#instalación-de-iskylims-en-docker) - - [Instalación de iSkyLIMS en su servidor con Ubuntu/CentOS](#instalación-de-iskylims-en-su-servidor-con-ubuntucentos) - - [Clonar el repositorio de GitHub](#clonar-el-repositorio-de-github) - - [Crear la base de datos de iSkyLIMS y otorgar permisos](#crear-la-base-de-datos-de-iskylims-y-otorgar-permisos) - - [Configuración de ajustes](#configuración-de-ajustes) - - [Ejecutar el script de instalación](#ejecutar-el-script-de-instalación) - - [Actualización a la versión 3.0.0 de iSkyLIMS](#actualización-a-la-versión-300-de-iskylims) - - [Prerrequisitos](#prerrequisitos) - - [Clonar el repositorio de GitHub](#clonar-el-repositorio-de-github-1) - - [Configuración de opciones](#configuración-de-opciones) - - [Ejecución del script de actualización](#ejecución-del-script-de-actualización) - - [Pasos que necesitan permisos de adminsitración](#pasos-que-necesitan-permisos-de-adminsitración) - - [Pasos que no necesitan de permisos de administración](#pasos-que-no-necesitan-de-permisos-de-administración) - - [Qué hacer si algo falla](#qué-hacer-si-algo-falla) - - [Pasos finales de configuración](#pasos-finales-de-configuración) - - [Configuración de SAMBA](#configuración-de-samba) - - [Verificación de correo electrónico](#verificación-de-correo-electrónico) - - [Configurar el servidor Apache](#configurar-el-servidor-apache) - - [Verificación de la instalación](#verificación-de-la-instalación) - - [Documentación de iSkyLIMS](#documentación-de-iskylims) - -## Instalación - -Si tienes algún problema o deseas informar de algún error, por favor, publícalo en [issue](https://github.com/BU-ISCIII/iSkyLIMS/issues) - -### Requisitos previos - -Antes de comenzar la instalación, asegúrate de lo siguiente: - -- Tienes privilegios de **sudo** para instalar los paquetes de software adicionales que iSkyLIMS necesita. -- Dependencias: - - Librerías: -``` - yum groupinstall "Development tools" - yum install zlib-devel bzip2-devel openssl-devel \ - wget httpd-devel mysql-libs sqlite sqlite-devel \ - mariadb-devel mysql-client libffi-devel \ - gnuplot cifs-utils -``` - - lsb_relase: - - RedHat/CentOS: `yum install redhat-lsb-core` - - Ubuntu: `apt install lsb-core lsb-release` -- Base de datos MySQL > 8.0 o MariaDB > 10.4 -- Tienes configurado un servidor local para enviar correos electrónicos. -- git > 2.34 -- Tienes Apache servidor v2.4 -- Tienes Python > 3.8 (si lo compilas debes haber instalado previamente las dependecias de arriba) -- Tienes una conexión a la carpeta compartida de Samba donde se almacenan las carpetas de ejecución (por ejemplo, galera/NGS_Data). -- Dependencias: - - -### Instalación de iSkyLIMS en Docker - -Puedes probar iSkyLIMS creando un contenedor Docker en tu máquina local. - -Clona el repositorio de GitHub de iSkyLIMS y ejecuta el script de Docker para crear el contenedor Docker. - -```bash -git clone https://github.com/BU-ISCIII/iSkyLIMS.git iSkyLIMS -sudo bash docker_install.sh -``` - -El script crea un contenedor de Docker Compose con 3 servicios: - -- web1: contiene la aplicación web iSkyLIMS -- db1: contiene la base de datos MySQL -- samba: contiene el servidor Samba - -Después de crear Docker y tener los servicios en funcionamiento, la estructura de la base de datos y los datos iniciales se cargan en la base de datos. Cuando se complete este paso, se le pedirá que defina al superusuario que tendrá acceso a las páginas de administración de Django. Puede escribir cualquier nombre, pero recomendamos que utilice "admin", ya que más adelante se le pedirá un usuario administrador cuando defina la configuración inicial. - -Siga el mensaje de instrucciones para crear la cuenta del superusuario. - -Cuando el script finalice, abra su navegador escribiendo **localhost:8001** para acceder a iSkyLIMS - -### Instalación de iSkyLIMS en su servidor con Ubuntu/CentOS - -#### Clonar el repositorio de GitHub - -Abra una terminal de Linux y vaya a un directorio donde se descargará el código de iSkyLIMS - -```bash -cd -git clone https://github.com/BU-ISCIII/iskylims.git iskylims -cd iskylims -``` - -#### Crear la base de datos de iSkyLIMS y otorgar permisos - -1. Cree una nueva base de datos llamada "iskylims" (esto es obligatorio). -2. Cree un nuevo usuario con permisos para leer y modificar esa base de datos. -3. Anote el nombre de usuario, la contraseña y la información del servidor de la base de datos. - -#### Configuración de ajustes - -Copia la plantilla de ajustes iniciales en un archivo llamado `install_settings.txt` - -```bash -cp conf/template_install_settings.txt install_settings.txt -``` - -Abra el archivo de configuración con su editor favorito para establecer sus propios valores para la base de datos, la configuración de correo electrónico y la dirección IP local del servidor donde se ejecutará iSkyLIMS. - -```bash -sudo nano install_settings.txt -``` - -#### Ejecutar el script de instalación - -iSkyLIMS debe instalarse en el directorio "/opt". - -Necesitará privilegios de administrador para instalar las dependencias. Para manejar diferentes responsabilidades de instalación dentro de la organización, donde es posible que no sea la persona con privilegios de administrador, nuestro script de instalación tiene estas opciones en el parámetro `--install`: - -- `dep`: para instalar los paquetes de software, así como los paquetes de Python dentro del entorno virtual. Se necesita permisos de administrador. -- `app`: para instalar solo el software de la aplicación iSkyLIMS sin necesidad de tener permisos de administrador. -- `full`: si tiene directamente permisos de administrador, puede instalar tanto las dependencias como la aplicación con esta opción. - -Ejecute uno de los siguientes comandos en una terminal de Linux para la instalación, de acuerdo con la descripción anterior. - -```bash -# para instalar solo las dependencias -sudo bash install.sh --install dep - -# para instalar la aplicación iskylims -bash install.sh --install app - -# para instalar ambos al mismo tiempo -sudo bash install.sh --install full -``` - -### Actualización a la versión 3.0.0 de iSkyLIMS - -Si ya tienes iSkyLIMS en la versión 2.3.0, puedes actualizar a la última versión estable, la 3.0.0. - -La versión 3.0.0 es una versión importante con actualizaciones significativas en dependencias de terceros como Bootstrap. También hemos realizado un gran trabajo en la refactorización y el cambio de nombres de variables/funciones que afectan a la base de datos. Para obtener más detalles sobre los cambios, consulta las notas de la versión. - -#### Prerrequisitos - -Debido a que en esta actualización se modifican muchas tablas en la base de datos, es necesario que hagas una copia de seguridad de: - -- La base de datos de iSkyLIMS. -- La carpeta de iSkyLIMS (carpeta de instalación completa, por ejemplo, /opt/iSkyLIMS). - -Se recomienda encarecidamente que hagas estas copias de seguridad y las guardes de manera segura en caso de que la actualización falle, para poder recuperar tu sistema. Por ejemplo crea una carpeta en `/home/dadmin/backup_pro` que contenga la base de datos y la carpeta de /opt/iskylims para tenerla a mano y poder [restaurar el sistema](#qué-hacer-si-algo-falla). - -#### Clonar el repositorio de GitHub - -También hemos cambiado la forma en que se instala y actualiza iSkyLIMS. A partir de ahora, iSkyLIMS se descarga en una carpeta del usuario y se instala en otro lugar (por ejemplo, /opt/). - -Abre una terminal de Linux y dirígete a un directorio donde se descargará el código de iSkyLIMS. - -```bash -cd < directorio distinto al directorio de instalación > -git clone https://gitlab.isciii.es/BU-ISCIII/iskylims.git iskylims -cd iskylims -``` - -#### Configuración de opciones - -Copia la plantilla de configuración inicial en un archivo llamado install_settings.txt - -```bash -cp conf/template_install_settings.txt install_settings.txt -``` - -Abre el archivo de configuración con tu editor favorito para establecer tus propios valores para la base de datos, la configuración de correo electrónico y la dirección IP local del servidor donde se ejecutará iSkyLIMS. -> Si utilizas un sistema basado en Windows para modificar el archivo, asegúrate de que el archivo se guarde con una codificación amigable para Linux, como ASCII o UTF-8. - -```bash -nano install_settings.txt -``` - -#### Ejecución del script de actualización - -Si en tu organización se requiere que las dependencias u otros elementos que necesiten permisos de administrador sean instalados por una persona diferente a la que instala la aplicación, puedes utilizar el script de instalación en varios pasos de la siguiente manera. - -El script te irá solicitando confirmación en algun paso, si todo está yendo bien sin errores deberás pulsar `y` o `yes` según te lo solicite. - -> Nota: Los errores: "ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'query' at line 1" son normales ya que se trata del título de las sentencias sql que no deben ejecutarse. Se puede ignorar. - -##### Pasos que necesitan permisos de adminsitración - -En primer lugar, debes cambiar el nombre de la carpeta de la aplicación en la carpeta de instalación (`/opt/iSkyLIMS`): - -```bash -# Necesitas ser usuario root para realizar esta operación -sudo mv /opt/iSkyLIMS /opt/iskylims -``` - -Asegúrate de que la carpeta de instalación tenga los permisos correctos para que la persona que instala la aplicación pueda escribir en esa carpeta. - -```bash -# En el caso de que tengas un script para esta tarea. Necesitarás ajustar este script de acuerdo al cambio en el nombre de la ruta: /opt/iSkyLIMS a /opt/iskylims -sudo /scripts/hardening.sh -``` - -En la terminal de Linux, ejecuta uno de los siguientes comandos que mejor se adapte a ti: - -```bash -# para actualizar solo las dependencias del software. ES NECESARIO DISPONER DE PERMISOS DE ROOT. -sudo bash install.sh --upgrade dep - -# PARA INSTALAR AMBAS COSAS AL MISMO TIEMPO. REQUIERE DE ROOT. SI SE VA A INSTALAR POR OTRA PERSONA SIN ROOT NO HACER ESTO. -sudo bash install.sh --upgrade full --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables -``` - -##### Pasos que no necesitan de permisos de administración - -A continuación instalamos la aplicación de iskylims usando el siguiente comando: - -```bash -# para actualizar la aplicación de iskylims, incluyendo los cambios necesarios para la versión en base de datos. NO ES NECESARIO DISPONER DE PERMISOS ROOT. -bash install.sh --upgrade app --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables -``` - -Por último, asegúrate que los permisos de la carpeta son correctos. - -```bash -# En el caso de que tengas un script para esta tarea. En esta versión han cambiado algunas rutas a ficheros, es posible que tengas que ajustar el script en consecuencia. -sudo /scripts/hardening.sh -``` - -#### Qué hacer si algo falla - -Cuando actualizamos la aplicación usando el script estamos realizando varios cambios en la base de datos. Si algo falla tenemos que restaurar el estado anterior, antes de que hubiesemos realizado ninguna acción. - -Necesitamos copiar de vuelta nuestro backup de carpet ade aplicación a /opt/iSkyLIMS (o la carpeta de instalación de nuestra elección), y restaurar la base de datos realizando algo como lo siguiente: - -```bash -sudo rm -rf /opt/iskylims -sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ -sudo /scripts/hardening.sh -mysql -u iskylims -h dmysqlps.isciiides.es -p -# drop database iskylims; -# create database iskylims; -mysql -u iskylims -h dmysqlps.isciiides.es iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_202310160737.sql -``` - -### Pasos finales de configuración - -#### Configuración de SAMBA - -- Inicia sesión con la cuenta de administrador. -- Ve a Massive Sequencing -![Ir a WetLab](img/got_to_wetlab.png){width:50px} -- Ve a Configuración -> Configuración de SAMBA -- Completa el formulario con los parámetros apropiados para la carpeta compartida de SAMBA: -![Formulario SAMBA](img/samba_form.png) - -#### Verificación de correo electrónico - -- Ve a Massive Sequencing -- Ve a Configuración -> Configuración de correo electrónico -- Completa el formulario con los parámetros necesarios para la configuración de correo electrónico y trata de enviar un correo de prueba. - -#### Configurar el servidor Apache - -Copia el archivo de configuración de Apache que se encuentra en la carpeta `conf` según tu distribución dentro del directorio de configuración de Apache y cambia el nombre a iskylims.conf. Revisa cualquier requerimiento de tu sistema, se trata solo de un ejemplo. - -#### Verificación de la instalación - -Abre el navegador y escribe "localhost" o la "IP local del servidor" para comprobar que iSkyLIMS está en funcionamiento. - -También puedes verificar algunas funcionalidades mientras compruebas las conexiones de SAMBA y la base de datos usando: - -- Ve a [configurationTest](https://iskylims.isciii.es/wetlab/configurationTest/) -- Haz clic en Enviar -- Verifica todas las pestañas para asegurarte de que cada conexión sea exitosa. -- Ejecuta las 3 pruebas para cada máquina de secuenciación: MiSeq, NextSeq y NovaSeq. - -### Documentación de iSkyLIMS - -La documentación de iSkyLIMS está disponible en [https://iskylims.readthedocs.io/en/latest](https://iskylims.readthedocs.io/en/latest) +# iSkyLIMS + +[![Django](https://img.shields.io/static/v1?label=Django&message=4.2&color=azul?style=plastic&logo=django)](https://github.com/django/django) +[![Python](https://img.shields.io/static/v1?label=Python&message=3.8.10&color=verde?style=plastic&logo=Python)](https://www.python.org/) +[![Bootstrap](https://img.shields.io/badge/Bootstrap-v5.0-azulvioleta?style=plastic&logo=Bootstrap)](https://getbootstrap.com) +[![version](https://img.shields.io/badge/version-3.0.0-naranja?style=plastic&logo=GitHub)](https://github.com/BU-ISCIII/iskylims.git) + +La introduccion de la secuenciacion masiva (MS) en las instalaciones de genomica ha significado un crecimiento exponencial en la generacion de datos, lo que requiere un sistema de seguimiento preciso, desde la preparacion de la biblioteca hasta la generacion de archivos fastq, el analisis y la entrega al investigador. El software disenado para manejar esas tareas se llama Sistemas de Gestion de Informacion de Laboratorio (LIMS), y su software debe adaptarse a las necesidades particulares de su laboratorio de genomica. iSkyLIMS nace con el objetivo de ayudar con las tareas de laboratorio humedo e implementar un flujo de trabajo que guie a los laboratorios de genomica en sus actividades, desde la preparacion de la biblioteca hasta la produccion de datos, reduciendo los posibles errores asociados a la tecnologia de alto rendimiento y facilitando el control de calidad de la secuenciacion. Ademas, iSkyLIMS conecta el laboratorio humedo con el laboratorio seco, facilitando el analisis de datos por parte de bioinformaticos. + +![Imagen](img/iskylims_scheme.png) + +De acuerdo con la infraestructura existente, la secuenciacion se realiza en un instrumento Illumina NextSeq. Los datos se almacenan en un dispositivo de almacenamiento masivo NetApp y los archivos fastq se generan (bcl2fastq) en un cluster de computo de alto rendimiento Sun Grid Engine (SGE-HPC). Los servidores de aplicaciones ejecutan aplicaciones web para el analisis bioinformatico (GALAXY), la aplicacion iSkyLIMS y alojan la capa de informacion de MySQL. El flujo de trabajo de iSkyLIMS WetLab se ocupa del seguimiento y las estadisticas de la ejecucion de la secuenciacion. El seguimiento de la ejecucion pasa por cinco estados: "registrado", el usuario de genomica registra la nueva ejecucion de la secuenciacion en el sistema, el proceso esperara hasta que la ejecucion se complete en la maquina y los datos se transfieran al dispositivo de almacenamiento masivo; "Envio de hoja de muestra", el archivo de hoja de muestra con la informacion de la ejecucion de la secuenciacion se copiara en la carpeta de ejecucion para el proceso de bcl2fastq; "Procesamiento de datos", se procesan los archivos de parametros de ejecucion y los datos se almacenan en la base de datos; "Estadisticas en ejecucion", los datos de desmultiplexacion generados en el proceso de bcl2fastq se procesan y almacenan en la base de datos, "Completado", todos los datos se procesan y almacenan correctamente. Se proporcionan estadisticas por muestra, por proyecto, por ejecucion y por investigacion, asi como informes anuales y mensuales. El flujo de trabajo de iSkyLIMS DryLab se encarga de la solicitud de servicios de bioinformatica y estadisticas. El usuario solicita servicios que pueden estar asociados con una ejecucion de secuenciacion. Se proporciona seguimiento de estadisticas y servicios. + +- [iSkyLIMS](#iskylims) + - [Obtener el codigo (obligatorio)](#obtener-el-codigo-obligatorio) + - [Elige tu ruta](#elige-tu-ruta) + - [Requisitos minimos](#requisitos-minimos) + - [Despliegue con Docker](#despliegue-con-docker) + - [Contenedor local de pruebas](#contenedor-local-de-pruebas) + - [Contenedor de produccion](#contenedor-de-produccion) + - [Persistir logs/documentos en el host](#persistir-logsdocumentos-en-el-host) + - [Proxy inverso con Apache (contenedor) + Gunicorn](#proxy-inverso-con-apache-contenedor--gunicorn) + - [Tareas cron dentro del contenedor](#tareas-cron-dentro-del-contenedor) + - [Gestionar contenedores despues de la instalacion](#gestionar-contenedores-despues-de-la-instalacion) + - [Actualizacion del despliegue Docker](#actualizacion-del-despliegue-docker) + - [Actualizacion del despliegue Docker v3.0.0 a 3.1.0](#actualizacion-del-despliegue-docker-v300-a-310) + - [Haz copia de seguridad](#haz-copia-de-seguridad) + - [Actualizar codigo y ajustes](#actualizar-codigo-y-ajustes) + - [Despliegue bare-metal (Ubuntu/CentOS)](#despliegue-bare-metal-ubuntucentos) + - [Instalacion](#instalacion) + - [Requisitos previos](#requisitos-previos) + - [Clonar el repositorio](#clonar-el-repositorio) + - [Preparar la base de datos](#preparar-la-base-de-datos) + - [Configurar install\_settings.txt](#configurar-install_settingstxt) + - [Ejecutar install.sh](#ejecutar-installsh) + - [Actualizacion (3.0.x a 3.1.x)](#actualizacion-30x-a-31x) + - [Haz copia de seguridad](#haz-copia-de-seguridad-1) + - [Actualizar codigo y ajustes](#actualizar-codigo-y-ajustes-1) + - [Ejecutar pasos de actualizacion con root](#ejecutar-pasos-de-actualizacion-con-root) + - [Ejecutar pasos de actualizacion sin root](#ejecutar-pasos-de-actualizacion-sin-root) + - [Operaciones comunes (Docker + bare-metal)](#operaciones-comunes-docker--bare-metal) + - [Creacion de base de datos, usuarios y permisos](#creacion-de-base-de-datos-usuarios-y-permisos) + - [Copias de seguridad](#copias-de-seguridad) + - [Restauracion / rollback](#restauracion--rollback) + - [Que hacer si algo falla](#que-hacer-si-algo-falla) + - [Bare-metal](#bare-metal) + - [Docker](#docker) + - [Pasos finales de configuracion](#pasos-finales-de-configuracion) + - [Configuracion de SAMBA](#configuracion-de-samba) + - [Verificacion de correo electronico](#verificacion-de-correo-electronico) + - [Notas para desarrolladores](#notas-para-desarrolladores) + - [Flujo de migraciones Django](#flujo-de-migraciones-django) + - [Rutas persistentes en el host](#rutas-persistentes-en-el-host) + - [Configurar el servidor Apache](#configurar-el-servidor-apache) + - [Verificacion de la instalacion](#verificacion-de-la-instalacion) + - [Documentacion de iSkyLIMS](#documentacion-de-iskylims) + +Si tienes algun problema o deseas informar de algun error, por favor, publicalo en [issue](https://github.com/BU-ISCIII/iSkyLIMS/issues) + +## Obtener el codigo (obligatorio) + +Todas las rutas de instalacion asumen que ya clonaste el repositorio: + +```bash +git clone https://gitlab.isciii.es/bu-isciii/iSkyLIMS.git iskylims +cd iskylims +``` + +## Elige tu ruta + +- **Docker (pruebas locales)**: levanta MySQL + Samba + iSkyLIMS con datos de demo para probar rapidamente. +- **Docker (contenedor de produccion)**: despliega solo la aplicacion, apuntando a tu DB/Samba existente. +- **Bare-metal**: instala o actualiza directamente en hosts Ubuntu/CentOS con `install.sh`. + +## Requisitos minimos + +Requisitos para despliegue en contenedor: + +- Docker Engine + Docker Compose v2, o Podman + `podman-compose` +- git >= 2.34 para clonar/actualizar el repositorio +- MySQL/MariaDB, Apache, Python y `lsb_release` en el host no son necesarios para el despliegue en contenedor +- Para contenedores locales de prueba: MySQL y Samba se arrancan como contenedores con `container_install.sh --test` +- Para contenedores de produccion: acceso a un servidor MySQL/MariaDB externo y a la carpeta Samba configurados en el fichero de instalacion seleccionado +- Directorios y permisos en el host para logs, documentos y estaticos, como se describe en [Persistir logs/documentos en el host](#persistir-logsdocumentos-en-el-host) + +Requisitos para despliegue bare-metal: + +- **Privilegios sudo** para instalar dependencias +- MySQL >= 8.0 o MariaDB > 10.4 +- Apache >= 2.4 +- git >= 2.34 +- Python >= 3.11 +- Servidor local configurado para enviar correos +- Acceso a la carpeta Samba donde estan los run folders +- Paquete `lsb_release`: + - RedHat/CentOS: `yum install redhat-lsb-core` + - Ubuntu: `apt install lsb-core lsb-release` + +## Despliegue con Docker + +### Contenedor local de pruebas + +Levanta el sistema completo (base de datos, Samba y app) con fixtures y datos de demo: + +```bash +bash container_install.sh --test +``` + +Usa `--engine podman` para ejecutar el mismo flujo con Podman: + +```bash +bash container_install.sh --test --engine podman +``` + +Esto usa `docker-compose.test.yml` por defecto. + +Puedes personalizar los valores por defecto: + +- `--demo_data /ruta/a/iskylims_demo_data.tar.gz` para reutilizar un archivo local (si no, se descarga). +- `--skip_demo_data` o `--skip_test_data` para evitar cargar datos extra. +- `--install_type` (`full` por defecto) y `--git_revision` para controlar el build. +- `--script` para ejecutar uno o mas scripts de migracion via `install.sh` (puedes repetir la opcion). + +Ejemplo de uso con un script de migracion en Docker: + +```bash +bash container_install.sh --test --script migrate_optional_values +``` + +Cuando el script termine, abre `http://localhost:8001` y crea el superusuario de Django cuando te lo pida. + +La imagen ahora incluye el arbol de la aplicacion ya preparado dentro de `${APP_INSTALL_PATH}`. Por tanto, los contenedores de prueba pueden recrearse o reiniciarse sin volver a ejecutar la instalacion de ficheros; `container_install.sh` solo lanza las tareas de bootstrap de BD, scripts opcionales y estaticos. + +### Contenedor de produccion + +Despliega el contenedor de iSkyLIMS contra servicios MySQL/Samba externos: + +1. Copia y edita la plantilla de produccion: + + ```bash + cp conf/docker_production_settings.txt conf/my_prod_settings.txt + # edita conf/my_prod_settings.txt con tus datos de DB/Samba + ``` + +2. Construye y ejecuta en modo produccion (usa `docker-compose.prod.yml` por defecto): + + ```bash + bash container_install.sh --install_conf conf/my_prod_settings.txt + ``` + + Usa `--compose_file` para cambiar el compose o `--install_type`/`--git_revision` para variar el build. + Añade `--engine podman` para usar Podman en lugar de Docker. + Tip: captura logs para depuracion: + + ```bash + bash container_install.sh --install_conf conf/my_prod_settings.txt 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log + ``` + +3. Si es una instalacion nueva, crea el superusuario cuando se solicite y completa la configuracion de Samba en la UI. + +Las imagenes de produccion ahora incorporan la aplicacion iSkyLIMS ya preparada. Un reinicio del host o la recreacion del contenedor ya no requiere reinstalar la aplicacion; `container_install.sh` solo ejecuta tareas de bootstrap en runtime, como migraciones, refresco de fixtures, scripts opcionales, creacion del superusuario en la primera instalacion y `collectstatic`. + +Los valores de build/runtime del contenedor se configuran en el fichero de instalacion seleccionado, no exportando variables en la shell. Edita estos campos en `conf/my_prod_settings.txt` antes de ejecutar `container_install.sh`: + +- `APP_INSTALL_PATH`: raiz de instalacion en runtime usada por el contenedor `app`, los estaticos/documentos y los scripts de instalacion. Dejalo vacio para reutilizar `INSTALL_PATH`. +- `APACHE_CONF_PATH`: directorio host usado para los ficheros de configuracion de Apache montados por bind mount. Dejalo vacio para usar `${APP_INSTALL_PATH}/conf`. +- `DJANGO_SETTINGS_PATH`: path host usado para el `settings.py` de Django montado por bind mount. Dejalo vacio para usar `${APP_INSTALL_PATH}/iskylims/settings.py`. Si el valor es un directorio o termina en `/`, `container_install.sh` anade `settings.py`. +- `APP_UID` / `APP_GID`: UID/GID de ejecucion del usuario `iskylims` dentro del contenedor. Valor por defecto: `1212:1212`. +- `APP_SHELL`: shell asignada al usuario de runtime durante la build. Valor por defecto: `/sbin/nologin`. +- `APP_PORT`: puerto interno donde Gunicorn escucha dentro del servicio `app`. Valor por defecto: `8001`. +- `DJANGO_DEBUG`: flag de debug de Django que se pasa al contenedor de produccion. Valor por defecto: `false`; mantenlo desactivado en produccion. +- `DB_CONN_MAX_AGE`: tiempo de vida, en segundos, de las conexiones persistentes de Django a la BD. Valor por defecto: `60`. +- `WEB_CONCURRENCY`: numero de workers de Gunicorn. Valor por defecto: `2`. +- `GUNICORN_THREADS`: numero de hilos por worker de Gunicorn. Valor por defecto: `2`. +- `GUNICORN_TIMEOUT`: timeout de peticiones Gunicorn en segundos. Valor por defecto: `300`. +- `GUNICORN_KEEPALIVE`: keep-alive de Gunicorn en segundos. Valor por defecto: `5`. + +Durante una instalacion/actualizacion de produccion, `container_install.sh` escribe `.env.prod.file` en la raiz del repositorio. Este fichero esta ignorado por git y Compose lo usa para interpolar variables en `docker-compose.prod.yml`. Intencionadamente contiene metadatos de Compose/runtime, no passwords de base de datos ni de correo. + +Asegura que la carpeta de estaticos en el host sea escribible por ese UID/GID: + +```bash +sudo chown -R : +``` + +#### Persistir logs/documentos en el host + +Si usas un compose personalizado, asegurate de mantener estos montajes para conservar logs y datos. + +El compose de produccion usa `INSTALL_PATH` del fichero de configuracion seleccionado, o `APP_INSTALL_PATH` si la exportas, como raiz de ejecucion para la app y para los montajes de configuracion/estaticos del Apache en contenedor. + +Persistencia actual: + +- `/var/log/local/iskylims/apps` -> `${INSTALL_PATH}/logs` dentro del contenedor `app` +- `/var/log/local/iskylims/apache` -> `/var/log/httpd` dentro del contenedor `apache` +- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_reverse_proxy.conf` -> `/etc/httpd/conf.d/iskylims.conf` dentro del contenedor `apache` +- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_logs.conf` -> `/etc/httpd/conf.d/logformat.conf` dentro del contenedor `apache` +- `${DJANGO_SETTINGS_PATH:-${INSTALL_PATH}/iskylims/settings.py}` -> `${INSTALL_PATH}/iskylims/settings.py` dentro del contenedor `app` +- volumen nombrado `iskylims_documents` -> `${INSTALL_PATH}/documents` +- volumen nombrado `iskylims_static` -> `${INSTALL_PATH}/static` + +Crea los directorios necesarios en el host: + +```bash +sudo mkdir -p /var/log/local/iskylims/apps +sudo mkdir -p /var/log/local/iskylims/apache +sudo mkdir -p /conf +sudo chown -R : /var/log/local/iskylims/apps +``` + +En hosts Podman rootless o endurecidos, ejecuta el script de preparacion del host con el mismo usuario que arranca los contenedores. El script pre-crea los ficheros de log de Apache, ajusta la propiedad para el usuario UBI httpd en Podman rootless y aplica etiquetas SELinux de contenedor cuando SELinux esta activo: + +```bash +bash hardening.sh +``` + +Si un administrador lo ejecuta como root, define `PODMAN_USER` con el usuario que arranca los contenedores rootless: + +```bash +PODMAN_USER=bioinfo bash hardening.sh +``` + +#### Proxy inverso con Apache (contenedor) + Gunicorn + +En produccion, el contenedor `app` ejecuta `gunicorn` (no `manage.py runserver`) y el servicio `apache` de `docker-compose.prod.yml` hace de proxy inverso. + +Archivos estaticos: + +- La aplicacion genera los estaticos en `${INSTALL_PATH}/static`. +- `docker-compose.prod.yml` comparte ese directorio con `apache` mediante el volumen nombrado `iskylims_static`. +- La configuracion del proxy sirve `/static` directamente desde `${INSTALL_PATH}/static`. + +Durante `container_install.sh`, los ficheros `conf/iskylims_apache_reverse_proxy.conf` y `conf/iskylims_apache_logs.conf` se renderizan y se copian al host en `${APACHE_CONF_PATH}`. Si `APACHE_CONF_PATH` esta vacio, se copian a `${INSTALL_PATH}/conf`. El `ServerName`, el forwarded host y los nombres de logs access/error del proxy inverso se generan desde `DNS_URL` en el fichero de instalacion seleccionado. A partir de ese momento, los cambios de runtime deben hacerse sobre esas copias. + +`container_install.sh` prepara un fichero host `settings.py` de Django para el bind mount en `${DJANGO_SETTINGS_PATH}`, o en `${INSTALL_PATH}/iskylims/settings.py` si `DJANGO_SETTINGS_PATH` esta vacio. Durante el bootstrap, `install.sh` actualiza ese fichero montado a partir de `conf/template_settings.txt` y el fichero de configuracion seleccionado, conservando el `SECRET_KEY` si ya existe. Despues se pueden editar settings de runtime y reiniciar el contenedor sin reconstruir la imagen. + +Si necesitas otra raiz de instalacion, define `INSTALL_PATH` en el fichero de configuracion o exporta `APP_INSTALL_PATH` antes de ejecutar `container_install.sh`. Si necesitas que los ficheros de Apache o los settings de Django queden fuera de la raiz de runtime de la app, define `APACHE_CONF_PATH` o `DJANGO_SETTINGS_PATH` en el fichero de configuracion, o exportalos antes de ejecutar `container_install.sh`. + +`container_install.sh` crea `${APP_INSTALL_PATH}/conf`, `${APACHE_CONF_PATH:-${APP_INSTALL_PATH}/conf}`, `/var/log/local/iskylims/apps` y `/var/log/local/iskylims/apache` antes de `compose up`, copia ahi los dos ficheros de configuracion de Apache, prepara el fichero de settings montado por bind mount si no existe, exporta `APP_INSTALL_PATH`, `APACHE_CONF_PATH` y `DJANGO_SETTINGS_PATH` a Compose y despues ejecuta `install.sh --bootstrap ...` dentro del contenedor `app`. La imagen del contenedor ya contiene el proyecto Django y el virtualenv preparados dentro de `${APP_INSTALL_PATH}`; el bootstrap actualiza settings, aplica migraciones, scripts/fixtures opcionales y refresca `${INSTALL_PATH}/static`, mientras que el contenedor `apache` sigue escribiendo sus logs en el path del host `/var/log/local/iskylims/apache`. + +Nota SELinux para pre-produccion y produccion: + +- Asegura que `/var/log/local/iskylims/apache` sea escribible por el runtime de contenedores y tenga una etiqueta valida para contenedores, por ejemplo `container_file_t`. +- Si el path del host ya esta etiquetado como `container_file_t`, no anadas `:Z` al bind mount de logs de Apache. `:Z` fuerza un relabel y puede fallar con `lsetxattr(... container_file_t ...): operation not permitted`. +- Comprobacion rapida: + +```bash +ls -ldZ /var/log/local/iskylims/apache +``` + +- Ejemplo esperado: + +```text +system_u:object_r:container_file_t:s0 +``` + +- Si Apache falla al arrancar con `ModSecurity: Failed to open debug log file: /var/log/httpd/modsec_debug.log`, elimina cualquier fichero host obsoleto y recrea/reinicia el contenedor. En la practica, borrar `/var/log/local/iskylims/apache/modsec_debug.log` ha sido suficiente cuando el inode existente tenia permisos o contexto incorrectos. + +#### Tareas cron dentro del contenedor + +Cron se ejecuta mediante `supercronic`, lanzado por el script de arranque del contenedor. El script escribe las entradas de django-crontab en `${APP_INSTALL_PATH}/cron/iskylims` y arranca `supercronic` como usuario no root. + +Si modificas `CRONJOBS`, reconstruye o reinicia el contenedor para regenerar el archivo de cron. + +### Gestionar contenedores despues de la instalacion + +Despues de una instalacion de produccion, usa el fichero generado `.env.prod.file` siempre que ejecutes Compose directamente. Asi las rutas, UID/GID, puertos y ajustes de Gunicorn siguen alineados con el fichero de instalacion. + +Ejemplos con Docker Compose: + +```bash +docker compose --env-file .env.prod.file -f docker-compose.prod.yml ps +docker compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app +docker compose --env-file .env.prod.file -f docker-compose.prod.yml restart app +docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d +``` + +Ejemplos con Podman Compose: + +```bash +podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps +podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app +podman compose --env-file .env.prod.file -f docker-compose.prod.yml restart app +podman compose --env-file .env.prod.file -f docker-compose.prod.yml up -d +``` + +Si editas valores de runtime del contenedor en el fichero de instalacion, vuelve a ejecutar `container_install.sh --install_conf ` para regenerar `.env.prod.file` y los contenedores de forma consistente. + +### Actualizacion del despliegue Docker + +Mantén los mismos valores de `APP_UID`/`APP_GID` en el fichero de instalacion seleccionado antes de actualizar. + +Re-despliega el contenedor de aplicacion contra una base de datos existente: + +```bash +bash container_install.sh --install_conf conf/my_prod_settings.txt --action upgrade +``` + +La actualizacion reconstruye/reinicia el contenedor y ejecuta `install.sh --bootstrap upgrade --tables` dentro del contenedor. Los ficheros de la aplicacion ya van incorporados en la nueva imagen; la fase de bootstrap aplica migraciones con `--fake-initial`, refresca `conf/first_install_tables.json`, refresca los estaticos y evita cargar superusuario/datos demo/prueba. + +### Actualizacion del despliegue Docker v3.0.0 a 3.1.0 + +#### Haz copia de seguridad + +Ejecuta primero los pasos de [Copias de seguridad](#copias-de-seguridad). + +Para 3.0.0 -> 3.1.0, exporta primero el mapeo de LibraryPool y luego ejecuta la actualizacion con scripts pre/post: + +```bash +mysql --user= --password= --host= --port= iskylims \ + -e "SELECT id, run_process_id_id FROM wetlab_library_pool" \ + > /tmp/library_pool_run_process.tsv +``` + +#### Actualizar codigo y ajustes + +```bash +cd /iskylims +git pull +cp conf/docker_production_settings.txt myprod_settings.txt +sudo nano myprod_settings.txt +``` + +Si editas el archivo en Windows, asegurate de guardarlo con codificacion UTF-8/ASCII. + +Mantén los mismos valores de `APP_UID`/`APP_GID` en el fichero de instalacion seleccionado antes de ejecutar la actualizacion 3.0.0 -> 3.1.0. + +Ejecuta la actualizacion: + +```bash +bash container_install.sh --engine podman --install_conf myprod_settings.txt --action upgrade \ + --script_before convert_rawtop_counter_to_int \ + --script_after library_pool_to_many_relation,/tmp/library_pool_run_process.tsv +``` + +## Despliegue bare-metal (Ubuntu/CentOS) + +### Instalacion + +#### Requisitos previos + +- **Privilegios sudo** para instalar dependencias +- MySQL > 8.0 o MariaDB > 10.4 +- Apache 2.4 +- git > 2.34 +- Python > 3.11 +- Servidor local configurado para enviar correos +- Acceso a la carpeta Samba donde estan los run folders +- Paquete `lsb_release` (`yum install redhat-lsb-core` en RedHat/CentOS, `apt install lsb-core lsb-release` en Ubuntu) + +#### Clonar el repositorio + +```bash +cd +git clone https://gitlab.isciii.es/bu-isciii/iSkyLIMS.git iskylims +cd iskylims +``` + +#### Preparar la base de datos + +Crea la base de datos y el usuario de aplicacion siguiendo [Creacion de base de datos, usuarios y permisos](#creacion-de-base-de-datos-usuarios-y-permisos). Guarda host, puerto, usuario y password para `install_settings.txt`. + +#### Configurar install_settings.txt + +```bash +cp conf/template_install_settings.txt install_settings.txt +nano install_settings.txt +``` + +Completa los valores de base de datos, email, IP/URL del servidor y logging. + +#### Ejecutar install.sh + +iSkyLIMS se instala en `/opt/iskylims` por defecto. El script `install.sh` gestiona dependencias y aplicacion; elige lo que necesitas con `--install`: + +- `dep`: instala dependencias del sistema y de Python (requiere sudo). +- `app`: despliega el codigo, actualiza ajustes, ejecuta migraciones y collectstatic (sin sudo). +- `full`: ejecuta ambos pasos. + +La separacion interna entre preparacion de ficheros y bootstrap se usa solo para las imagenes de contenedor. En bare-metal no cambian los comandos operativos: `--install` y `--upgrade` siguen ejecutando el flujo completo de dependencias, aplicacion y base de datos descrito aqui. + +Ejemplos: + +```bash +# solo dependencias del sistema +sudo bash install.sh --install dep + +# solo aplicacion iSkyLIMS +bash install.sh --install app --git_revision main --tables + +# dependencias + aplicacion +sudo bash install.sh --install full --git_revision main --tables +``` + +- Añade `--tables` para cargar los datos iniciales en instalaciones nuevas, o `--skip_tables` si quieres omitirlos. +- Captura logs para depuracion con `tee`: + + ```bash + sudo bash install.sh --install full --git_revision main --tables 2>&1 | tee ./iskylims_install_$(date +%Y%m%d_%H%M%S).log + ``` + +- Si Apache se gestiona desde otro sitio, omite el reinicio automatico con `--skip_apache_restart`. + +### Actualizacion (3.0.x a 3.1.x) + +Sigue estos pasos para pasar de la version 3.0.0 a la serie 3.1.x. + +#### Haz copia de seguridad + +- Ejecuta primero los pasos de [Copias de seguridad](#copias-de-seguridad). +- Ademas, guarda una copia completa de la carpeta de instalacion (por ejemplo `/opt/iskylims`) para rollback bare-metal. +- Si usas library pools, exportalos antes de actualizar: + + ```bash + mysql --user= --password= --host= --port= iskylims \ + -e "SELECT id, run_process_id_id FROM wetlab_library_pool" \ + > /tmp/library_pool_run_process.tsv + ``` + +#### Actualizar codigo y ajustes + +```bash +cd /iskylims +git pull +cp conf/template_install_settings.txt install_settings.txt +sudo nano install_settings.txt +``` + +Si editas el archivo en Windows, asegurate de guardarlo con codificacion UTF-8/ASCII. + +#### Ejecutar pasos de actualizacion con root + +Actualiza dependencias del sistema y de Python: + +```bash +sudo bash install.sh --upgrade dep 2>&1 | tee install_full.log +``` + +Asegura que los permisos permiten que el paso sin root escriba en `/opt/iskylims` (ajusta tu hardening si cambio la ruta). + +#### Ejecutar pasos de actualizacion sin root + +Actualiza el codigo y la base de datos: + +```bash +# con restauracion de library pool +bash install.sh --upgrade app --git_revision main \ + --script_before convert_rawtop_counter_to_int \ + --script_after library_pool_to_many_relation,/tmp/library_pool_run_process.tsv +``` + +O ejecuta todo en un unico comando: + +```bash +sudo bash install.sh --upgrade full --git_revision main --tables +``` + +Las actualizaciones regeneran las migraciones y las aplican con `--fake-initial` para conservar las tablas existentes, igual que en Docker. + +## Operaciones comunes (Docker + bare-metal) + +### Creacion de base de datos, usuarios y permisos + +Ejecuta como root de MySQL: + +```sql +CREATE DATABASE IF NOT EXISTS iskylims CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE USER IF NOT EXISTS 'iskylims'@'%' IDENTIFIED BY 'djangopass'; +CREATE USER IF NOT EXISTS 'iskylims'@'localhost' IDENTIFIED BY 'djangopass'; + +GRANT ALL PRIVILEGES ON iskylims.* TO 'iskylims'@'%'; +GRANT ALL PRIVILEGES ON iskylims.* TO 'iskylims'@'localhost'; + +FLUSH PRIVILEGES; +``` + +Verificacion: + +```sql +SHOW GRANTS FOR 'iskylims'@'%'; +``` + +### Copias de seguridad + +Dump de base de datos: + +```bash +mysqldump -h -P -u iskylims -p iskylims > iskylims_$(date +%Y%m%d_%H%M%S).sql +``` + +Archivo de logs: + +```bash +tar -czf iskylims_app_logs_$(date +%Y%m%d_%H%M%S).tgz -C /var/log/local/iskylims/apps . + +tar -czf iskylims_apache_logs_$(date +%Y%m%d_%H%M%S).tgz -C /var/log/local/iskylims/apache . +``` + +Archivo del volumen de documents: + +```bash +docker run --rm -v iskylims_documents:/from -v "$PWD":/to alpine \ + tar -czf /to/iskylims_documents_$(date +%Y%m%d_%H%M%S).tgz -C /from . +``` + +Con Podman, usa el mismo comando sustituyendo `docker` por `podman`. + +Orden recomendado antes de actualizar: + +1. Dump de BD +2. Archivo del volumen de documents +3. Archivo de logs + +### Restauracion / rollback + +Restaurar BD: + +```bash +mysql -h -P -u iskylims -p iskylims < iskylims_YYYYMMDD_HHMMSS.sql +``` + +Restaurar volumen de documents: + +```bash +docker run --rm -v iskylims_documents:/to -v "$PWD":/from alpine \ + sh -lc "cd /to && tar -xzf /from/iskylims_documents_YYYYMMDD_HHMMSS.tgz" +``` + +Con Podman, usa el mismo comando sustituyendo `docker` por `podman`. + +Restaurar logs: + +```bash +mkdir -p /var/log/local/iskylims/apps +tar -xzf iskylims_app_logs_YYYYMMDD_HHMMSS.tgz -C /var/log/local/iskylims/apps + +mkdir -p /var/log/local/iskylims/apache +tar -xzf iskylims_apache_logs_YYYYMMDD_HHMMSS.tgz -C /var/log/local/iskylims/apache +``` + +Ejemplo de rollback completo bare-metal: + +```bash +sudo rm -rf /opt/iskylims +sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ +sudo /scripts/hardening.sh +mysql -u iskylims -p -h iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_YYYYMMDDHHMM.sql +``` + +## Que hacer si algo falla + +Cuando una instalacion o actualizacion falla, restaura el estado anterior y reintenta con logs activados. + +Diagnosticos rapidos: + +```bash +# bare-metal +cd /opt/iskylims +python manage.py check + +# docker +docker compose --env-file .env.prod.file -f docker-compose.prod.yml ps +docker compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app + +# podman +podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps +podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app +``` + +Si sospechas que una imagen o cache de build de Docker esta corrupta: + +```bash +docker compose --env-file .env.prod.file -f docker-compose.prod.yml build --no-cache app +docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d --force-recreate app +``` + +### Bare-metal + +Necesitamos copiar la carpeta completa `/opt/iskylims` de vuelta a `/opt/iskylims` (o tu ruta de instalacion), y restaurar la base de datos con algo como: + +```bash +sudo rm -rf /opt/iskylims +sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ +sudo /scripts/hardening.sh +mysql -u iskylims -p -h dmysqlps.isciiides.es +# drop database iskylims; +# create database iskylims; +mysql -u iskylims -p -h dmysqlps.isciiides.es iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_202310160737.sql +``` + +### Docker + +1. Para el contenedor: + + ```bash + docker compose --env-file .env.prod.file -f docker-compose.prod.yml down + ``` + +2. Restaura la base de datos desde tu backup. + +3. Si sospechas que la imagen o la cache de build esta corrupta, elimina la imagen de la app y reconstruye: + + ```bash + docker image ls | grep iskylims + docker rmi + ``` + +4. Si los volumenes estan comprometidos, restauralos desde los tar: + +```bash +mkdir -p /var/log/local/iskylims/apps +tar -xzf iskylims_app_logs.tgz -C /var/log/local/iskylims/apps + +mkdir -p /var/log/local/iskylims/apache +tar -xzf iskylims_apache_logs.tgz -C /var/log/local/iskylims/apache + +docker run --rm -v iskylims_documents:/to -v "$PWD":/from alpine \ + tar -xzf /from/iskylims_documents.tgz -C /to +``` + +5. Arranca el contenedor de nuevo: + + ```bash + bash container_install.sh --install_conf conf/my_prod_settings.txt --action upgrade + ``` + +## Pasos finales de configuracion + +### Configuracion de SAMBA + +- Inicia sesion con la cuenta admin. +- Ve a Massive sequencing +![go_to_wetlab](img/got_to_wetlab.png){width:50px} +- Ve a Configuration -> Samba configuration +- Rellena el formulario con los parametros apropiados para la carpeta compartida de Samba: +![samba form](img/samba_form.png) + +### Verificacion de correo electronico + +- Ve a Massive sequencing +- Ve a Configuration -> Email configuration +- Rellena el formulario con los parametros necesarios y prueba a enviar un correo. + +## Notas para desarrolladores + +### Flujo de migraciones Django + +Las migraciones se versionan en el repositorio. No ejecutes `makemigrations` durante la instalacion o actualizacion. + +Flujo base + actualizacion para nuevas releases: + +1. Genera las migraciones base desde el ultimo tag estable (por ejemplo 3.0.0). +2. Versiona las migraciones base. +3. Genera en `develop` las nuevas migraciones para cambios de esquema y versionalas. +4. Las actualizaciones ejecutan una vez `migrate --fake-initial` para alinear tablas existentes, y despues `migrate` para aplicar los nuevos ficheros de migracion. + +### Rutas persistentes en el host + +Consulta [Persistir logs/documentos en el host](#persistir-logsdocumentos-en-el-host) en la seccion de despliegue de produccion. + +### Configurar el servidor Apache + +Copia el archivo de configuracion de Apache segun tu distribucion dentro del directorio de configuracion de Apache y renombralo a iskylims.conf + +Ubicaciones tipicas: + +- Ubuntu/Debian: `/etc/apache2/sites-available/iskylims.conf` (habilitar con `a2ensite`) +- CentOS/RHEL: `/etc/httpd/conf.d/iskylims.conf` + +Pasos sugeridos (Apache en el host como proxy inverso): + +Estos pasos aplican a instalaciones bare-metal con Apache en el host. En despliegues Docker de produccion se usa el contenedor `apache` y no hace falta copiar configuracion a `/etc/apache2` o `/etc/httpd`. + +1. Copia el ejemplo de configuracion: + + ```bash + sudo cp conf/iskylims_apache_reverse_proxy.conf /etc/apache2/sites-available/iskylims.conf + # CentOS/RHEL: + # sudo cp conf/iskylims_apache_reverse_proxy.conf /etc/httpd/conf.d/iskylims.conf + ``` + +2. Edita la configuracion: + + - Ajusta `ServerName` + - Comprueba que `ProxyPass` apunte a `http://localhost:8001/` + - Comprueba `Alias /static/ /opt/iskylims/static/` + +3. Crea la carpeta de estaticos en el host: + + ```bash + sudo mkdir -p /opt/iskylims/static + ``` + +4. Habilita modulos necesarios (Ubuntu/Debian): + + ```bash + sudo a2enmod proxy proxy_http headers + sudo a2ensite iskylims.conf + ``` + +5. Recarga Apache: + + ```bash + sudo systemctl reload apache2 + # CentOS/RHEL: + # sudo systemctl reload httpd + ``` + +### Verificacion de la instalacion + +Abre el navegador y escribe "localhost" o la IP local del servidor para comprobar que iSkyLIMS esta funcionando. + +Tambien puedes comprobar parte de la funcionalidad y las conexiones a Samba y base de datos usando: + +- Ve a [configuration test](https://iskylims.isciii.es/wetlab/configurationTest/) +- Haz click en submit +- Revisa todas las pestañas para confirmar que la conexion es correcta. +- Ejecuta las 3 pruebas para cada maquina de secuenciacion: MiSeq, NextSeq y NovaSeq. + +## Documentacion de iSkyLIMS + +La documentacion de iSkyLIMS esta disponible en [https://iskylims.readthedocs.io/en/latest](https://iskylims.readthedocs.io/en/latest) diff --git a/README.md b/README.md index d585518e6..3a6a3c646 100644 --- a/README.md +++ b/README.md @@ -13,239 +13,551 @@ According to existent infrastructure sequencing is performed on an Illumina Next Application servers run web applications for bioinformatics analysis (GALAXY), the iSkyLIMS app, and host the MySQL information tier. iSkyLIMS WetLab workflow deals with sequencing run tracking and statistics. Run tracking passes through five states: "recorded” genomics user record the new sequencing run into the system, the process will wait till run is completed by the machine and data is transferred to the mass storage device; “Sample sheet sent” sample sheet file with the sequencing run information will be copied to the run folder for bcl2fastq process; “Processing data” run parameters files are processed and data is stored in the database; “Running stats” demultiplexing data generated in bcl2fastq process is processed and stored into the database, “Completed” all data is processed and stored successfully. Statistics per sample, per project, per run and per investigation are provided, as well as annual and monthly reports. iSkyLIMS DryLab workflow deals with bioinformatics services request and statistics. User request services that can be associated with a sequencing run. Stats and services tracking is provided. - [iSkyLIMS](#iskylims) - - [Installation](#installation) - - [Pre-requisites](#pre-requisites) - - [iSkyLIMS docker installation](#iskylims-docker-installation) - - [Install iSkyLIMS in your server running ubuntu/CentOS](#install-iskylims-in-your-server-running-ubuntucentos) - - [Clone github repository](#clone-github-repository) - - [Create iskylims database and grant permissions](#create-iskylims-database-and-grant-permissions) - - [Configuration settings](#configuration-settings) - - [Run installation script](#run-installation-script) - - [Upgrade to iSkyLIMS version 3.0.0](#upgrade-to-iskylims-version-300) - - [Pre-requisites](#pre-requisites-1) - - [Clone github repository](#clone-github-repository-1) - - [Configuration settings](#configuration-settings-1) - - [Running upgrade script](#running-upgrade-script) - - [Steps requiring root](#steps-requiring-root) - - [Steps not requiring root](#steps-not-requiring-root) - - [What to do if something fails](#what-to-do-if-something-fails) - - [Final configuration steps](#final-configuration-steps) - - [SAMBA configurarion](#samba-configurarion) - - [Email verification](#email-verification) - - [Configure Apache server](#configure-apache-server) - - [Verification of the installation](#verification-of-the-installation) - - [iSkyLIMS documentation](#iskylims-documentation) - -## Installation + - [Get the code (required)](#get-the-code-required) + - [Choose your path](#choose-your-path) + - [Minimum requirements](#minimum-requirements) + - [Docker deployment](#docker-deployment) + - [Local test stack](#local-test-stack) + - [Production container](#production-container) + - [Persist logs/documents on the host](#persist-logsdocuments-on-the-host) + - [Apache reverse proxy (container) + Gunicorn](#apache-reverse-proxy-container--gunicorn) + - [Cron jobs inside the container](#cron-jobs-inside-the-container) + - [Manage containers after installation](#manage-containers-after-installation) + - [Upgrade docker deployment](#upgrade-docker-deployment) + - [Upgrade docker deployment v3.0.0 to 3.1.0](#upgrade-docker-deployment-v300-to-310) + - [Back up first](#back-up-first) + - [Refresh code and settings](#refresh-code-and-settings) + - [Bare-metal deployment (Ubuntu/CentOS)](#bare-metal-deployment-ubuntucentos) + - [Install](#install) + - [Clone the repository](#clone-the-repository) + - [Prepare the database](#prepare-the-database) + - [Configure install\_settings.txt](#configure-install_settingstxt) + - [Run install.sh](#run-installsh) + - [Upgrade (3.0.0 to 3.1.0)](#upgrade-300-to-310) + - [Back up first](#back-up-first-1) + - [Refresh code and settings](#refresh-code-and-settings-1) + - [Run upgrade steps requiring root](#run-upgrade-steps-requiring-root) + - [Run upgrade steps without root](#run-upgrade-steps-without-root) + - [Common operations (Docker + bare-metal)](#common-operations-docker--bare-metal) + - [Database creation, users and grants](#database-creation-users-and-grants) + - [Backups](#backups) + - [Restore / rollback](#restore--rollback) + - [What to do if something fails](#what-to-do-if-something-fails) + - [Final configuration steps](#final-configuration-steps) + - [SAMBA configurarion](#samba-configurarion) + - [Email verification](#email-verification) + - [Developer notes](#developer-notes) + - [Django migrations workflow](#django-migrations-workflow) + - [Persistent host paths](#persistent-host-paths) + - [Configure Apache server](#configure-apache-server) + - [Verification of the installation](#verification-of-the-installation) + - [iSkyLIMS documentation](#iskylims-documentation) For any problems or bug reporting please post us an [issue](https://github.com/BU-ISCIII/iSkyLIMS/issues) -### Pre-requisites +## Get the code (required) -Before starting the installation make sure : +All installation paths assume you already cloned the repository: -- You have **sudo privileges** to install the additional software packets that iSkyLIMS needs. -- Database MySQL > 8.0 or MariaDB > 10.4 -- Local server configured for sending emails -- Apache server v2.4 -- git > 2.34 -- Python > 3.8 -- Connection to samba shared folder where run folders are stored (p.e galera/NGS_Data) -- Dependencies: - - lsb_release: - - RedHat/CentOS: ```yum install redhat-lsb-core``` - - Ubuntu: ```apt install lsb-core lsb-release``` +```bash +git clone https://github.com/BU-ISCIII/iskylims.git iskylims +cd iskylims +``` + +## Choose your path + +- **Docker (local test)**: spin up MySQL + Samba + iSkyLIMS with demo data to try the app quickly. +- **Docker (production container)**: deploy only the application container, pointing to your existing DB/Samba. +- **Bare-metal**: install or upgrade directly on Ubuntu/CentOS hosts with `install.sh`. + +## Minimum requirements + +Container deployment requirements: + +- Docker Engine + Docker Compose v2, or Podman + `podman-compose` +- git >= 2.34 to clone/update the repository +- Host MySQL/MariaDB, Apache, Python, and `lsb_release` are not required for container deployment +- For local test containers: MySQL and Samba are started as containers by `container_install.sh --test` +- For production containers: access to an external MySQL/MariaDB server and Samba share configured in the selected install config +- Host directories and permissions for logs, documents, and static files, as described in [Persist logs/documents on the host](#persist-logsdocuments-on-the-host) + +Bare-metal deployment requirements: + +- **sudo privileges** for dependency installation +- MySQL >= 8.0 or MariaDB > 10.4 +- Apache >= 2.4 +- git >= 2.34 +- Python >= 3.11 +- Local email sender configured +- Access to the Samba share where run folders live +- `lsb_release` package: + - RedHat/CentOS: `yum install redhat-lsb-core` + - Ubuntu: `apt install lsb-core lsb-release` -### iSkyLIMS docker installation +## Docker deployment -You can test iSkyLIMS by creating a docker container on your local machine. +### Local test stack -Clone the iSkyLIMS github repository and run the docker script to create the docker +Bring up a full test stack (database, Samba, app) plus fixtures and demo data: ```bash -git clone https://github.com/BU-ISCIII/iSkyLIMS.git iSkyLIMS -sudo bash docker_install.sh +bash container_install.sh --test 2>&1 | tee test.log ``` -The script creates a docker compose container with 3 services: +Use `--engine podman` to run the same flow with Podman: + +```bash +bash container_install.sh --test --engine podman 2>&1 | tee test.log +``` + +This uses `docker-compose.test.yml` by default. + +Defaults can be customised: + +- `--demo_data /path/to/iskylims_demo_data.tar.gz` to reuse a local demo archive (otherwise it is downloaded). +- `--skip_demo_data` or `--skip_test_data` to avoid loading extra data. +- `--install_type` (`full` by default) and `--git_revision` to control the build. +- `--script` to run one or more Django migration scripts through `install.sh` (repeat the flag as needed). + +Example running a migration script during Docker install: + +```bash +bash container_install.sh --test --script migrate_optional_values 2>&1 | tee test.log +``` -- web1: contains the iSkyLIMS web application -- db1: contains the mySQL database -- samba: contains samba server +When the script finishes, open `http://localhost:8001` and follow the prompt to create the Django superuser. -After Docker is created and services are up, database structure and initial data are loaded into database. When this step is completed, you will be asked to define the super user which will have access to django admin pages. You can type any name, but we recommend that you use "admin", because admin user is requested later on when defining the initial settings. +The image now includes the staged application tree under `${APP_INSTALL_PATH}`. Test containers can therefore be recreated or restarted without rerunning the file installation step; only DB/bootstrap tasks are executed by `container_install.sh`. -Follow the prompt message to create the super user account. +### Production container -When script ends open your navigator typing **localhost:8001** to access to iSkyLIMS +Deploy the iSkyLIMS container against external MySQL/Samba services: -### Install iSkyLIMS in your server running ubuntu/CentOS +1. Copy and edit the production settings template: -#### Clone github repository + ```bash + cp conf/docker_production_settings.txt conf/my_prod_settings.txt + # edit conf/my_prod_settings.txt with your DB/Samba details + ``` -Open a linux terminal and move to a directory where iSkyLIMS code will be -downloaded +2. Build and run in production mode (uses `docker-compose.prod.yml` by default): + + ```bash + bash container_install.sh --install_conf conf/my_prod_settings.txt 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log + ``` + + Use `--compose_file` to override the compose file or `--install_type`/`--git_revision` to change the build. + Add `--engine podman` to use Podman instead of Docker. + Tip: capture logs for troubleshooting: + + ```bash + bash container_install.sh --install_conf conf/my_prod_settings.txt 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log + ``` + +3. If this is a fresh install, create the Django superuser when prompted and complete the Samba configuration in the UI. + +Production images now bake the staged iSkyLIMS application into the image itself. Host reboots or container recreation no longer require rerunning the app installation step; `container_install.sh` only performs runtime bootstrap tasks such as migrations, fixture refreshes, optional scripts, superuser creation on first install, and `collectstatic`. + +Container build/runtime values are configured in the selected install config, not by exporting shell variables. Edit these fields in `conf/my_prod_settings.txt` before running `container_install.sh`: + +- `APP_INSTALL_PATH`: runtime install root used by the app container, static/documents mounts, and install scripts. Leave empty to reuse `INSTALL_PATH`. +- `APACHE_CONF_PATH`: host directory used for Apache bind-mounted config files. Leave empty to use `${APP_INSTALL_PATH}/conf`. +- `DJANGO_SETTINGS_PATH`: host path used for the bind-mounted Django `settings.py`. Leave empty to use `${APP_INSTALL_PATH}/iskylims/settings.py`. If the value is a directory or ends with `/`, `container_install.sh` appends `settings.py`. +- `APP_UID` / `APP_GID`: runtime UID/GID for the `iskylims` user inside the container. Default: `1212:1212`. +- `APP_SHELL`: shell assigned to the runtime user during image build. Default: `/sbin/nologin`. +- `APP_PORT`: internal Gunicorn bind port for the `app` service. Default: `8001`. +- `DJANGO_DEBUG`: Django debug flag passed to the production app container. Default: `false`; keep it disabled in production. +- `DB_CONN_MAX_AGE`: Django persistent DB connection lifetime in seconds. Default: `60`. +- `WEB_CONCURRENCY`: Gunicorn worker count. Default: `2`. +- `GUNICORN_THREADS`: threads per Gunicorn worker. Default: `2`. +- `GUNICORN_TIMEOUT`: Gunicorn request timeout in seconds. Default: `300`. +- `GUNICORN_KEEPALIVE`: Gunicorn keep-alive in seconds. Default: `5`. + +During production install/upgrade, `container_install.sh` writes `.env.prod.file` in the repository root. This file is ignored by git and is used by Compose for variable interpolation in `docker-compose.prod.yml`. It intentionally contains Compose/runtime metadata, not database or email passwords. + +Host directory and ownership preparation is described in [Persist logs/documents on the host](#persist-logsdocuments-on-the-host). + +#### Persist logs/documents on the host + +The production compose file uses `INSTALL_PATH` from the selected install config, or `APP_INSTALL_PATH` if exported, as the runtime root for the app and for the Apache config/static mounts. + +Persistence layout: + +- `/var/log/local/iskylims/apps` -> `${INSTALL_PATH}/logs` inside the `app` container +- `/var/log/local/iskylims/apache` -> `/var/log/httpd` inside the `apache` container +- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_reverse_proxy.conf` -> `/etc/httpd/conf.d/iskylims.conf` inside the `apache` container +- `${APACHE_CONF_PATH:-${INSTALL_PATH}/conf}/iskylims_apache_logs.conf` -> `/etc/httpd/conf.d/logformat.conf` inside the `apache` container +- `${DJANGO_SETTINGS_PATH:-${INSTALL_PATH}/iskylims/settings.py}` -> `${INSTALL_PATH}/iskylims/settings.py` inside the `app` container +- `iskylims_documents` named volume -> `${INSTALL_PATH}/documents` +- `iskylims_static` named volume -> `${INSTALL_PATH}/static` + +If you override the compose file, ensure these mounts exist to keep logs and documents persistent. + +Create host directories before the first deployment: ```bash -cd < your personal folder > -git clone https://github.com/BU-ISCIII/iskylims.git iskylims -cd iskylims +sudo mkdir -p /var/log/local/iskylims/apps +sudo mkdir -p /var/log/local/iskylims/apache +sudo mkdir -p /conf +sudo chown -R : /var/log/local/iskylims/apps ``` -#### Create iskylims database and grant permissions - -1. Create a new database named "iskylims" (this is mandatory) -2. Create a new user with permission to read and modify that database. -3. Write down user, passwd and db server info. +For hardened/rootless Podman hosts, run the host preparation script as the same +user that starts the containers. The script pre-creates Apache log files, fixes +rootless Podman ownership for the UBI httpd user, and applies SELinux container +labels when SELinux is enabled: -#### Configuration settings +```bash +bash hardening.sh +``` -Copy the initial setting template into a file named install_settings.txt +If an administrator runs it as root, set `PODMAN_USER` to the user that starts +the rootless containers: ```bash -cp conf/template_install_settings.txt install_settings.txt +PODMAN_USER=bioinfo bash hardening.sh ``` -Open with your favourite editor the configuration file to set your own values for -database ,email settings and the local IP of the server where iSkyLIMS will run. +#### Apache reverse proxy (container) + Gunicorn + +For production, the `app` container runs `gunicorn` (not `manage.py runserver`) and the `apache` service in `docker-compose.prod.yml` acts as the reverse proxy. + +Static files: + +- The app collects static files into `${INSTALL_PATH}/static`. +- `docker-compose.prod.yml` shares that directory with the `apache` service through the named volume `iskylims_static`. +- The reverse proxy config serves `/static` directly from `${INSTALL_PATH}/static`. + +During `container_install.sh`, `conf/iskylims_apache_reverse_proxy.conf` and `conf/iskylims_apache_logs.conf` are rendered and copied to `${APACHE_CONF_PATH}` on the host. If `APACHE_CONF_PATH` is empty, they are copied to `${INSTALL_PATH}/conf`. The reverse proxy `ServerName`, forwarded host, and access/error log file names are generated from `DNS_URL` in the selected install config. Edit those copied files for runtime Apache changes after deployment. + +`container_install.sh` prepares a host-side Django `settings.py` bind source at `${DJANGO_SETTINGS_PATH}`, or at `${INSTALL_PATH}/iskylims/settings.py` when `DJANGO_SETTINGS_PATH` is empty. During the bootstrap step, `install.sh` updates that bind-mounted file from `conf/template_settings.txt` and the selected install config, preserving an existing `SECRET_KEY`. Runtime settings can then be edited and the container restarted without rebuilding the image. + +If you need a different runtime root, set `INSTALL_PATH` in the install config file or export `APP_INSTALL_PATH` before running `container_install.sh`. If you need Apache configs or Django settings outside the app runtime root, set `APACHE_CONF_PATH` or `DJANGO_SETTINGS_PATH` in the install config file, or export them before running `container_install.sh`. + +`container_install.sh` creates `${APP_INSTALL_PATH}/conf`, `${APACHE_CONF_PATH:-${APP_INSTALL_PATH}/conf}`, `/var/log/local/iskylims/apps`, and `/var/log/local/iskylims/apache` before `compose up`, copies both Apache config files there, prepares the bind-mounted Django settings file if it does not exist, passes `APP_INSTALL_PATH`, `APACHE_CONF_PATH`, and `DJANGO_SETTINGS_PATH` into Compose, and then runs `install.sh --bootstrap ...` inside the `app` container. The container image already contains the staged Django project and virtualenv under `${APP_INSTALL_PATH}`; the bootstrap step updates settings, applies migrations, optional scripts/fixtures, and refreshes `${INSTALL_PATH}/static`, while the Apache container keeps using the host log path `/var/log/local/iskylims/apache`. + +SELinux note for pre-production and production: + +- Ensure `/var/log/local/iskylims/apache` is writable by the container runtime and labeled for containers, for example `container_file_t`. +- If the host path is already labeled `container_file_t`, do not add `:Z` to the Apache log bind mount. `:Z` forces a relabel and may fail with `lsetxattr(... container_file_t ...): operation not permitted`. +- A quick check is: ```bash -nano install_settings.txt +ls -ldZ /var/log/local/iskylims/apache ``` -#### Run installation script +- Expected example: + +```text +system_u:object_r:container_file_t:s0 +``` + +- If Apache fails on startup with `ModSecurity: Failed to open debug log file: /var/log/httpd/modsec_debug.log`, remove any stale host file and recreate/restart the container. In practice, deleting `/var/log/local/iskylims/apache/modsec_debug.log` has been enough when the existing inode had bad permissions/label state. + +#### Cron jobs inside the container -iSkyLIMS should be installed on the "/opt" directory. +Cron runs via `supercronic`, started by the container entrypoint script. The script writes the django-crontab entries to `${APP_INSTALL_PATH}/cron/iskylims` and starts `supercronic` as the non-root app user. -You will need sudo privileges for installing dependencies. In order to handle different installation responsibilities inside the organization, where you may not be the person with root privileges, our instalation script has these options in ```--install``` parameter: +If you change `CRONJOBS`, rebuild or restart the container to regenerate the cron file. -- dep: to install the software packages as well as python packages inside the virtual environment. Root is needed. -- app: to install only the iSkyLIMS application software without need of being root. -- full: if you directly have root permissions you can install both deps and app at the same time with this option. +### Manage containers after installation -Execute one of the following commands in a linux terminal to install, according as -above description. +After a production install, use the generated `.env.prod.file` whenever you run Compose directly. This keeps paths, UID/GID, ports, and Gunicorn settings aligned with the install config. + +Docker Compose examples: ```bash -# to install only software packages dependences -sudo bash install.sh --install dep +docker compose --env-file .env.prod.file -f docker-compose.prod.yml ps +docker compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app +docker compose --env-file .env.prod.file -f docker-compose.prod.yml restart app +docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d +``` + +Podman Compose examples: + +```bash +podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps +podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app +podman compose --env-file .env.prod.file -f docker-compose.prod.yml restart app +podman compose --env-file .env.prod.file -f docker-compose.prod.yml up -d +``` + +If you edit container runtime values in the install config, rerun `container_install.sh --install_conf ` so `.env.prod.file` and the running containers are regenerated consistently. + +### Upgrade docker deployment + +Keep the same `APP_UID`/`APP_GID` values in the selected install config before running an upgrade. -# to install only iSkyLIMS application -bash install.sh --install app +Re-deploy the application container against an existing production database: -# to install both software -sudo bash install.sh --install full +```bash +bash container_install.sh --install_conf conf/my_prod_settings.txt --action upgrade 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log ``` -### Upgrade to iSkyLIMS version 3.0.0 +The upgrade path rebuilds/restarts the container and runs `install.sh --bootstrap upgrade --tables` inside the app container. The app files are already baked into the rebuilt image; the bootstrap phase applies migrations with `--fake-initial`, refreshes `conf/first_install_tables.json`, refreshes static files, and skips superuser/demo/test data loading. + +### Upgrade docker deployment v3.0.0 to 3.1.0 + +#### Back up first + +Run the backup steps in [Backups](#backups) first. + +For 3.0.0 -> 3.1.0, export the LibraryPool mapping first, then run the upgrade with pre/post scripts: -If you have already iSkyLIMS on version 2.3.0 you can upgrade to the latest stable version 3.0.0. +```bash +mysql --user= --password= --host= --port= iskylims \ + -e "SELECT id, run_process_id_id FROM wetlab_library_pool" \ + > /tmp/library_pool_run_process.tsv +``` + +#### Refresh code and settings -Version 3.0.0 is a major release with important upgrades in third parties dependencies like bootstrap. Also, we 've done a huge work on refactoring and variables/function renaming that affects the database. For more details about the changes see the release notes. +```bash +cd /iskylims +git pull +cp conf/docker_production_settings.txt myprod_settings.txt +sudo nano myprod_settings.txt +``` -#### Pre-requisites +Ensure the file uses Linux-friendly encoding (UTF-8/ASCII) if you edit it on Windows. -Because in this upgrade many tables in database are modified it is required that you backup: +Keep the same `APP_UID`/`APP_GID` values in the selected install config before running the 3.0.0 -> 3.1.0 upgrade. -- iSkyLIMS database -- iSkyLIMS folder (complete installation folder, p.e /opt/iSkyLIMS) +Run upgrade command: -It is highly recomended that you made these backups and keep them safely in case of upgrade failure, to recover your system. +```bash +bash container_install.sh --engine podman --install_conf my_prod_settings.txt --action upgrade \ + --script_before convert_rawtop_counter_to_int \ + --script_after library_pool_to_many_relation,/tmp/library_pool_run_process.tsv 2>&1 | tee ./iskylims_docker_install_$(date +%Y%m%d_%H%M%S).log +``` -#### Clone github repository +## Bare-metal deployment (Ubuntu/CentOS) -We've also change the way that iSkyLIMS is installed and upgraded. From now on iskylims is downloaded in a user folder and installed elsewhere (p.e /opt/). +### Install -Open a linux terminal and move to a directory where iSkyLIMS code will be -downloaded +#### Clone the repository ```bash -cd < your personal folder > -git clone https://github.com/BU-ISCIII/iSkyLIMS.git iskylims +cd +git clone https://github.com/BU-ISCIII/iskylims.git iskylims cd iskylims ``` -#### Configuration settings +#### Prepare the database -Copy the initial setting template into a file named install_settings.txt +Create the database and application user following [Database creation, users and grants](#database-creation-users-and-grants), then note DB host/port/user/password for `install_settings.txt`. + +#### Configure install_settings.txt ```bash cp conf/template_install_settings.txt install_settings.txt +nano install_settings.txt ``` -Open with your favourite editor the configuration file to set your own values for -database ,email settings and the local IP of the server where iSkyLIMS will run. -> If you use a windows-based system for modifying the file, make sure the file is saved using a linux-friendly encoding like ASCII or UTF-8 +Set your database, email, server IP/URL, and logging preferences in that file. + +#### Run install.sh + +iSkyLIMS is installed to `/opt/iskylims` by default. The single `install.sh` script handles both dependencies and the app; choose what you need with `--install`: + +- `dep`: install system and Python dependencies (requires sudo). +- `app`: deploy iSkyLIMS code, update settings, run migrations, and collect static files (no sudo needed). +- `full`: run both stages in sequence. + +The staged/bootstrap split added for container images is internal. Bare-metal commands do not change: `--install` and `--upgrade` still run the complete dependency, application, and database workflow documented here. + +Examples: ```bash +# only software dependencies +sudo bash install.sh --install dep + +# only iSkyLIMS application +bash install.sh --install app --git_revision main --tables + +# dependencies + application +sudo bash install.sh --install full --git_revision main --tables +``` + +- Add `--tables` to load the initial fixtures on first-time installs, or `--skip_tables` if you want to skip them. +- Capture logs for troubleshooting with `tee`: + + ```bash + sudo bash install.sh --install full --git_revision main --tables 2>&1 | tee ./iskylims_install_$(date +%Y%m%d_%H%M%S).log + ``` + +- If Apache is managed elsewhere, skip the automatic restart with `--skip_apache_restart`. + +### Upgrade (3.0.0 to 3.1.0) + +Follow these steps to move from version 3.0.0 to the 3.1.x series. + +#### Back up first + +Run the backup steps in [Backups](#backups) first. +- Additionally, back up the full installation folder (for example `/opt/iskylims`) for bare-metal rollback. +- If you use library pools, export them before upgrading: + + ```bash +mysql --user= --password= --host= --port= iskylims \ + -e "SELECT id, run_process_id_id FROM wetlab_library_pool" \ + > /tmp/library_pool_run_process.tsv + ``` + +#### Refresh code and settings + +```bash +cd /iskylims +git pull +cp conf/template_install_settings.txt install_settings.txt sudo nano install_settings.txt ``` -#### Running upgrade script +Ensure the file uses Linux-friendly encoding (UTF-8/ASCII) if you edit it on Windows. -If your organization requires that dependencies / stuff that needs root are installed by a different person that install the application the you can use the install script in several steps as follows. +#### Run upgrade steps requiring root -First you need to rename the folder app name in the installation folder (`/opt/iSkyLIMS`): +Update system and Python dependencies: -##### Steps requiring root +```bash +sudo bash install.sh --upgrade dep 2>&1 | tee install_full.log +``` + +Make sure the installation directory permissions allow the non-root step to write to `/opt/iskylims` (adapt your hardening script if paths changed). + +#### Run upgrade steps without root + +Upgrade the application code and database: ```bash -# You need root for this operation -sudo mv /opt/iSkyLIMS /opt/iskylims +# with library pool restore +bash install.sh --upgrade app --git_revision main \ + --script_before convert_rawtop_counter_to_int \ + --script_after library_pool_to_many_relation,/tmp/library_pool_run_process.tsv +``` + +Upgrades regenerate migrations and apply them with `--fake-initial` so existing tables remain intact, matching the Docker workflow. + +## Common operations (Docker + bare-metal) + +### Database creation, users and grants + +Run as MySQL root: + +```sql +CREATE DATABASE IF NOT EXISTS iskylims CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE USER IF NOT EXISTS 'iskylims'@'%' IDENTIFIED BY 'djangopass'; +CREATE USER IF NOT EXISTS 'iskylims'@'localhost' IDENTIFIED BY 'djangopass'; + +GRANT ALL PRIVILEGES ON iskylims.* TO 'iskylims'@'%'; +GRANT ALL PRIVILEGES ON iskylims.* TO 'iskylims'@'localhost'; + +FLUSH PRIVILEGES; +``` + +Verification: + +```sql +SHOW GRANTS FOR 'iskylims'@'%'; ``` -Make sure that the installation folder has the correct permissions so the person installing the app can write in that folder. +### Backups + +Database dump: ```bash -# In case you have a script for this task. You'll need to adjust this script according to the name changing: /opt/iSkyLIMS to /opt/iskylims -/scripts/hardening.sh +mysqldump -h -P -u iskylims -p iskylims > iskylims_$(date +%Y%m%d_%H%M%S).sql ``` -In the linux terminal execute one of the following command that fit better to you: +Logs archive: ```bash -# to upgrade only software packages dependences. NEEDS ROOT. -sudo bash install.sh --upgrade dep +tar -czf iskylims_app_logs_$(date +%Y%m%d_%H%M%S).tgz -C /var/log/local/iskylims/apps . -# to install both software. NEEDS ROOT. -sudo bash install.sh --upgrade full --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables +tar -czf iskylims_apache_logs_$(date +%Y%m%d_%H%M%S).tgz -C /var/log/local/iskylims/apache . ``` -##### Steps not requiring root +Documents volume archive: -Next you need to upgrade iskylims app. Please use the command below: +```bash +docker run --rm -v iskylims_documents:/from -v "$PWD":/to alpine \ + tar -czf /to/iskylims_documents_$(date +%Y%m%d_%H%M%S).tgz -C /from . +``` + +With Podman, use the same command replacing `docker` with `podman`. + +Suggested order before upgrades: + +1. DB dump +2. Documents volume archive +3. Logs archive + +### Restore / rollback + +Restore DB: ```bash -# to upgrade only iSkyLIMS application including changes required in this release. DOES NOT NEED ROOT. -bash install.sh --upgrade app --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables +mysql -h -P -u iskylims -p iskylims < iskylims_YYYYMMDD_HHMMSS.sql ``` -Make sure that the installation folder has the correct permissions. +Restore documents volume: ```bash -# In case you have a script for this task. Some paths have changed in this version, so you may need to adjust your hardening script. -/scripts/hardening.sh +docker run --rm -v iskylims_documents:/to -v "$PWD":/from alpine \ + sh -lc "cd /to && tar -xzf /from/iskylims_documents_YYYYMMDD_HHMMSS.tgz" ``` -#### What to do if something fails +With Podman, use the same command replacing `docker` with `podman`. -When we upgrade using the installation script we are performing several changes in the database. If something fails we need to restore the app situation before anything happened and start all over. +Restore logs: -We need to copy back the full `/opt/iSkyLIMS` folder back to `/opt` (or your installation path preference), and restore the database doing something like this: +```bash +mkdir -p /var/log/local/iskylims/apps +tar -xzf iskylims_app_logs_YYYYMMDD_HHMMSS.tgz -C /var/log/local/iskylims/apps + +mkdir -p /var/log/local/iskylims/apache +tar -xzf iskylims_apache_logs_YYYYMMDD_HHMMSS.tgz -C /var/log/local/iskylims/apache +``` + +Bare-metal full rollback example: ```bash sudo rm -rf /opt/iskylims sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ sudo /scripts/hardening.sh -mysql -u iskylims -h dmysqlps.isciiides.es -# drop database iskylims; -# create database iskylims; -mysql -u iskylims -h dmysqlps.isciiides.es iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_202310160737.sql +mysql -u iskylims -p -h iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_YYYYMMDDHHMM.sql +``` + +### What to do if something fails + +When install/upgrade fails, restore the previous state and retry with logs enabled. + +Quick diagnostics: + +```bash +# bare-metal +cd /opt/iskylims +python manage.py check + +# docker +docker compose --env-file .env.prod.file -f docker-compose.prod.yml ps +docker compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app +# podman +podman compose --env-file .env.prod.file -f docker-compose.prod.yml ps +podman compose --env-file .env.prod.file -f docker-compose.prod.yml logs --tail 200 app ``` -### Final configuration steps +If you suspect a corrupted image/build cache in Docker: -#### SAMBA configurarion +```bash +docker compose --env-file .env.prod.file -f docker-compose.prod.yml build --no-cache app +docker compose --env-file .env.prod.file -f docker-compose.prod.yml up -d --force-recreate app +``` + +## Final configuration steps + +### SAMBA configurarion - Login with admin account. - Go to Massive sequencing @@ -254,17 +566,78 @@ mysql -u iskylims -h dmysqlps.isciiides.es iskylims < /home/dadmin/backup_prod/b - Fill the form with the appropiate params for the samba shared folder: ![samba form](img/samba_form.png) -#### Email verification +### Email verification - Go to Massive sequencing - Go to Configuration -> Email configuration - Fill the form with the needed params for your email configuration and try to send a test email. -#### Configure Apache server +## Developer notes + +### Django migrations workflow + +Migrations are committed to the repo. Do not run `makemigrations` during install/upgrade. + +Baseline + upgrade flow for new releases: + +1. Generate baseline migrations from the last stable tag (example 3.0.0). +2. Commit the baseline migrations. +3. Generate new migrations on `develop` for schema changes and commit them. +4. Upgrades run `migrate --fake-initial` once to align existing tables, then `migrate` to apply the new migration files. + +### Persistent host paths + +See [Persist logs/documents on the host](#persist-logsdocuments-on-the-host) in the production deployment section. + +### Configure Apache server + +These steps apply to bare-metal Apache installations. Docker production deployments use the `apache` container described above and do not require copying configs into `/etc/apache2` or `/etc/httpd`. + +Copy the apache configuration file according to your distribution inside the apache configuration directory and rename it to `iskylims.conf`. + +Typical config locations: + +- Ubuntu/Debian: `/etc/apache2/sites-available/iskylims.conf` (enable with `a2ensite`) +- CentOS/RHEL: `/etc/httpd/conf.d/iskylims.conf` + +Suggested steps (host Apache as reverse proxy): + +1. Copy the example config: + + ```bash + sudo cp conf/iskylims_apache_reverse_proxy.conf /etc/apache2/sites-available/iskylims.conf + # CentOS/RHEL: + # sudo cp conf/iskylims_apache_reverse_proxy.conf /etc/httpd/conf.d/iskylims.conf + ``` + +2. Edit the config: + + - Set `ServerName` + - Ensure `ProxyPass` points to `http://localhost:8001/` + - Ensure `Alias /static/ /opt/iskylims/static/` + +3. Create the static folder on the host: + + ```bash + sudo mkdir -p /opt/iskylims/static + ``` + +4. Enable required modules (Ubuntu/Debian): + + ```bash + sudo a2enmod proxy proxy_http headers + sudo a2ensite iskylims.conf + ``` + +5. Reload Apache: -Copy the apache configuration file according to your distribution inside the apache configutation directory and rename it to iskylims.conf + ```bash + sudo systemctl reload apache2 + # CentOS/RHEL: + # sudo systemctl reload httpd + ``` -#### Verification of the installation +### Verification of the installation Open the navigator and type "localhost" or the "server local IP" and check that iSkyLIMs is running. @@ -275,6 +648,6 @@ You can also check some of the functionality, while also checking samba and data - Check all tabs so every connectin is successful. - Run the 3 tests for each sequencing machine: MiSeq, NextSeq and NovaSeq. -### iSkyLIMS documentation +## iSkyLIMS documentation iSkyLIMS documentation is available at [https://iskylims.readthedocs.io/en/latest](https://iskylims.readthedocs.io/en/latest) diff --git a/UPGRADE_SCRIPTS.md b/UPGRADE_SCRIPTS.md new file mode 100644 index 000000000..919af840f --- /dev/null +++ b/UPGRADE_SCRIPTS.md @@ -0,0 +1,36 @@ +# Upgrade scripts + +This file lists data migration scripts and the version range they apply to. +Run them with: + +```bash +python manage.py runscript +``` + +## 3.0.0 -> 3.1.0 + +- Run order: + 1. Export LibraryPool run_process_id data (before migrations). + 2. Run `convert_rawtop_counter_to_int` before migrations. + 3. Run migrations. + 4. Run `library_pool_to_many_relation` after migrations with the exported file. + +- Export example: + ```bash + mysql -u -p -h -D \ + -e "SELECT id, run_process_id_id FROM wetlab_library_pool" \ + > /tmp/library_pool_run_process.tsv + ``` + +- `wetlab/scripts/convert_rawtop_counter_to_int.py` (script name: `convert_rawtop_counter_to_int`) +- `wetlab/scripts/library_pool_to_many_relation.py` (script name: `library_pool_to_many_relation`) + +## 2.3.0 -> 3.0.0 + +- `core/scripts/rename_app_name.py` (script name: `rename_app_name`) +- `core/scripts/migrate_sample_type.py` (script name: `migrate_sample_type`) +- `core/scripts/migrate_optional_values.py` (script name: `migrate_optional_values`) + +## 2.3.0 -> 2.3.1 + +- `drylab/scripts/drylab_service_state_migration.py` (script name: `drylab_service_state_migration`) diff --git a/clinic/.gitignore b/clinic/.gitignore deleted file mode 100644 index 87884db5e..000000000 --- a/clinic/.gitignore +++ /dev/null @@ -1,113 +0,0 @@ -## Custom -*.xml -*.bin -migrations/ -tmp/ -logs/ -tests/ - - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/clinic/migrations/0001_initial.py b/clinic/migrations/0001_initial.py new file mode 100644 index 000000000..2de933cee --- /dev/null +++ b/clinic/migrations/0001_initial.py @@ -0,0 +1,307 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("core", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="ClinicSampleRequest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("entry_order", models.CharField(blank=True, max_length=8, null=True)), + ( + "confirmation_code", + models.CharField(blank=True, max_length=80, null=True), + ), + ("priority", models.IntegerField(blank=True, null=True)), + ("comments", models.CharField(blank=True, max_length=255, null=True)), + ("service_date", models.DateTimeField(blank=True, null=True)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="ClinicSampleState", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("clinic_state", models.CharField(max_length=20)), + ], + ), + migrations.CreateModel( + name="ConfigSetting", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("configuration_name", models.CharField(max_length=80)), + ( + "configuration_value", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="FamilyRelatives", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("relationship", models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name="PatientData", + fields=[ + ( + "patien_core", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="core.patientcore", + ), + ), + ("address", models.CharField(blank=True, max_length=255, null=True)), + ("phone", models.CharField(blank=True, max_length=20, null=True)), + ("email", models.CharField(blank=True, max_length=50, null=True)), + ("birthday", models.DateTimeField(blank=True, null=True)), + ("smoker", models.CharField(blank=True, max_length=20, null=True)), + ( + "notification_preference", + models.CharField(blank=True, max_length=20, null=True), + ), + ("comments", models.CharField(blank=True, max_length=255, null=True)), + ], + ), + migrations.CreateModel( + name="ServiceUnits", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("service_unit_name", models.CharField(max_length=80)), + ], + ), + migrations.CreateModel( + name="ResultParameterValue", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("parameterValue", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "clinicSample_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="clinic.clinicsamplerequest", + ), + ), + ( + "parameter_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.protocolparameters", + ), + ), + ], + ), + migrations.CreateModel( + name="PatientHistory", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("entry_date", models.DateTimeField(blank=True, null=True)), + ("description", models.CharField(max_length=255)), + ( + "patient_core", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientcore", + ), + ), + ], + ), + migrations.CreateModel( + name="Family", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("family_id", models.CharField(max_length=50)), + ("relative_1", models.CharField(max_length=50)), + ("relative_2", models.CharField(max_length=50)), + ("affected", models.BooleanField()), + ( + "family_comments", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "family_relatives_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="clinic.familyrelatives", + ), + ), + ( + "patien_core_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientcore", + ), + ), + ], + ), + migrations.CreateModel( + name="Doctor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("doctor_name", models.CharField(max_length=80)), + ( + "service_unit_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="clinic.serviceunits", + ), + ), + ], + ), + migrations.AddField( + model_name="clinicsamplerequest", + name="clinic_sample_state", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="clinic.clinicsamplestate", + ), + ), + migrations.AddField( + model_name="clinicsamplerequest", + name="doctor_id", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="clinic.doctor", + ), + ), + migrations.AddField( + model_name="clinicsamplerequest", + name="patient_core", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientcore", + ), + ), + migrations.AddField( + model_name="clinicsamplerequest", + name="sample_core", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.samples" + ), + ), + migrations.AddField( + model_name="clinicsamplerequest", + name="sample_request_user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="clinicsamplerequest", + name="service_unit_id", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="clinic.serviceunits", + ), + ), + ] diff --git a/clinic/migrations/__init__.py b/clinic/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/clinic/utils/patient.py b/clinic/utils/patient.py index 86c0303e5..eed421778 100644 --- a/clinic/utils/patient.py +++ b/clinic/utils/patient.py @@ -158,10 +158,10 @@ def display_one_patient_info(p_id, app_name): for pat_project in pat_projects: project_name = pat_project.get_project_name() - patient_info["project_information"][ - project_name - ] = core.utils.patient_projects.get_project_field_values( - pat_project.get_project_id(), patient_core_obj + patient_info["project_information"][project_name] = ( + core.utils.patient_projects.get_project_field_values( + pat_project.get_project_id(), patient_core_obj + ) ) # get other projects that patient could belongs to @@ -180,9 +180,9 @@ def display_one_patient_info(p_id, app_name): clinic_samples = clinic.models.SampleRequest.objects.filter( patient_core=patient_core_obj ) - patient_info[ - "samples_heading" - ] = clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_DATA_IN_PATIENT_INFO + patient_info["samples_heading"] = ( + clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_DATA_IN_PATIENT_INFO + ) patient_info["samples_data"] = [] for clinic_sample in clinic_samples: sample_obj = clinic_sample.get_core_sample_obj() @@ -362,20 +362,20 @@ def read_batch_patient_file(batch_file): num_rows, num_cols = patient_batch_df.shape if num_cols != 5: - error_data[ - "ERROR" - ] = clinic.clinic_config.ERROR_MESSAGE_FOR_INVALID_PATIENT_BATCH_FILE + error_data["ERROR"] = ( + clinic.clinic_config.ERROR_MESSAGE_FOR_INVALID_PATIENT_BATCH_FILE + ) return error_data for field in patient_batch_heading: if field not in patient_batch_df.columns: - error_data[ - "ERROR" - ] = clinic.clinic_config.ERROR_MESSAGE_FOR_INVALID_PATIENT_BATCH_FILE + error_data["ERROR"] = ( + clinic.clinic_config.ERROR_MESSAGE_FOR_INVALID_PATIENT_BATCH_FILE + ) return error_data if num_rows == 0: - error_data[ - "ERROR" - ] = clinic.clinic_config.ERROR_MESSAGE_FOR_EMPTY_PATIENT_BATCH_FILE + error_data["ERROR"] = ( + clinic.clinic_config.ERROR_MESSAGE_FOR_EMPTY_PATIENT_BATCH_FILE + ) return error_data p_projects = set(patient_batch_df["patientProjects"]) for p_project in p_projects: @@ -384,19 +384,18 @@ def read_batch_patient_file(batch_file): if not core.models.PatientProjects.objects.filter( project_name__exact=p_project ).exists(): - error_data[ - "ERROR" - ] = clinic.clinic_config.ERROR_MESSAGE_NOT_VALID_PROJECT_IN_BATCH_FILE + [ - p_project - ] + error_data["ERROR"] = ( + clinic.clinic_config.ERROR_MESSAGE_NOT_VALID_PROJECT_IN_BATCH_FILE + + [p_project] + ) return error_data for index, row_data in patient_batch_df.iterrows(): patient_data = {} for index in range(len(clinic.clinic_config.PATIENT_BATCH_FILE_HEADING)): - patient_data[ - clinic.clinic_config.PATIENT_BATCH_FILE_HEADING[index] - ] = row_data[patient_batch_heading[index]] + patient_data[clinic.clinic_config.PATIENT_BATCH_FILE_HEADING[index]] = ( + row_data[patient_batch_heading[index]] + ) patient_batch_data.append(patient_data) return patient_batch_data diff --git a/clinic/utils/samples.py b/clinic/utils/samples.py index de71e052f..6c372ace6 100644 --- a/clinic/utils/samples.py +++ b/clinic/utils/samples.py @@ -69,13 +69,13 @@ def analyze_and_store_patient_data(user_post, user): suspicion_data ) if stored_samples: - analyze_data[ - "stored_samples_heading" - ] = clinic.clinic_config.HEADING_FOR_STORED_PATIENT_DATA + analyze_data["stored_samples_heading"] = ( + clinic.clinic_config.HEADING_FOR_STORED_PATIENT_DATA + ) if incomplete_clinic_samples_ids: - analyze_data[ - "heading" - ] = clinic.clinic_config.ADDITIONAL_HEADING_FOR_RECORDING_SAMPLES + analyze_data["heading"] = ( + clinic.clinic_config.ADDITIONAL_HEADING_FOR_RECORDING_SAMPLES + ) analyze_data["heading_length"] = len( clinic.clinic_config.ADDITIONAL_HEADING_FOR_RECORDING_SAMPLES ) @@ -175,9 +175,9 @@ def display_one_sample_info(id): sample_info["s_name"] = core_sample_obj.get_sample_name() sample_info["sample_core_info"] = clinic_sample_obj.get_sample_core_info() - sample_info[ - "sample_core_heading" - ] = clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_MAIN_INFORMATION + sample_info["sample_core_heading"] = ( + clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_MAIN_INFORMATION + ) p_main_info = clinic_sample_obj.get_patient_information() @@ -211,9 +211,9 @@ def display_one_sample_info(id): def display_sample_list(sample_c_list): display_sample_list_info = {} - display_sample_list_info[ - "heading" - ] = clinic.clinic_config.HEADING_SEARCH_LIST_SAMPLES_CLINIC + display_sample_list_info["heading"] = ( + clinic.clinic_config.HEADING_SEARCH_LIST_SAMPLES_CLINIC + ) sample_c_data = [] for sample_c in sample_c_list: sample_c_obj = clinic.models.ClinicSampleRequest.objects.get(pk__exact=sample_c) @@ -314,9 +314,9 @@ def get_clinic_samples_defined_state(user): for sample_obj in samples_obj: sample_information.append(sample_obj.get_info_for_defined_state()) samples_in_state["sample_information"] = sample_information - samples_in_state[ - "sample_heading" - ] = clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_DEFINED_STATE + samples_in_state["sample_heading"] = ( + clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_DEFINED_STATE + ) samples_in_state["length"] = len(sample_information) return samples_in_state else: @@ -346,9 +346,9 @@ def get_clinic_samples_patient_sequencing_state(user, state): sample_obj.get_info_for_patient_sequencing_state() ) samples_in_state["sample_information"] = sample_information - samples_in_state[ - "sample_heading" - ] = clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_PATIENT_SEQUENCING_STATE + samples_in_state["sample_heading"] = ( + clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_PATIENT_SEQUENCING_STATE + ) samples_in_state["length"] = len(sample_information) return samples_in_state else: @@ -388,9 +388,9 @@ def get_clinic_samples_pending_results(user, state): c_sample.append(sample_obj.get_id()) sample_information.append(c_sample) samples_in_state["sample_information"] = sample_information - samples_in_state[ - "sample_heading" - ] = clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_PENDING_RESULT_STATE + samples_in_state["sample_heading"] = ( + clinic.clinic_config.HEADING_FOR_DISPLAY_SAMPLE_PENDING_RESULT_STATE + ) samples_in_state["length"] = len(sample_information) return samples_in_state else: @@ -476,9 +476,9 @@ def get_samples_clinic_in_search(data_request): def prepare_patient_form(clinic_samples_ids): patient_info = {} patient_info["data"] = [] - patient_info[ - "heading" - ] = clinic.clinic_config.ADDITIONAL_HEADING_FOR_RECORDING_SAMPLES + patient_info["heading"] = ( + clinic.clinic_config.ADDITIONAL_HEADING_FOR_RECORDING_SAMPLES + ) heading_length = len(clinic.clinic_config.ADDITIONAL_HEADING_FOR_RECORDING_SAMPLES) for clinic_s_id in clinic_samples_ids: clinic_sample_obj = get_clinic_sample_obj_from_id(clinic_s_id) diff --git a/clinic/views.py b/clinic/views.py index b4789d614..eb620a0da 100644 --- a/clinic/views.py +++ b/clinic/views.py @@ -253,9 +253,9 @@ def create_sample_projects(request): def define_extraction_molecules(request): extraction_molecules = {} - extraction_molecules[ - "extract_molecule" - ] = core.utils.samples.get_sample_objs_in_state("defined", request.user) + extraction_molecules["extract_molecule"] = ( + core.utils.samples.get_sample_objs_in_state("defined", request.user) + ) if request.method == "POST" and request.POST["action"] == "continueWithMolecule": # processing the samples selected by user pass @@ -790,32 +790,31 @@ def pending_to_update(request): request.user ) - pending[ - "patient_update" - ] = clinic.utils.samples.get_clinic_samples_patient_sequencing_state( - request.user, "Patient update" + pending["patient_update"] = ( + clinic.utils.samples.get_clinic_samples_patient_sequencing_state( + request.user, "Patient update" + ) ) - pending[ - "sequencing" - ] = clinic.utils.samples.get_clinic_samples_patient_sequencing_state( - request.user, "Sequencing" + pending["sequencing"] = ( + clinic.utils.samples.get_clinic_samples_patient_sequencing_state( + request.user, "Sequencing" + ) ) - pending[ - "pending_protocol" - ] = clinic.utils.samples.get_clinic_samples_pending_results( - request.user, "Pending protocol" + pending["pending_protocol"] = ( + clinic.utils.samples.get_clinic_samples_pending_results( + request.user, "Pending protocol" + ) ) - pending[ - "pending_results" - ] = clinic.utils.samples.get_clinic_samples_pending_results( - request.user, "Pending results" + pending["pending_results"] = ( + clinic.utils.samples.get_clinic_samples_pending_results( + request.user, "Pending results" + ) ) - pending[ - "graphic_pending_samples" - ] = clinic.utils.samples.pending_clinic_samples_for_grafic(pending).render() - + pending["graphic_pending_samples"] = ( + clinic.utils.samples.pending_clinic_samples_for_grafic(pending).render() + ) return render(request, "clinic/pendingToUpdate.html", {"pending": pending}) diff --git a/conf/collection_index_kits/AmpliSeq CD Indexes Plate A..txt b/conf/collection_index_kits/AmpliSeq CD Indexes Plate A..txt new file mode 100644 index 000000000..109c6a605 --- /dev/null +++ b/conf/collection_index_kits/AmpliSeq CD Indexes Plate A..txt @@ -0,0 +1,225 @@ +[Version] +1 +[Name] +AmpliSeq CD Indexes Plate A +[PlateExtension] +amp384a +[Settings] +IndexOnly +Adapter CTGTCTCTTATACACATCT +[I7] +7005 GTGAATAT +7006 ACAGGCGC +7007 CATAGAGT +7008 TGCGAGAC +7015 TCTCTACT +7016 CTCTCGTC +7017 CCAAGTCT +7018 TTGGACTC +7023 GCAGAATT +7024 ATGAGGCC +7025 ACTAAGAT +7026 GTCGGAGC +[I5] +5001 AGCGCTAG +5002 GATATCGA +5007 ACATAGCG +5008 GTGCGATA +5009 CCAACAGA +5010 TTGGTGAG +5013 AACCGCGG +5014 GGTTATAA +[IndexPlateLayout] +A01 7005 5001 +A02 7015 5002 +A03 7006 5007 +A04 7007 5008 +A05 7016 5009 +A06 7008 5010 +A07 7018 5001 +A08 7023 5002 +A09 7017 5007 +A10 7025 5008 +A11 7024 5009 +A12 7026 5010 +B01 7006 5002 +B02 7016 5001 +B03 7005 5008 +B04 7008 5007 +B05 7015 5010 +B06 7007 5009 +B07 7017 5002 +B08 7024 5001 +B09 7018 5008 +B10 7026 5007 +B11 7023 5010 +B12 7025 5009 +C01 7016 5007 +C02 7008 5008 +C03 7015 5009 +C04 7006 5010 +C05 7007 5013 +C06 7005 5014 +C07 7024 5007 +C08 7026 5008 +C09 7023 5009 +C10 7017 5010 +C11 7025 5013 +C12 7018 5014 +D01 7015 5008 +D02 7007 5007 +D03 7016 5010 +D04 7005 5009 +D05 7008 5014 +D06 7006 5013 +D07 7023 5008 +D08 7025 5007 +D09 7024 5010 +D10 7018 5009 +D11 7026 5014 +D12 7017 5013 +E01 7017 5009 +E02 7025 5010 +E03 7018 5013 +E04 7023 5014 +E05 7026 5001 +E06 7024 5002 +E07 7006 5009 +E08 7007 5010 +E09 7005 5013 +E10 7015 5014 +E11 7008 5001 +E12 7016 5002 +F01 7018 5010 +F02 7026 5009 +F03 7017 5014 +F04 7024 5013 +F05 7025 5002 +F06 7023 5001 +F07 7005 5010 +F08 7008 5009 +F09 7006 5014 +F10 7016 5013 +F11 7007 5002 +F12 7015 5001 +G01 7026 5013 +G02 7024 5014 +G03 7025 5001 +G04 7018 5002 +G05 7023 5007 +G06 7017 5008 +G07 7008 5013 +G08 7016 5014 +G09 7007 5001 +G10 7005 5002 +G11 7015 5007 +G12 7006 5008 +H01 7025 5014 +H02 7023 5013 +H03 7026 5002 +H04 7017 5001 +H05 7024 5008 +H06 7018 5007 +H07 7007 5014 +H08 7015 5013 +H09 7008 5002 +H10 7006 5001 +H11 7016 5008 +H12 7005 5007 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/AmpliSeq CD Indexes Plate B.txt b/conf/collection_index_kits/AmpliSeq CD Indexes Plate B.txt new file mode 100644 index 000000000..17052d464 --- /dev/null +++ b/conf/collection_index_kits/AmpliSeq CD Indexes Plate B.txt @@ -0,0 +1,226 @@ + +[Version] +1 +[Name] +AmpliSeq CD Indexes Plate B +[PlateExtension] +amp384b +[Settings] +IndexOnly +Adapter CTGTCTCTTATACACATCT +[I7] +7027 AGCCTCAT +7028 GATTCTGC +7029 TCGTAGTG +7030 CTACGACA +7035 ATGGCATG +7036 GCAATGCA +7039 CTTATCGG +7040 TCCGCTAA +7041 GATCTATC +7042 AGCTCGCT +7047 ACACTAAG +7048 GTGTCGGA +[I5] +5001 AGCGCTAG +5002 GATATCGA +5007 ACATAGCG +5008 GTGCGATA +5009 CCAACAGA +5010 TTGGTGAG +5013 AACCGCGG +5014 GGTTATAA +[IndexPlateLayout] +A01 7027 5001 +A02 7035 5002 +A03 7028 5007 +A04 7029 5008 +A05 7036 5009 +A06 7030 5010 +A07 7040 5001 +A08 7041 5002 +A09 7039 5007 +A10 7047 5008 +A11 7042 5009 +A12 7048 5010 +B01 7028 5002 +B02 7036 5001 +B03 7027 5008 +B04 7030 5007 +B05 7035 5010 +B06 7029 5009 +B07 7039 5002 +B08 7042 5001 +B09 7040 5008 +B10 7048 5007 +B11 7041 5010 +B12 7047 5009 +C01 7036 5007 +C02 7030 5008 +C03 7035 5009 +C04 7028 5010 +C05 7029 5013 +C06 7027 5014 +C07 7042 5007 +C08 7048 5008 +C09 7041 5009 +C10 7039 5010 +C11 7047 5013 +C12 7040 5014 +D01 7035 5008 +D02 7029 5007 +D03 7036 5010 +D04 7027 5009 +D05 7030 5014 +D06 7028 5013 +D07 7041 5008 +D08 7047 5007 +D09 7042 5010 +D10 7040 5009 +D11 7048 5014 +D12 7039 5013 +E01 7039 5009 +E02 7047 5010 +E03 7040 5013 +E04 7041 5014 +E05 7048 5001 +E06 7042 5002 +E07 7028 5009 +E08 7029 5010 +E09 7027 5013 +E10 7035 5014 +E11 7030 5001 +E12 7036 5002 +F01 7040 5010 +F02 7048 5009 +F03 7039 5014 +F04 7042 5013 +F05 7047 5002 +F06 7041 5001 +F07 7027 5010 +F08 7030 5009 +F09 7028 5014 +F10 7036 5013 +F11 7029 5002 +F12 7035 5001 +G01 7048 5013 +G02 7042 5014 +G03 7047 5001 +G04 7040 5002 +G05 7041 5007 +G06 7039 5008 +G07 7030 5013 +G08 7036 5014 +G09 7029 5001 +G10 7027 5002 +G11 7035 5007 +G12 7028 5008 +H01 7047 5014 +H02 7041 5013 +H03 7048 5002 +H04 7039 5001 +H05 7042 5008 +H06 7040 5007 +H07 7029 5014 +H08 7035 5013 +H09 7030 5002 +H10 7028 5001 +H11 7036 5008 +H12 7027 5007 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/AmpliSeq CD Indexes Plate C.txt b/conf/collection_index_kits/AmpliSeq CD Indexes Plate C.txt new file mode 100644 index 000000000..8feeaf01b --- /dev/null +++ b/conf/collection_index_kits/AmpliSeq CD Indexes Plate C.txt @@ -0,0 +1,225 @@ +[Version] +1 +[Name] +AmpliSeq CD Indexes Plate C +[PlateExtension] +amp384c +[Settings] +IndexOnly +Adapter CTGTCTCTTATACACATCT +[I7] +7005 GTGAATAT +7006 ACAGGCGC +7007 CATAGAGT +7008 TGCGAGAC +7015 TCTCTACT +7016 CTCTCGTC +7017 CCAAGTCT +7018 TTGGACTC +7023 GCAGAATT +7024 ATGAGGCC +7025 ACTAAGAT +7026 GTCGGAGC +[I5] +5017 CTAGCTTG +5018 TCGATCCA +5025 ATACCAAG +5026 GCGTTGGA +5027 CTTCACGG +5028 TCCTGTAA +5031 CGCTCGTG +5032 TATCTACA +[IndexPlateLayout] +A01 7005 5017 +A02 7015 5018 +A03 7006 5025 +A04 7007 5026 +A05 7016 5027 +A06 7008 5028 +A07 7018 5017 +A08 7023 5018 +A09 7017 5025 +A10 7025 5026 +A11 7024 5027 +A12 7026 5028 +B01 7006 5018 +B02 7016 5017 +B03 7005 5026 +B04 7008 5025 +B05 7015 5028 +B06 7007 5027 +B07 7017 5018 +B08 7024 5017 +B09 7018 5026 +B10 7026 5025 +B11 7023 5028 +B12 7025 5027 +C01 7016 5025 +C02 7008 5026 +C03 7015 5027 +C04 7006 5028 +C05 7007 5031 +C06 7005 5032 +C07 7024 5025 +C08 7026 5026 +C09 7023 5027 +C10 7017 5028 +C11 7025 5031 +C12 7018 5032 +D01 7015 5026 +D02 7007 5025 +D03 7016 5028 +D04 7005 5027 +D05 7008 5032 +D06 7006 5031 +D07 7023 5026 +D08 7025 5025 +D09 7024 5028 +D10 7018 5027 +D11 7026 5032 +D12 7017 5031 +E01 7017 5027 +E02 7025 5028 +E03 7018 5031 +E04 7023 5032 +E05 7026 5017 +E06 7024 5018 +E07 7006 5027 +E08 7007 5028 +E09 7005 5031 +E10 7015 5032 +E11 7008 5017 +E12 7016 5018 +F01 7018 5028 +F02 7026 5027 +F03 7017 5032 +F04 7024 5031 +F05 7025 5018 +F06 7023 5017 +F07 7005 5028 +F08 7008 5027 +F09 7006 5032 +F10 7016 5031 +F11 7007 5018 +F12 7015 5017 +G01 7026 5031 +G02 7024 5032 +G03 7025 5017 +G04 7018 5018 +G05 7023 5025 +G06 7017 5026 +G07 7008 5031 +G08 7016 5032 +G09 7007 5017 +G10 7005 5018 +G11 7015 5025 +G12 7006 5026 +H01 7025 5032 +H02 7023 5031 +H03 7026 5018 +H04 7017 5017 +H05 7024 5026 +H06 7018 5025 +H07 7007 5032 +H08 7015 5031 +H09 7008 5018 +H10 7006 5017 +H11 7016 5026 +H12 7005 5025 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/AmpliSeq CD Indexes Plate D.txt b/conf/collection_index_kits/AmpliSeq CD Indexes Plate D.txt new file mode 100644 index 000000000..9b7414e67 --- /dev/null +++ b/conf/collection_index_kits/AmpliSeq CD Indexes Plate D.txt @@ -0,0 +1,225 @@ +[Version] +1 +[Name] +AmpliSeq CD Indexes Plate D +[PlateExtension] +amp384d +[Settings] +IndexOnly +Adapter CTGTCTCTTATACACATCT +[I7] +7027 AGCCTCAT +7028 GATTCTGC +7029 TCGTAGTG +7030 CTACGACA +7035 ATGGCATG +7036 GCAATGCA +7039 CTTATCGG +7040 TCCGCTAA +7041 GATCTATC +7042 AGCTCGCT +7047 ACACTAAG +7048 GTGTCGGA +[I5] +5017 CTAGCTTG +5018 TCGATCCA +5025 ATACCAAG +5026 GCGTTGGA +5027 CTTCACGG +5028 TCCTGTAA +5031 CGCTCGTG +5032 TATCTACA +[IndexPlateLayout] +A01 7027 5017 +A02 7035 5018 +A03 7028 5025 +A04 7029 5026 +A05 7036 5027 +A06 7030 5028 +A07 7040 5017 +A08 7041 5018 +A09 7039 5025 +A10 7047 5026 +A11 7042 5027 +A12 7048 5028 +B01 7028 5018 +B02 7036 5017 +B03 7027 5026 +B04 7030 5025 +B05 7035 5028 +B06 7029 5027 +B07 7039 5018 +B08 7042 5017 +B09 7040 5026 +B10 7048 5025 +B11 7041 5028 +B12 7047 5027 +C01 7036 5025 +C02 7030 5026 +C03 7035 5027 +C04 7028 5028 +C05 7029 5031 +C06 7027 5032 +C07 7042 5025 +C08 7048 5026 +C09 7041 5027 +C10 7039 5028 +C11 7047 5031 +C12 7040 5032 +D01 7035 5026 +D02 7029 5025 +D03 7036 5028 +D04 7027 5027 +D05 7030 5032 +D06 7028 5031 +D07 7041 5026 +D08 7047 5025 +D09 7042 5028 +D10 7040 5027 +D11 7048 5032 +D12 7039 5031 +E01 7039 5027 +E02 7047 5028 +E03 7040 5031 +E04 7041 5032 +E05 7048 5017 +E06 7042 5018 +E07 7028 5027 +E08 7029 5028 +E09 7027 5031 +E10 7035 5032 +E11 7030 5017 +E12 7036 5018 +F01 7040 5028 +F02 7048 5027 +F03 7039 5032 +F04 7042 5031 +F05 7047 5018 +F06 7041 5017 +F07 7027 5028 +F08 7030 5027 +F09 7028 5032 +F10 7036 5031 +F11 7029 5018 +F12 7035 5017 +G01 7048 5031 +G02 7042 5032 +G03 7047 5017 +G04 7040 5018 +G05 7041 5025 +G06 7039 5026 +G07 7030 5031 +G08 7036 5032 +G09 7029 5017 +G10 7027 5018 +G11 7035 5025 +G12 7028 5026 +H01 7047 5032 +H02 7041 5031 +H03 7048 5018 +H04 7039 5017 +H05 7042 5026 +H06 7040 5025 +H07 7029 5032 +H08 7035 5031 +H09 7030 5018 +H10 7028 5017 +H11 7036 5026 +H12 7027 5025 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set A.txt b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set A.txt new file mode 100644 index 000000000..8bcdf33f2 --- /dev/null +++ b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set A.txt @@ -0,0 +1,494 @@ +[Version] +1 +[Name] +IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set A +[PlateExtension] +nexud384a +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +UDP0001 GAACTGAGCG +UDP0002 AGGTCAGATA +UDP0003 CGTCTCATAT +UDP0004 ATTCCATAAG +UDP0005 GACGAGATTA +UDP0006 AACATCGCGC +UDP0007 CTAGTGCTCT +UDP0008 GATCAAGGCA +UDP0009 GACTGAGTAG +UDP0010 AGTCAGACGA +UDP0011 CCGTATGTTC +UDP0012 GAGTCATAGG +UDP0013 CTTGCCATTA +UDP0014 GAAGCGGCAC +UDP0015 TCCATTGCCG +UDP0016 CGGTTACGGC +UDP0017 GAGAATGGTT +UDP0018 AGAGGCAACC +UDP0019 CCATCATTAG +UDP0020 GATAGGCCGA +UDP0021 ATGGTTGACT +UDP0022 TATTGCGCTC +UDP0023 ACGCCTTGTT +UDP0024 TTCTACATAC +UDP0025 AACCATAGAA +UDP0026 GGTTGCGAGG +UDP0027 TAAGCATCCA +UDP0028 ACCACGACAT +UDP0029 GCCGCACTCT +UDP0030 CCACCAGGCA +UDP0031 GTGACACGCA +UDP0032 ACAGTGTATG +UDP0033 TGATTATACG +UDP0034 CAGCCGCGTA +UDP0035 GGTAACTCGC +UDP0036 ACCGGCCGTA +UDP0037 TGTAATCGAC +UDP0038 GTGCAGACAG +UDP0039 CAATCGGCTG +UDP0040 TATGTAGTCA +UDP0041 ACTCGGCAAT +UDP0042 GTCTAATGGC +UDP0043 CCATCTCGCC +UDP0044 CTGCGAGCCA +UDP0045 CGTTATTCTA +UDP0046 AGATCCATTA +UDP0047 GTCCTGGATA +UDP0048 CAGTGGCACT +UDP0049 AGTGTTGCAC +UDP0050 GACACCATGT +UDP0051 CCTGTCTGTC +UDP0052 TGATGTAAGA +UDP0053 GGAATTGTAA +UDP0054 GCATAAGCTT +UDP0055 CTGAGGAATA +UDP0056 AACGCACGAG +UDP0057 TCTATCCTAA +UDP0058 CTCGCTTCGG +UDP0059 CTGTTGGTCC +UDP0060 TTACCTGGAA +UDP0061 TGGCTAATCA +UDP0062 AACACTGTTA +UDP0063 ATTGCGCGGT +UDP0064 TGGCGCGAAC +UDP0065 TAATGTGTCT +UDP0066 ATACCAACGC +UDP0067 AGGATGTGCT +UDP0068 CACGGAACAA +UDP0069 TGGAGTACTT +UDP0070 GTATTGACGT +UDP0071 CTTGTACACC +UDP0072 ACACAGGTGG +UDP0073 CCTGCGGAAC +UDP0074 TTCATAAGGT +UDP0075 CTCTGCAGCG +UDP0076 CTGACTCTAC +UDP0077 TCTGGTATCC +UDP0078 CATTAGTGCG +UDP0079 ACGGTCAGGA +UDP0080 GGCAAGCCAG +UDP0081 TGTCGCTGGT +UDP0082 ACCGTTACAA +UDP0083 TATGCCTTAC +UDP0084 ACAAGTGGAC +UDP0085 TGGTACCTAA +UDP0086 TTGGAATTCC +UDP0087 CCTCTACATG +UDP0088 GGAGCGTGTA +UDP0089 GTCCGTAAGC +UDP0090 ACTTCAAGCG +UDP0091 TCAGAAGGCG +UDP0092 GCGTTGGTAT +UDP0093 ACATATCCAG +UDP0094 TCATAGATTG +UDP0095 GTATTCCACC +UDP0096 CCTCCGTCCA +[I5] +UDP0001 TCGTGGAGCG +UDP0002 CTACAAGATA +UDP0003 TATAGTAGCT +UDP0004 TGCCTGGTGG +UDP0005 ACATTATCCT +UDP0006 GTCCACTTGT +UDP0007 TGGAACAGTA +UDP0008 CCTTGTTAAT +UDP0009 GTTGATAGTG +UDP0010 ACCAGCGACA +UDP0011 CATACACTGT +UDP0012 GTGTGGCGCT +UDP0013 ATCACGAAGG +UDP0014 CGGCTCTACT +UDP0015 GAATGCACGA +UDP0016 AAGACTATAG +UDP0017 TCGGCAGCAA +UDP0018 CTAATGATGG +UDP0019 GGTTGCCTCT +UDP0020 CGCACATGGC +UDP0021 GGCCTGTCCT +UDP0022 CTGTGTTAGG +UDP0023 TAAGGAACGT +UDP0024 CTAACTGTAA +UDP0025 GGCGAGATGG +UDP0026 AATAGAGCAA +UDP0027 TCAATCCATT +UDP0028 TCGTATGCGG +UDP0029 TCCGACCTCG +UDP0030 CTTATGGAAT +UDP0031 GCTTACGGAC +UDP0032 GAACATACGG +UDP0033 GTCGATTACA +UDP0034 ACTAGCCGTG +UDP0035 AAGTTGGTGA +UDP0036 TGGCAATATT +UDP0037 GATCACCGCG +UDP0038 TACCATCCGT +UDP0039 GCTGTAGGAA +UDP0040 CGCACTAATG +UDP0041 GACAACTGAA +UDP0042 AGTGGTCAGG +UDP0043 TTCTATGGTT +UDP0044 AATCCGGCCA +UDP0045 CCATAAGGTT +UDP0046 ATCTCTACCA +UDP0047 CGGTGGCGAA +UDP0048 TAACAATAGG +UDP0049 CTGGTACACG +UDP0050 TCAACGTGTA +UDP0051 ACTGTTGTGA +UDP0052 GTGCGTCCTT +UDP0053 AGCACATCCT +UDP0054 TTCCGTCGCA +UDP0055 CTTAACCACT +UDP0056 GCCTCGGATA +UDP0057 CGTCGACTGG +UDP0058 TACTAGTCAA +UDP0059 ATAGACCGTT +UDP0060 ACAGTTCCAG +UDP0061 AGGCATGTAG +UDP0062 GCAAGTCTCA +UDP0063 TTGGCTCCGC +UDP0064 AACTGATACT +UDP0065 GTAAGGCATA +UDP0066 AATTGCTGCG +UDP0067 TTACAATTCC +UDP0068 AACCTAGCAC +UDP0069 TCTGTGTGGA +UDP0070 GGAATTCCAA +UDP0071 AAGCGCGCTT +UDP0072 TGAGCGTTGT +UDP0073 ATCATAGGCT +UDP0074 TGTTAGAAGG +UDP0075 GATGGATGTA +UDP0076 ACGGCCGTCA +UDP0077 CGTTGCTTAC +UDP0078 TGACTACATA +UDP0079 CGGCCTCGTT +UDP0080 CAAGCATCCG +UDP0081 TCGTCTGACT +UDP0082 CTCATAGCGA +UDP0083 AGACACATTA +UDP0084 GCGCGATGTT +UDP0085 CATGAGTACT +UDP0086 ACGTCAATAC +UDP0087 GATACCTCCT +UDP0088 ATCCGTAAGT +UDP0089 CGTGTATCTT +UDP0090 GAACCATGAA +UDP0091 GGCCATCATA +UDP0092 ACATACTTCC +UDP0093 TATGTGCAAT +UDP0094 GATTAAGGTG +UDP0095 ATGTAGACAA +UDP0096 CACATCGGTG + +[IndexPlateLayout] +A01 UDP0001 UDP0001 +B01 UDP0002 UDP0002 +C01 UDP0003 UDP0003 +D01 UDP0004 UDP0004 +E01 UDP0005 UDP0005 +F01 UDP0006 UDP0006 +G01 UDP0007 UDP0007 +H01 UDP0008 UDP0008 +A02 UDP0009 UDP0009 +B02 UDP0010 UDP0010 +C02 UDP0011 UDP0011 +D02 UDP0012 UDP0012 +E02 UDP0013 UDP0013 +F02 UDP0014 UDP0014 +G02 UDP0015 UDP0015 +H02 UDP0016 UDP0016 +A03 UDP0017 UDP0017 +B03 UDP0018 UDP0018 +C03 UDP0019 UDP0019 +D03 UDP0020 UDP0020 +E03 UDP0021 UDP0021 +F03 UDP0022 UDP0022 +G03 UDP0023 UDP0023 +H03 UDP0024 UDP0024 +A04 UDP0025 UDP0025 +B04 UDP0026 UDP0026 +C04 UDP0027 UDP0027 +D04 UDP0028 UDP0028 +E04 UDP0029 UDP0029 +F04 UDP0030 UDP0030 +G04 UDP0031 UDP0031 +H04 UDP0032 UDP0032 +A05 UDP0033 UDP0033 +B05 UDP0034 UDP0034 +C05 UDP0035 UDP0035 +D05 UDP0036 UDP0036 +E05 UDP0037 UDP0037 +F05 UDP0038 UDP0038 +G05 UDP0039 UDP0039 +H05 UDP0040 UDP0040 +A06 UDP0041 UDP0041 +B06 UDP0042 UDP0042 +C06 UDP0043 UDP0043 +D06 UDP0044 UDP0044 +E06 UDP0045 UDP0045 +F06 UDP0046 UDP0046 +G06 UDP0047 UDP0047 +H06 UDP0048 UDP0048 +A07 UDP0049 UDP0049 +B07 UDP0050 UDP0050 +C07 UDP0051 UDP0051 +D07 UDP0052 UDP0052 +E07 UDP0053 UDP0053 +F07 UDP0054 UDP0054 +G07 UDP0055 UDP0055 +H07 UDP0056 UDP0056 +A08 UDP0057 UDP0057 +B08 UDP0058 UDP0058 +C08 UDP0059 UDP0059 +D08 UDP0060 UDP0060 +E08 UDP0061 UDP0061 +F08 UDP0062 UDP0062 +G08 UDP0063 UDP0063 +H08 UDP0064 UDP0064 +A09 UDP0065 UDP0065 +B09 UDP0066 UDP0066 +C09 UDP0067 UDP0067 +D09 UDP0068 UDP0068 +E09 UDP0069 UDP0069 +F09 UDP0070 UDP0070 +G09 UDP0071 UDP0071 +H09 UDP0072 UDP0072 +A10 UDP0073 UDP0073 +B10 UDP0074 UDP0074 +C10 UDP0075 UDP0075 +D10 UDP0076 UDP0076 +E10 UDP0077 UDP0077 +F10 UDP0078 UDP0078 +G10 UDP0079 UDP0079 +H10 UDP0080 UDP0080 +A11 UDP0081 UDP0081 +B11 UDP0082 UDP0082 +C11 UDP0083 UDP0083 +D11 UDP0084 UDP0084 +E11 UDP0085 UDP0085 +F11 UDP0086 UDP0086 +G11 UDP0087 UDP0087 +H11 UDP0088 UDP0088 +A12 UDP0089 UDP0089 +B12 UDP0090 UDP0090 +C12 UDP0091 UDP0091 +D12 UDP0092 UDP0092 +E12 UDP0093 UDP0093 +F12 UDP0094 UDP0094 +G12 UDP0095 UDP0095 +H12 UDP0096 UDP0096 +[DefaultLayout_SingleIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set B.txt b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set B.txt new file mode 100644 index 000000000..ce056735a --- /dev/null +++ b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set B.txt @@ -0,0 +1,494 @@ +[Version] +1 +[Name] +IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set B +[PlateExtension] +nexud384b +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +UDP0097 TGCCGGTCAG +UDP0098 CACTCAATTC +UDP0099 TCTCACACGC +UDP0100 TCAATGGAGA +UDP0101 ATATGCATGT +UDP0102 ATGGCGCCTG +UDP0103 TCCGTTATGT +UDP0104 GGTCTATTAA +UDP0105 CAGCAATCGT +UDP0106 TTCTGTAGAA +UDP0107 GAACGCAATA +UDP0108 AGTACTCATG +UDP0109 GGTAGAATTA +UDP0110 TAATTAGCGT +UDP0111 ATTAACAAGG +UDP0112 TGATGGCTAC +UDP0113 GAATTACAAG +UDP0114 TAGAATTGGA +UDP0115 AGGCAGCTCT +UDP0116 ATCGGCGAAG +UDP0117 CCGTGACCGA +UDP0118 ATACTTGTTC +UDP0119 TCCGCCAATT +UDP0120 AGGACAGGCC +UDP0121 AGAGAACCTA +UDP0122 GATATTGTGT +UDP0123 CGTACAGGAA +UDP0124 CTGCGTTACC +UDP0125 AGGCCGTGGA +UDP0126 AGGAGGTATC +UDP0127 GCTGACGTTG +UDP0128 CTAATAACCG +UDP0129 TCTAGGCGCG +UDP0130 ATAGCCAAGA +UDP0131 TTCGGTGTGA +UDP0132 ATGTAACGTT +UDP0133 AACGAGGCCG +UDP0134 TGGTGTTATG +UDP0135 TGGCCTCTGT +UDP0136 CCAGGCACCA +UDP0137 CCGGTTCCTA +UDP0138 GGCCAATATT +UDP0139 GAATACCTAT +UDP0140 TACGTGAAGG +UDP0141 CTTATTGGCC +UDP0142 ACAACTACTG +UDP0143 GTTGGATGAA +UDP0144 AATCCAATTG +UDP0145 TATGATGGCC +UDP0146 CGCAGCAATT +UDP0147 ACGTTCCTTA +UDP0148 CCGCGTATAG +UDP0149 GATTCTGAAT +UDP0150 TAGAGAATAC +UDP0151 TTGTATCAGG +UDP0152 CACAGCGGTC +UDP0153 CCACGCTGAA +UDP0154 GTTCGGAGTT +UDP0155 ATAGCGGAAT +UDP0156 GCAATATTCA +UDP0157 CTAGATTGCG +UDP0158 CGATGCGGTT +UDP0159 TCCGGACTAG +UDP0160 GTGACGGAGC +UDP0161 AATTCCATCT +UDP0162 TTAACGGTGT +UDP0163 ACTTGTTATC +UDP0164 CGTGTACCAG +UDP0165 TTAACCTTCG +UDP0166 CATATGCGAT +UDP0167 AGCCTATGAT +UDP0168 TATGACAATC +UDP0169 ATGTTGTTGG +UDP0170 GCACCACCAA +UDP0171 AGGCGTTCGC +UDP0172 CCTCCGGTTG +UDP0173 GTCCACCGCT +UDP0174 ATTGTTCGTC +UDP0175 GGACCAGTGG +UDP0176 CCTTCTAACA +UDP0177 CTCGAATATA +UDP0178 GATCGTCGCG +UDP0179 TATCCGAGGC +UDP0180 CGCTGTCTCA +UDP0181 AATGCGAACA +UDP0182 AATTCTTGGA +UDP0183 TTCCTACAGC +UDP0184 ATCCAGGTAT +UDP0185 ACGGTCCAAC +UDP0186 GTAACTTGGT +UDP0187 AGCGCCACAC +UDP0188 TGCTACTGCC +UDP0189 CAACACCGCA +UDP0190 CACCTTAATC +UDP0191 TTGAATGTTG +UDP0192 CCGGTAACAC +[I5] +UDP0097 CCTGATACAA +UDP0098 TTAAGTTGTG +UDP0099 CGGACAGTGA +UDP0100 GCACTACAAC +UDP0101 TGGTGCCTGG +UDP0102 TCCACGGCCT +UDP0103 TTGTAGTGTA +UDP0104 CCACGACACG +UDP0105 TGTGATGTAT +UDP0106 GAGCGCAATA +UDP0107 ATCTTACTGT +UDP0108 ATGTCGTGGT +UDP0109 GTAGCCATCA +UDP0110 TGGTTAAGAA +UDP0111 TGTTGTTCGT +UDP0112 CCAACAACAT +UDP0113 ACCGGCTCAG +UDP0114 GTTAATCTGA +UDP0115 CGGCTAACGT +UDP0116 TCCAAGAATT +UDP0117 CCGAACGTTG +UDP0118 TAACCGCCGA +UDP0119 CTCCGTGCTG +UDP0120 CATTCCAGCT +UDP0121 GGTTATGCTA +UDP0122 ACCACACGGT +UDP0123 TAGGTTCTCT +UDP0124 TATGGCTCGA +UDP0125 CTCGTGCGTT +UDP0126 CCAGTTGGCA +UDP0127 TGTTCGCATT +UDP0128 AACCGCATCG +UDP0129 CGAAGGTTAA +UDP0130 AGTGCCACTG +UDP0131 GAACAAGTAT +UDP0132 ACGATTGCTG +UDP0133 ATACCTGGAT +UDP0134 TCCAATTCTA +UDP0135 TGAGACAGCG +UDP0136 ACGCTAATTA +UDP0137 TATATTCGAG +UDP0138 CGGTCCGATA +UDP0139 ACAATAGAGT +UDP0140 CGGTTATTAG +UDP0141 GATAACAAGT +UDP0142 AGTTATCACA +UDP0143 TTCCAGGTAA +UDP0144 CATGTAGAGG +UDP0145 GATTGTCATA +UDP0146 ATTCCGCTAT +UDP0147 GACCGCTGTG +UDP0148 TAGGAACCGG +UDP0149 AGCGGTGGAC +UDP0150 TATAGATTCG +UDP0151 ACAGAGGCCA +UDP0152 ATTCCTATTG +UDP0153 TATTCCTCAG +UDP0154 CGCCTTCTGA +UDP0155 GCGCAGAGTA +UDP0156 GGCGCCAATT +UDP0157 AGATATGGCG +UDP0158 CCTGCTTGGT +UDP0159 GACGAACAAT +UDP0160 TGGCGGTCCA +UDP0161 CTTCAGTTAC +UDP0162 TCCTGACCGT +UDP0163 CGCGCCTAGA +UDP0164 AGGATAAGTT +UDP0165 AGGCCAGACA +UDP0166 CCTTGAACGG +UDP0167 CACCACCTAC +UDP0168 TTGCTTGTAT +UDP0169 CAATCTATGA +UDP0170 TGGTACTGAT +UDP0171 TTCATCCAAC +UDP0172 CATAACACCA +UDP0173 TCCTATTAGC +UDP0174 TCTCTAGATT +UDP0175 CGCGAGCCTA +UDP0176 GATAAGCTCT +UDP0177 GAGATGTCGA +UDP0178 CTGGATATGT +UDP0179 GGCCAATAAG +UDP0180 ATTACTCACC +UDP0181 AATTGGCGGA +UDP0182 TTGTCAACTT +UDP0183 GGCGAATTCT +UDP0184 CAACGTCAGC +UDP0185 TCTTACATCA +UDP0186 CGCCATACCT +UDP0187 CTAATGTCTT +UDP0188 CAACCGGAGG +UDP0189 GGCAGTAGCA +UDP0190 TTAGGATAGA +UDP0191 CGCAATCTAG +UDP0192 GAGTTGTACT + +[IndexPlateLayout] +A01 UDP0097 UDP0097 +B01 UDP0098 UDP0098 +C01 UDP0099 UDP0099 +D01 UDP0100 UDP0100 +E01 UDP0101 UDP0101 +F01 UDP0102 UDP0102 +G01 UDP0103 UDP0103 +H01 UDP0104 UDP0104 +A02 UDP0105 UDP0105 +B02 UDP0106 UDP0106 +C02 UDP0107 UDP0107 +D02 UDP0108 UDP0108 +E02 UDP0109 UDP0109 +F02 UDP0110 UDP0110 +G02 UDP0111 UDP0111 +H02 UDP0112 UDP0112 +A03 UDP0113 UDP0113 +B03 UDP0114 UDP0114 +C03 UDP0115 UDP0115 +D03 UDP0116 UDP0116 +E03 UDP0117 UDP0117 +F03 UDP0118 UDP0118 +G03 UDP0119 UDP0119 +H03 UDP0120 UDP0120 +A04 UDP0121 UDP0121 +B04 UDP0122 UDP0122 +C04 UDP0123 UDP0123 +D04 UDP0124 UDP0124 +E04 UDP0125 UDP0125 +F04 UDP0126 UDP0126 +G04 UDP0127 UDP0127 +H04 UDP0128 UDP0128 +A05 UDP0129 UDP0129 +B05 UDP0130 UDP0130 +C05 UDP0131 UDP0131 +D05 UDP0132 UDP0132 +E05 UDP0133 UDP0133 +F05 UDP0134 UDP0134 +G05 UDP0135 UDP0135 +H05 UDP0136 UDP0136 +A06 UDP0137 UDP0137 +B06 UDP0138 UDP0138 +C06 UDP0139 UDP0139 +D06 UDP0140 UDP0140 +E06 UDP0141 UDP0141 +F06 UDP0142 UDP0142 +G06 UDP0143 UDP0143 +H06 UDP0144 UDP0144 +A07 UDP0145 UDP0145 +B07 UDP0146 UDP0146 +C07 UDP0147 UDP0147 +D07 UDP0148 UDP0148 +E07 UDP0149 UDP0149 +F07 UDP0150 UDP0150 +G07 UDP0151 UDP0151 +H07 UDP0152 UDP0152 +A08 UDP0153 UDP0153 +B08 UDP0154 UDP0154 +C08 UDP0155 UDP0155 +D08 UDP0156 UDP0156 +E08 UDP0157 UDP0157 +F08 UDP0158 UDP0158 +G08 UDP0159 UDP0159 +H08 UDP0160 UDP0160 +A09 UDP0161 UDP0161 +B09 UDP0162 UDP0162 +C09 UDP0163 UDP0163 +D09 UDP0164 UDP0164 +E09 UDP0165 UDP0165 +F09 UDP0166 UDP0166 +G09 UDP0167 UDP0167 +H09 UDP0168 UDP0168 +A10 UDP0169 UDP0169 +B10 UDP0170 UDP0170 +C10 UDP0171 UDP0171 +D10 UDP0172 UDP0172 +E10 UDP0173 UDP0173 +F10 UDP0174 UDP0174 +G10 UDP0175 UDP0175 +H10 UDP0176 UDP0176 +A11 UDP0177 UDP0177 +B11 UDP0178 UDP0178 +C11 UDP0179 UDP0179 +D11 UDP0180 UDP0180 +E11 UDP0181 UDP0181 +F11 UDP0182 UDP0182 +G11 UDP0183 UDP0183 +H11 UDP0184 UDP0184 +A12 UDP0185 UDP0185 +B12 UDP0186 UDP0186 +C12 UDP0187 UDP0187 +D12 UDP0188 UDP0188 +E12 UDP0189 UDP0189 +F12 UDP0190 UDP0190 +G12 UDP0191 UDP0191 +H12 UDP0192 UDP0192 +[DefaultLayout_SingleIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set C.txt b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set C.txt new file mode 100644 index 000000000..9c84b6eff --- /dev/null +++ b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set C.txt @@ -0,0 +1,494 @@ +[Version] +1 +[Name] +IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set C +[PlateExtension] +nexud384c +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +UDP0193 TCTCATGATA +UDP0194 CGAGGCCAAG +UDP0195 TTCACGAGAC +UDP0196 GCGTGGATGG +UDP0197 TCCTGGTTGT +UDP0198 TAATTCTGCT +UDP0199 CGCACGACTG +UDP0200 GAGGTTAGAC +UDP0201 AACCGAGTTC +UDP0202 TGTGATAACT +UDP0203 AGTATGCTAC +UDP0204 GTAACTGAAG +UDP0205 TCCTCGGACT +UDP0206 CTGGAACTGT +UDP0207 GAATATGCGG +UDP0208 GATCGGATAA +UDP0209 GCTAGACTAT +UDP0210 AGCTACTATA +UDP0211 CCACCGGAGT +UDP0212 CTTACCGCAC +UDP0213 TTAGGATATC +UDP0214 TTATACGCGA +UDP0215 CGCTTAGAAT +UDP0216 CCGAAGCGCT +UDP0217 CACTATCAAC +UDP0218 TTGCTCTATT +UDP0219 TTACAGTTAG +UDP0220 CTAAGTACGC +UDP0221 TAGTTCGGTA +UDP0222 CTATTACTAC +UDP0223 TAGCATAACC +UDP0224 ACTCTATTGT +UDP0225 TAGTGGAAGC +UDP0226 CGCCATATCT +UDP0227 GCTTCATATT +UDP0228 ACTAGCGCTA +UDP0229 GCTCTTAACT +UDP0230 GTGGTATCTG +UDP0231 TGACGGCCGT +UDP0232 CAGTAATTAC +UDP0233 TACAAGACTT +UDP0234 CTGTGGTGAC +UDP0235 CTCCACTAAT +UDP0236 ATAGTTAGCA +UDP0237 ATAGGTCTTA +UDP0238 TTCTTAACCA +UDP0239 AAGGAAGAGT +UDP0240 GGAAGGAGAC +UDP0241 TGAACGCGGA +UDP0242 CCTGCAACCT +UDP0243 TTCATGGTTC +UDP0244 ATCCTCTCAA +UDP0245 CACTAGACCA +UDP0246 ATTATCCACT +UDP0247 ATGGCGTGCC +UDP0248 TCCAGAGATC +UDP0249 ATGTCCAGCA +UDP0250 CAACGTTCGG +UDP0251 GCGTATTAAT +UDP0252 GTTGTGACTA +UDP0253 TCTCAATACC +UDP0254 AAGCATCTTG +UDP0255 TCAGTCTCGT +UDP0256 TGCAAGATAA +UDP0257 GTAACAATCT +UDP0258 CAGCGGTAGA +UDP0259 TCATACCGTT +UDP0260 GGCGCCATTG +UDP0261 AGCGAATTAG +UDP0262 TTAGACCATG +UDP0263 CACACAGTAT +UDP0264 TCTTGTCGGC +UDP0265 TACCGCCTCG +UDP0266 CTGTTATATC +UDP0267 TAACCGGCGA +UDP0268 AAGAGAGTCT +UDP0269 GTAGGCGAGC +UDP0270 AACTTATCCT +UDP0271 ATTATGTCTC +UDP0272 TATAACAGCT +UDP0273 CCAATGATAC +UDP0274 GAGGCCTATT +UDP0275 AGCTAAGCGG +UDP0276 CTTCCTAGGA +UDP0277 CGATCTGTGA +UDP0278 GTGGACAAGT +UDP0279 AACAAGTACA +UDP0280 AGATTAAGTG +UDP0281 TATCACTCTG +UDP0282 AGAATTCGCC +UDP0283 CCTGACCACT +UDP0284 AGCTGGAATG +UDP0285 TGATAACGAG +UDP0286 CATAGTAAGG +UDP0287 ATTGGCTTCT +UDP0288 GTACCGATTA +[I5] +UDP0193 AACACGTGGA +UDP0194 GTGTTACCGG +UDP0195 AGATTGTTAC +UDP0196 TTGACCAATG +UDP0197 CTGACCGGCA +UDP0198 TCTCATCAAT +UDP0199 GGACCAACAG +UDP0200 AATGTATTGC +UDP0201 GATCTCTGGA +UDP0202 CAGGCGCCAT +UDP0203 TTAATAGACC +UDP0204 GGAGTCGCGA +UDP0205 AACGCCAGAG +UDP0206 CGTAATTAAC +UDP0207 ACGAGACTGA +UDP0208 GTATCGGCCG +UDP0209 AATACGACAT +UDP0210 GTTATATGGC +UDP0211 GCCTGCCATG +UDP0212 TAAGACCTAT +UDP0213 TATACCATGG +UDP0214 GCCGTCTGTT +UDP0215 CAGAGTGATA +UDP0216 TGCTAACTAT +UDP0217 TCAGTTAATG +UDP0218 GTGACCTTGA +UDP0219 ACATGCATAT +UDP0220 AACATACCTA +UDP0221 CCATGTGTAG +UDP0222 GAGTCTCTCC +UDP0223 GCTATGCGCA +UDP0224 ATCGCATATG +UDP0225 AGTACCTATA +UDP0226 GACCGGAGAT +UDP0227 CGTTCAGCCT +UDP0228 TTACTTCCTC +UDP0229 CACGTCCACC +UDP0230 GCTACTATCT +UDP0231 AGTCAACCAT +UDP0232 CGAGGCGGTA +UDP0233 CAGGTGTTCA +UDP0234 GACAGACAGG +UDP0235 TGTACTTGTT +UDP0236 CTCTAAGTAG +UDP0237 GTCACCACAG +UDP0238 TCTACATACC +UDP0239 CACGTTAGGC +UDP0240 TGGTGAGTCT +UDP0241 CTTCGAAGGA +UDP0242 GTAGAGTCAG +UDP0243 GACATTGTCA +UDP0244 TCCGCAAGGC +UDP0245 ACTGCCTTAT +UDP0246 TACGCACGTA +UDP0247 CGCTTGAAGT +UDP0248 CTGCACTTCA +UDP0249 CAGCGGACAA +UDP0250 GGATCCGCAT +UDP0251 TGCGGTGTTG +UDP0252 ACATAACGGA +UDP0253 GACGTTCGCG +UDP0254 CATTCAACAA +UDP0255 CACGGATTAT +UDP0256 TTGAGGACGG +UDP0257 CTCTGTATAC +UDP0258 GCAACAGGTG +UDP0259 GGTAACGCAG +UDP0260 ACCGCGCAAT +UDP0261 AGCCGGAACA +UDP0262 TCCTAGGAAG +UDP0263 TTGAGCCTAA +UDP0264 CCACCTGTGT +UDP0265 CCTCGCAACC +UDP0266 GTATAGCTGT +UDP0267 GCTACATTAG +UDP0268 TACGAATCTT +UDP0269 TAGGAGCGCA +UDP0270 GTACTGGCGT +UDP0271 AGTTAAGAGC +UDP0272 TCGCGTATAA +UDP0273 GAGTGTGCCG +UDP0274 CTAGTCCGGA +UDP0275 ATTAATACGC +UDP0276 CCTAGAGTAT +UDP0277 TAGGAAGACT +UDP0278 CCGTGGCCTT +UDP0279 GGATATATCC +UDP0280 CACCTCTTGG +UDP0281 AACGTTACAT +UDP0282 CGGCAAGCTC +UDP0283 TCTTGGCTAT +UDP0284 ACGGAATGCG +UDP0285 GTTCCGCAGG +UDP0286 ACCAAGTTAC +UDP0287 TGGCTCGCAG +UDP0288 AACTAACGTT + +[IndexPlateLayout] +A01 UDP0193 UDP0193 +B01 UDP0194 UDP0194 +C01 UDP0195 UDP0195 +D01 UDP0196 UDP0196 +E01 UDP0197 UDP0197 +F01 UDP0198 UDP0198 +G01 UDP0199 UDP0199 +H01 UDP0200 UDP0200 +A02 UDP0201 UDP0201 +B02 UDP0202 UDP0202 +C02 UDP0203 UDP0203 +D02 UDP0204 UDP0204 +E02 UDP0205 UDP0205 +F02 UDP0206 UDP0206 +G02 UDP0207 UDP0207 +H02 UDP0208 UDP0208 +A03 UDP0209 UDP0209 +B03 UDP0210 UDP0210 +C03 UDP0211 UDP0211 +D03 UDP0212 UDP0212 +E03 UDP0213 UDP0213 +F03 UDP0214 UDP0214 +G03 UDP0215 UDP0215 +H03 UDP0216 UDP0216 +A04 UDP0217 UDP0217 +B04 UDP0218 UDP0218 +C04 UDP0219 UDP0219 +D04 UDP0220 UDP0220 +E04 UDP0221 UDP0221 +F04 UDP0222 UDP0222 +G04 UDP0223 UDP0223 +H04 UDP0224 UDP0224 +A05 UDP0225 UDP0225 +B05 UDP0226 UDP0226 +C05 UDP0227 UDP0227 +D05 UDP0228 UDP0228 +E05 UDP0229 UDP0229 +F05 UDP0230 UDP0230 +G05 UDP0231 UDP0231 +H05 UDP0232 UDP0232 +A06 UDP0233 UDP0233 +B06 UDP0234 UDP0234 +C06 UDP0235 UDP0235 +D06 UDP0236 UDP0236 +E06 UDP0237 UDP0237 +F06 UDP0238 UDP0238 +G06 UDP0239 UDP0239 +H06 UDP0240 UDP0240 +A07 UDP0241 UDP0241 +B07 UDP0242 UDP0242 +C07 UDP0243 UDP0243 +D07 UDP0244 UDP0244 +E07 UDP0245 UDP0245 +F07 UDP0246 UDP0246 +G07 UDP0247 UDP0247 +H07 UDP0248 UDP0248 +A08 UDP0249 UDP0249 +B08 UDP0250 UDP0250 +C08 UDP0251 UDP0251 +D08 UDP0252 UDP0252 +E08 UDP0253 UDP0253 +F08 UDP0254 UDP0254 +G08 UDP0255 UDP0255 +H08 UDP0256 UDP0256 +A09 UDP0257 UDP0257 +B09 UDP0258 UDP0258 +C09 UDP0259 UDP0259 +D09 UDP0260 UDP0260 +E09 UDP0261 UDP0261 +F09 UDP0262 UDP0262 +G09 UDP0263 UDP0263 +H09 UDP0264 UDP0264 +A10 UDP0265 UDP0265 +B10 UDP0266 UDP0266 +C10 UDP0267 UDP0267 +D10 UDP0268 UDP0268 +E10 UDP0269 UDP0269 +F10 UDP0270 UDP0270 +G10 UDP0271 UDP0271 +H10 UDP0272 UDP0272 +A11 UDP0273 UDP0273 +B11 UDP0274 UDP0274 +C11 UDP0275 UDP0275 +D11 UDP0276 UDP0276 +E11 UDP0277 UDP0277 +F11 UDP0278 UDP0278 +G11 UDP0279 UDP0279 +H11 UDP0280 UDP0280 +A12 UDP0281 UDP0281 +B12 UDP0282 UDP0282 +C12 UDP0283 UDP0283 +D12 UDP0284 UDP0284 +E12 UDP0285 UDP0285 +F12 UDP0286 UDP0286 +G12 UDP0287 UDP0287 +H12 UDP0288 UDP0288 +[DefaultLayout_SingleIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set D.txt b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set D.txt new file mode 100644 index 000000000..ae4f63a7c --- /dev/null +++ b/conf/collection_index_kits/IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set D.txt @@ -0,0 +1,497 @@ +[Version] +1 +[Name] +IDT-ILMN Nextera DNA UD Indexes (96 Indexes) Set D +[PlateExtension] +nexud384d +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +UDP0289 GAACAATTCC +UDP0290 TGTGGTCCGG +UDP0291 CTTCTAAGTC +UDP0292 AATATTGCCA +UDP0293 TCGTGCATTC +UDP0294 AAGATACACG +UDP0295 TGCAATGAAT +UDP0296 CTATGAAGGA +UDP0297 GAAGACTAGA +UDP0298 AGGAGTCGAG +UDP0299 TTCACTCACT +UDP0300 GGTCCGCTTC +UDP0301 CAACGAGAGC +UDP0302 ATTGAGGTCC +UDP0303 GGAGAGACTC +UDP0304 CCGCTCCGTT +UDP0305 ATACATCACA +UDP0306 TAGGTATGTT +UDP0307 CACCTAGCAC +UDP0308 TTCAAGTATG +UDP0309 TTAAGACAAG +UDP0310 CACCTCTCTT +UDP0311 TTCTCGTGCA +UDP0312 GCTAGGAAGT +UDP0313 TTAATAGCAC +UDP0314 CATTCACGCT +UDP0315 GGCACTAAGG +UDP0316 ATTCGGTACA +UDP0317 ACTAATCTCC +UDP0318 TGTGTTAGTA +UDP0319 CAACGACCTA +UDP0320 CGGTCGGCAT +UDP0321 TCGACGCTAG +UDP0322 CTCGTAGGCA +UDP0323 AAGTTCTAGT +UDP0324 CCAAGAGGTG +UDP0325 ATATCTGCTT +UDP0326 TGGATCTGGC +UDP0327 TTGAATCCAA +UDP0328 CACGGCTAGT +UDP0329 GAGCTTGCCG +UDP0330 AGCTAGCTTC +UDP0331 CAATCCTTGT +UDP0332 CACCTGTTGC +UDP0333 CGTCACCTTG +UDP0334 AATGACTGGT +UDP0335 ATGATTCCGG +UDP0336 TTAGGCTCAA +UDP0337 TGTAAGGTGG +UDP0338 CAACTGCAAC +UDP0339 ACATGAGTGA +UDP0340 GCAACCAGTC +UDP0341 GAGCGACGAT +UDP0342 CGAACGCACC +UDP0343 TCTTACGCCG +UDP0344 AGCTGATGTC +UDP0345 CTGAATTAGT +UDP0346 TAAGGAGGAA +UDP0347 AGCTTACACA +UDP0348 AACCAGCCAC +UDP0349 CTTAAGTCGA +UDP0350 GCCTAACGTG +UDP0351 ACTTACTTCA +UDP0352 CGCATTCCGT +UDP0353 GATATCACAC +UDP0354 AGCGCTGTGT +UDP0355 TCACCGCGCT +UDP0356 GATAGCCTTG +UDP0357 CCTGGACGCA +UDP0358 TTACGCACCT +UDP0359 TCGTTGCTGC +UDP0360 CGACAAGGAT +UDP0361 GTGTACCTTC +UDP0362 ACCTGGCCAA +UDP0363 TGTCTGGCCT +UDP0364 AGTTAATGCT +UDP0365 GGTGAGTAAT +UDP0366 TACTCTGCGC +UDP0367 AGGTATGGCG +UDP0368 TCCAGCCTGC +UDP0369 GCCATATAAC +UDP0370 AGTGCGAGTG +UDP0371 CTGAGCCGGT +UDP0372 AACGGTCTAT +UDP0373 GTTGCGTTCA +UDP0374 CTTCAACCAC +UDP0375 TCTATTCAGT +UDP0376 CAAGACGTCC +UDP0377 TGAGTACAAC +UDP0378 CCGCGGTTCT +UDP0379 ATTGATACTG +UDP0380 GGATTATGGA +UDP0381 TGGTTCTCAT +UDP0382 TCAACCACGA +UDP0383 TATGAACTTG +UDP0384 AGTGGTTAAG + +[I5] +UDP0289 TAGAGTTGGA +UDP0290 AGAGCACTAG +UDP0291 ACTCTACAGG +UDP0292 CGGTGACACC +UDP0293 GCGTTGGTAT +UDP0294 TGTGCTAACA +UDP0295 CCAGAAGTAA +UDP0296 CTTATACCTG +UDP0297 ACTAGAACTT +UDP0298 TTAGGCTTAC +UDP0299 TATCATGAGA +UDP0300 CTCACACAAG +UDP0301 GAATTGAGTG +UDP0302 CGGATTATAT +UDP0303 TTGAAGCAGA +UDP0304 TACGGCGAAG +UDP0305 TCTCCATTGA +UDP0306 CGAGACCAAG +UDP0307 TGCTGGACAT +UDP0308 GATGGTATCG +UDP0309 GGCTTAATTG +UDP0310 CTCGACTCCT +UDP0311 ATACACAGAG +UDP0312 TCTCGGACGA +UDP0313 ACCACGTCTG +UDP0314 GTTGTACTCA +UDP0315 TCAGGTCAAC +UDP0316 AGTCCGAGGA +UDP0317 CACTTAATCT +UDP0318 TACTCTGTTA +UDP0319 GCGACTCGAT +UDP0320 CTAGGCAAGG +UDP0321 CCTCTTCGAA +UDP0322 TCATCCTCTT +UDP0323 GGTAAGATAA +UDP0324 AACGAGCCAG +UDP0325 TAGACAATCT +UDP0326 CAATGCTGAA +UDP0327 GTCACGGTGT +UDP0328 GGTGTACAAG +UDP0329 AGGTTGCAGG +UDP0330 TAATACGGAG +UDP0331 CGAAGACGCA +UDP0332 ATTGACACAT +UDP0333 CAGCCGATTG +UDP0334 TCTCACGCGT +UDP0335 CTCTGACGTG +UDP0336 TCGAATGGAA +UDP0337 AAGGCCTTGG +UDP0338 TGAACGCAAC +UDP0339 CCGCTTAGCT +UDP0340 CACCGAGGAA +UDP0341 CGTATAATCA +UDP0342 ATGACAGAAC +UDP0343 ATTCATTGCA +UDP0344 TCATGTCCTG +UDP0345 AATTCGATCG +UDP0346 TTCCGACATT +UDP0347 TGGCACGACC +UDP0348 GCCACAGCAC +UDP0349 CAGTAGTTGT +UDP0350 AGCTCTCAAG +UDP0351 TCTGGAATTA +UDP0352 ATTAGTGGAG +UDP0353 GACTATATGT +UDP0354 CGTTCGGAAC +UDP0355 TCGATACTAG +UDP0356 TACCACAATG +UDP0357 TGGTATACCA +UDP0358 GCTCTCGTTG +UDP0359 GTCTCGTGAA +UDP0360 AAGGCCACCT +UDP0361 CTGTGAGCTA +UDP0362 TCACAGATCG +UDP0363 AGAAGCCAAT +UDP0364 ACTGCAGCCG +UDP0365 AACATCTAGT +UDP0366 CCTTACTATG +UDP0367 GTGGCGAGAC +UDP0368 GCCAGATCCA +UDP0369 ACACAATATC +UDP0370 TGGAGGTAAT +UDP0371 CCTTCACGTA +UDP0372 CTATACGCGG +UDP0373 GTTGCAGTTG +UDP0374 TTATGCGCCT +UDP0375 TCTCAGTACA +UDP0376 AGTATACGGA +UDP0377 ACGCTTGGAC +UDP0378 GGAGTAGATT +UDP0379 TACACGCTCC +UDP0380 TCCGATAGAG +UDP0381 CTCAAGGCCG +UDP0382 CAAGTTCATA +UDP0383 AATCCTTAGG +UDP0384 GGTGGAATAC + + +[IndexPlateLayout] +A01 UDP0289 UDP0289 +B01 UDP0290 UDP0290 +C01 UDP0291 UDP0291 +D01 UDP0292 UDP0292 +E01 UDP0293 UDP0293 +F01 UDP0294 UDP0294 +G01 UDP0295 UDP0295 +H01 UDP0296 UDP0296 +A02 UDP0297 UDP0297 +B02 UDP0298 UDP0298 +C02 UDP0299 UDP0299 +D02 UDP0300 UDP0300 +E02 UDP0301 UDP0301 +F02 UDP0302 UDP0302 +G02 UDP0303 UDP0303 +H02 UDP0304 UDP0304 +A03 UDP0305 UDP0305 +B03 UDP0306 UDP0306 +C03 UDP0307 UDP0307 +D03 UDP0308 UDP0308 +E03 UDP0309 UDP0309 +F03 UDP0310 UDP0310 +G03 UDP0311 UDP0311 +H03 UDP0312 UDP0312 +A04 UDP0313 UDP0313 +B04 UDP0314 UDP0314 +C04 UDP0315 UDP0315 +D04 UDP0316 UDP0316 +E04 UDP0317 UDP0317 +F04 UDP0318 UDP0318 +G04 UDP0319 UDP0319 +H04 UDP0320 UDP0320 +A05 UDP0321 UDP0321 +B05 UDP0322 UDP0322 +C05 UDP0323 UDP0323 +D05 UDP0324 UDP0324 +E05 UDP0325 UDP0325 +F05 UDP0326 UDP0326 +G05 UDP0327 UDP0327 +H05 UDP0328 UDP0328 +A06 UDP0329 UDP0329 +B06 UDP0330 UDP0330 +C06 UDP0331 UDP0331 +D06 UDP0332 UDP0332 +E06 UDP0333 UDP0333 +F06 UDP0334 UDP0334 +G06 UDP0335 UDP0335 +H06 UDP0336 UDP0336 +A07 UDP0337 UDP0337 +B07 UDP0338 UDP0338 +C07 UDP0339 UDP0339 +D07 UDP0340 UDP0340 +E07 UDP0341 UDP0341 +F07 UDP0342 UDP0342 +G07 UDP0343 UDP0343 +H07 UDP0344 UDP0344 +A08 UDP0345 UDP0345 +B08 UDP0346 UDP0346 +C08 UDP0347 UDP0347 +D08 UDP0348 UDP0348 +E08 UDP0349 UDP0349 +F08 UDP0350 UDP0350 +G08 UDP0351 UDP0351 +H08 UDP0352 UDP0352 +A09 UDP0353 UDP0353 +B09 UDP0354 UDP0354 +C09 UDP0355 UDP0355 +D09 UDP0356 UDP0356 +E09 UDP0357 UDP0357 +F09 UDP0358 UDP0358 +G09 UDP0359 UDP0359 +H09 UDP0360 UDP0360 +A10 UDP0361 UDP0361 +B10 UDP0362 UDP0362 +C10 UDP0363 UDP0363 +D10 UDP0364 UDP0364 +E10 UDP0365 UDP0365 +F10 UDP0366 UDP0366 +G10 UDP0367 UDP0367 +H10 UDP0368 UDP0368 +A11 UDP0369 UDP0369 +B11 UDP0370 UDP0370 +C11 UDP0371 UDP0371 +D11 UDP0372 UDP0372 +E11 UDP0373 UDP0373 +F11 UDP0374 UDP0374 +G11 UDP0375 UDP0375 +H11 UDP0376 UDP0376 +A12 UDP0377 UDP0377 +B12 UDP0378 UDP0378 +C12 UDP0379 UDP0379 +D12 UDP0380 UDP0380 +E12 UDP0381 UDP0381 +F12 UDP0382 UDP0382 +G12 UDP0383 UDP0383 +H12 UDP0384 UDP0384 + +[DefaultLayout_SingleIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/Nextera DNA CD Indexes (96 indexes plated).txt b/conf/collection_index_kits/Nextera DNA CD Indexes (96 indexes plated).txt new file mode 100644 index 000000000..6b990e85b --- /dev/null +++ b/conf/collection_index_kits/Nextera DNA CD Indexes (96 indexes plated).txt @@ -0,0 +1,321 @@ +[Version] +1 +[Name] +Nextera DNA CD Indexes (96 Indexes plated) +[PlateExtension] +nex96 +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +H701 TAAGGCGA +H702 CGTACTAG +H703 AGGCAGAA +H705 GGACTCCT +H706 TAGGCATG +H707 CTCTCTAC +H710 CGAGGCTG +H711 AAGAGGCA +H712 GTAGAGGA +H714 GCTCATGA +H720 CGGAGCCT +H723 TAGCGCTC +[I5] +H503 TATCCTCT +H505 GTAAGGAG +H506 ACTGCATA +H510 CGTCTAAT +H513 TCGACTAG +H516 CCTAGAGT +H517 GCGTAAGA +H522 TTATGCGA +[IndexPlateLayout] +A01 H701 H505 +A02 H702 H506 +A03 H703 H517 +A04 H705 H505 +A05 H707 H506 +A06 H723 H517 +A07 H706 H505 +A08 H712 H506 +A09 H720 H517 +A10 H710 H505 +A11 H711 H506 +A12 H714 H517 +B01 H702 H517 +B02 H703 H505 +B03 H701 H506 +B04 H707 H517 +B05 H723 H505 +B06 H705 H506 +B07 H712 H517 +B08 H720 H505 +B09 H706 H506 +B10 H711 H517 +B11 H714 H505 +B12 H710 H506 +C01 H703 H506 +C02 H701 H517 +C03 H702 H505 +C04 H723 H506 +C05 H705 H517 +C06 H707 H505 +C07 H720 H506 +C08 H706 H517 +C09 H712 H505 +C10 H714 H506 +C11 H710 H517 +C12 H711 H505 +D01 H705 H503 +D02 H707 H503 +D03 H723 H503 +D04 H706 H503 +D05 H712 H503 +D06 H720 H503 +D07 H710 H503 +D08 H711 H503 +D09 H714 H503 +D10 H701 H503 +D11 H702 H503 +D12 H703 H503 +E01 H706 H516 +E02 H712 H516 +E03 H720 H516 +E04 H710 H516 +E05 H711 H516 +E06 H714 H516 +E07 H701 H516 +E08 H702 H516 +E09 H703 H516 +E10 H705 H516 +E11 H707 H516 +E12 H723 H516 +F01 H710 H522 +F02 H711 H510 +F03 H714 H513 +F04 H701 H522 +F05 H702 H510 +F06 H703 H513 +F07 H705 H522 +F08 H707 H510 +F09 H723 H513 +F10 H706 H522 +F11 H712 H510 +F12 H720 H513 +G01 H711 H513 +G02 H714 H522 +G03 H710 H510 +G04 H702 H513 +G05 H703 H522 +G06 H701 H510 +G07 H707 H513 +G08 H723 H522 +G09 H705 H510 +G10 H712 H513 +G11 H720 H522 +G12 H706 H510 +H01 H714 H510 +H02 H710 H513 +H03 H711 H522 +H04 H703 H510 +H05 H701 H513 +H06 H702 H522 +H07 H723 H510 +H08 H705 H513 +H09 H707 H522 +H10 H720 H510 +H11 H706 H513 +H12 H712 H522 +[DefaultLayout_SingleIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 +[DefaultLayout_DualIndex] +A01 A01 +B01 B01 +C01 C01 +D01 D01 +E01 E01 +F01 F01 +G01 G01 +H01 H01 +A02 A02 +B02 B02 +C02 C02 +D02 D02 +E02 E02 +F02 F02 +G02 G02 +H02 H02 +A03 A03 +B03 B03 +C03 C03 +D03 D03 +E03 E03 +F03 F03 +G03 G03 +H03 H03 +A04 A04 +B04 B04 +C04 C04 +D04 D04 +E04 E04 +F04 F04 +G04 G04 +H04 H04 +A05 A05 +B05 B05 +C05 C05 +D05 D05 +E05 E05 +F05 F05 +G05 G05 +H05 H05 +A06 A06 +B06 B06 +C06 C06 +D06 D06 +E06 E06 +F06 F06 +G06 G06 +H06 H06 +A07 A07 +B07 B07 +C07 C07 +D07 D07 +E07 E07 +F07 F07 +G07 G07 +H07 H07 +A08 A08 +B08 B08 +C08 C08 +D08 D08 +E08 E08 +F08 F08 +G08 G08 +H08 H08 +A09 A09 +B09 B09 +C09 C09 +D09 D09 +E09 E09 +F09 F09 +G09 G09 +H09 H09 +A10 A10 +B10 B10 +C10 C10 +D10 D10 +E10 E10 +F10 F10 +G10 G10 +H10 H10 +A11 A11 +B11 B11 +C11 C11 +D11 D11 +E11 E11 +F11 F11 +G11 G11 +H11 H11 +A12 A12 +B12 B12 +C12 C12 +D12 D12 +E12 E12 +F12 F12 +G12 G12 +H12 H12 \ No newline at end of file diff --git a/conf/collection_index_kits/Nextera Index Kit (24 Indexes 96 Samples).txt b/conf/collection_index_kits/Nextera Index Kit (24 Indexes 96 Samples).txt new file mode 100644 index 000000000..168e69609 --- /dev/null +++ b/conf/collection_index_kits/Nextera Index Kit (24 Indexes 96 Samples).txt @@ -0,0 +1,70 @@ +[Version] +1 +[Name] +Nextera Index Kit (24 Indexes 96 Samples) +[PlateExtension] +nexfc36 +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +N701 TAAGGCGA +N702 CGTACTAG +N703 AGGCAGAA +N704 TCCTGAGC +N705 GGACTCCT +N706 TAGGCATG +[I5] +N502 CTCTCTAT +N503 TATCCTCT +N504 AGAGTAGA +N517 GCGTAAGA +[DefaultLayout_SingleIndex] +A01 N701 +B01 N701 +C01 N701 +D01 N701 +A02 N702 +B02 N702 +C02 N702 +D02 N702 +A03 N703 +B03 N703 +C03 N703 +D03 N703 +A04 N704 +B04 N704 +C04 N704 +D04 N704 +A05 N705 +B05 N705 +C05 N705 +D05 N705 +A06 N706 +B06 N706 +C06 N706 +D06 N706 +[DefaultLayout_DualIndex] +A01 N701 N502 +B01 N701 N503 +C01 N701 N504 +D01 N701 N517 +A02 N702 N502 +B02 N702 N503 +C02 N702 N504 +D02 N702 N517 +A03 N703 N502 +B03 N703 N503 +C03 N703 N504 +D03 N703 N517 +A04 N704 N502 +B04 N704 N503 +C04 N704 N504 +D04 N704 N517 +A05 N705 N502 +B05 N705 N503 +C05 N705 N504 +D05 N705 N517 +A06 N706 N502 +B06 N706 N503 +C06 N706 N504 +D06 N706 N517 diff --git a/conf/collection_index_kits/Nextera Index Kit (96 Indexes 384 Samples).txt b/conf/collection_index_kits/Nextera Index Kit (96 Indexes 384 Samples).txt new file mode 100644 index 000000000..71d2c33bf --- /dev/null +++ b/conf/collection_index_kits/Nextera Index Kit (96 Indexes 384 Samples).txt @@ -0,0 +1,225 @@ +[Version] +1 +[Name] +Nextera Index Kit (96 Indexes 384 Samples) +[PlateExtension] +nexfc96 +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +N701 TAAGGCGA +N702 CGTACTAG +N703 AGGCAGAA +N704 TCCTGAGC +N705 GGACTCCT +N706 TAGGCATG +N707 CTCTCTAC +N708 CAGAGAGG +N709 GCTACGCT +N710 CGAGGCTG +N711 AAGAGGCA +N712 GTAGAGGA +[I5] +N501 TAGATCGC +N502 CTCTCTAT +N503 TATCCTCT +N504 AGAGTAGA +N505 GTAAGGAG +N506 ACTGCATA +N507 AAGGAGTA +N508 CTAAGCCT +N517 GCGTAAGA +[DefaultLayout_SingleIndex] +A01 N701 +A02 N702 +A03 N703 +A04 N704 +A05 N705 +A06 N706 +A07 N707 +A08 N708 +A09 N709 +A10 N710 +A11 N711 +A12 N712 +B01 N701 +B02 N702 +B03 N703 +B04 N704 +B05 N705 +B06 N706 +B07 N707 +B08 N708 +B09 N709 +B10 N710 +B11 N711 +B12 N712 +C01 N701 +C02 N702 +C03 N703 +C04 N704 +C05 N705 +C06 N706 +C07 N707 +C08 N708 +C09 N709 +C10 N710 +C11 N711 +C12 N712 +D01 N701 +D02 N702 +D03 N703 +D04 N704 +D05 N705 +D06 N706 +D07 N707 +D08 N708 +D09 N709 +D10 N710 +D11 N711 +D12 N712 +E01 N701 +E02 N702 +E03 N703 +E04 N704 +E05 N705 +E06 N706 +E07 N707 +E08 N708 +E09 N709 +E10 N710 +E11 N711 +E12 N712 +F01 N701 +F02 N702 +F03 N703 +F04 N704 +F05 N705 +F06 N706 +F07 N707 +F08 N708 +F09 N709 +F10 N710 +F11 N711 +F12 N712 +G01 N701 +G02 N702 +G03 N703 +G04 N704 +G05 N705 +G06 N706 +G07 N707 +G08 N708 +G09 N709 +G10 N710 +G11 N711 +G12 N712 +H01 N701 +H02 N702 +H03 N703 +H04 N704 +H05 N705 +H06 N706 +H07 N707 +H08 N708 +H09 N709 +H10 N710 +H11 N711 +H12 N712 +[DefaultLayout_DualIndex] +A01 N701 N502 +A02 N702 N502 +A03 N703 N502 +A04 N704 N502 +A05 N705 N502 +A06 N706 N502 +A07 N707 N502 +A08 N708 N502 +A09 N709 N502 +A10 N710 N502 +A11 N711 N502 +A12 N712 N502 +B01 N701 N503 +B02 N702 N503 +B03 N703 N503 +B04 N704 N503 +B05 N705 N503 +B06 N706 N503 +B07 N707 N503 +B08 N708 N503 +B09 N709 N503 +B10 N710 N503 +B11 N711 N503 +B12 N712 N503 +C01 N701 N504 +C02 N702 N504 +C03 N703 N504 +C04 N704 N504 +C05 N705 N504 +C06 N706 N504 +C07 N707 N504 +C08 N708 N504 +C09 N709 N504 +C10 N710 N504 +C11 N711 N504 +C12 N712 N504 +D01 N701 N505 +D02 N702 N505 +D03 N703 N505 +D04 N704 N505 +D05 N705 N505 +D06 N706 N505 +D07 N707 N505 +D08 N708 N505 +D09 N709 N505 +D10 N710 N505 +D11 N711 N505 +D12 N712 N505 +E01 N701 N506 +E02 N702 N506 +E03 N703 N506 +E04 N704 N506 +E05 N705 N506 +E06 N706 N506 +E07 N707 N506 +E08 N708 N506 +E09 N709 N506 +E10 N710 N506 +E11 N711 N506 +E12 N712 N506 +F01 N701 N507 +F02 N702 N507 +F03 N703 N507 +F04 N704 N507 +F05 N705 N507 +F06 N706 N507 +F07 N707 N507 +F08 N708 N507 +F09 N709 N507 +F10 N710 N507 +F11 N711 N507 +F12 N712 N507 +G01 N701 N508 +G02 N702 N508 +G03 N703 N508 +G04 N704 N508 +G05 N705 N508 +G06 N706 N508 +G07 N707 N508 +G08 N708 N508 +G09 N709 N508 +G10 N710 N508 +G11 N711 N508 +G12 N712 N508 +H01 N701 N517 +H02 N702 N517 +H03 N703 N517 +H04 N704 N517 +H05 N705 N517 +H06 N706 N517 +H07 N707 N517 +H08 N708 N517 +H09 N709 N517 +H10 N710 N517 +H11 N711 N517 +H12 N712 N517 diff --git a/conf/collection_index_kits/Nextera XT Index Kit (24 Indexes 96 Samples).txt b/conf/collection_index_kits/Nextera XT Index Kit (24 Indexes 96 Samples).txt new file mode 100644 index 000000000..46c8c80c9 --- /dev/null +++ b/conf/collection_index_kits/Nextera XT Index Kit (24 Indexes 96 Samples).txt @@ -0,0 +1,70 @@ +[Version] +1 +[Name] +Nextera XT Index Kit (24 Indexes 96 Samples) +[PlateExtension] +nexxt24 +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +N701 TAAGGCGA +N702 CGTACTAG +N703 AGGCAGAA +N704 TCCTGAGC +N705 GGACTCCT +N706 TAGGCATG +[I5] +S502 CTCTCTAT +S503 TATCCTCT +S504 AGAGTAGA +S517 GCGTAAGA +[DefaultLayout_SingleIndex] +A01 N701 +B01 N701 +C01 N701 +D01 N701 +A02 N702 +B02 N702 +C02 N702 +D02 N702 +A03 N703 +B03 N703 +C03 N703 +D03 N703 +A04 N704 +B04 N704 +C04 N704 +D04 N704 +A05 N705 +B05 N705 +C05 N705 +D05 N705 +A06 N706 +B06 N706 +C06 N706 +D06 N706 +[DefaultLayout_DualIndex] +A01 N701 S502 +B01 N701 S503 +C01 N701 S504 +D01 N701 S517 +A02 N702 S502 +B02 N702 S503 +C02 N702 S504 +D02 N702 S517 +A03 N703 S502 +B03 N703 S503 +C03 N703 S504 +D03 N703 S517 +A04 N704 S502 +B04 N704 S503 +C04 N704 S504 +D04 N704 S517 +A05 N705 S502 +B05 N705 S503 +C05 N705 S504 +D05 N705 S517 +A06 N706 S502 +B06 N706 S503 +C06 N706 S504 +D06 N706 S517 \ No newline at end of file diff --git a/conf/collection_index_kits/Nextera XT Index Kit (96 Indexes 384 Samples).txt b/conf/collection_index_kits/Nextera XT Index Kit (96 Indexes 384 Samples).txt new file mode 100644 index 000000000..2c220d40b --- /dev/null +++ b/conf/collection_index_kits/Nextera XT Index Kit (96 Indexes 384 Samples).txt @@ -0,0 +1,225 @@ +[Version] +1 +[Name] +Nextera XT Index Kit (96 Indexes 384 Samples) +[PlateExtension] +nexxt96 +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +N701 TAAGGCGA +N702 CGTACTAG +N703 AGGCAGAA +N704 TCCTGAGC +N705 GGACTCCT +N706 TAGGCATG +N707 CTCTCTAC +N708 CAGAGAGG +N709 GCTACGCT +N710 CGAGGCTG +N711 AAGAGGCA +N712 GTAGAGGA +[I5] +S501 TAGATCGC +S502 CTCTCTAT +S503 TATCCTCT +S504 AGAGTAGA +S505 GTAAGGAG +S506 ACTGCATA +S507 AAGGAGTA +S508 CTAAGCCT +S517 GCGTAAGA +[DefaultLayout_SingleIndex] +A01 N701 +A02 N702 +A03 N703 +A04 N704 +A05 N705 +A06 N706 +A07 N707 +A08 N708 +A09 N709 +A10 N710 +A11 N711 +A12 N712 +B01 N701 +B02 N702 +B03 N703 +B04 N704 +B05 N705 +B06 N706 +B07 N707 +B08 N708 +B09 N709 +B10 N710 +B11 N711 +B12 N712 +C01 N701 +C02 N702 +C03 N703 +C04 N704 +C05 N705 +C06 N706 +C07 N707 +C08 N708 +C09 N709 +C10 N710 +C11 N711 +C12 N712 +D01 N701 +D02 N702 +D03 N703 +D04 N704 +D05 N705 +D06 N706 +D07 N707 +D08 N708 +D09 N709 +D10 N710 +D11 N711 +D12 N712 +E01 N701 +E02 N702 +E03 N703 +E04 N704 +E05 N705 +E06 N706 +E07 N707 +E08 N708 +E09 N709 +E10 N710 +E11 N711 +E12 N712 +F01 N701 +F02 N702 +F03 N703 +F04 N704 +F05 N705 +F06 N706 +F07 N707 +F08 N708 +F09 N709 +F10 N710 +F11 N711 +F12 N712 +G01 N701 +G02 N702 +G03 N703 +G04 N704 +G05 N705 +G06 N706 +G07 N707 +G08 N708 +G09 N709 +G10 N710 +G11 N711 +G12 N712 +H01 N701 +H02 N702 +H03 N703 +H04 N704 +H05 N705 +H06 N706 +H07 N707 +H08 N708 +H09 N709 +H10 N710 +H11 N711 +H12 N712 +[DefaultLayout_DualIndex] +A01 N701 S502 +A02 N702 S502 +A03 N703 S502 +A04 N704 S502 +A05 N705 S502 +A06 N706 S502 +A07 N707 S502 +A08 N708 S502 +A09 N709 S502 +A10 N710 S502 +A11 N711 S502 +A12 N712 S502 +B01 N701 S503 +B02 N702 S503 +B03 N703 S503 +B04 N704 S503 +B05 N705 S503 +B06 N706 S503 +B07 N707 S503 +B08 N708 S503 +B09 N709 S503 +B10 N710 S503 +B11 N711 S503 +B12 N712 S503 +C01 N701 S504 +C02 N702 S504 +C03 N703 S504 +C04 N704 S504 +C05 N705 S504 +C06 N706 S504 +C07 N707 S504 +C08 N708 S504 +C09 N709 S504 +C10 N710 S504 +C11 N711 S504 +C12 N712 S504 +D01 N701 S505 +D02 N702 S505 +D03 N703 S505 +D04 N704 S505 +D05 N705 S505 +D06 N706 S505 +D07 N707 S505 +D08 N708 S505 +D09 N709 S505 +D10 N710 S505 +D11 N711 S505 +D12 N712 S505 +E01 N701 S506 +E02 N702 S506 +E03 N703 S506 +E04 N704 S506 +E05 N705 S506 +E06 N706 S506 +E07 N707 S506 +E08 N708 S506 +E09 N709 S506 +E10 N710 S506 +E11 N711 S506 +E12 N712 S506 +F01 N701 S507 +F02 N702 S507 +F03 N703 S507 +F04 N704 S507 +F05 N705 S507 +F06 N706 S507 +F07 N707 S507 +F08 N708 S507 +F09 N709 S507 +F10 N710 S507 +F11 N711 S507 +F12 N712 S507 +G01 N701 S508 +G02 N702 S508 +G03 N703 S508 +G04 N704 S508 +G05 N705 S508 +G06 N706 S508 +G07 N707 S508 +G08 N708 S508 +G09 N709 S508 +G10 N710 S508 +G11 N711 S508 +G12 N712 S508 +H01 N701 S517 +H02 N702 S517 +H03 N703 S517 +H04 N704 S517 +H05 N705 S517 +H06 N706 S517 +H07 N707 S517 +H08 N708 S517 +H09 N709 S517 +H10 N710 S517 +H11 N711 S517 +H12 N712 S517 diff --git a/conf/collection_index_kits/Nextera XT v2 Index Kit A.txt b/conf/collection_index_kits/Nextera XT v2 Index Kit A.txt new file mode 100644 index 000000000..fd5d306ba --- /dev/null +++ b/conf/collection_index_kits/Nextera XT v2 Index Kit A.txt @@ -0,0 +1,224 @@ +[Version] +1 +[Name] +Nextera XT v2 Index Kit A +[PlateExtension] +nexxtv2a +[Settings] +Adapter CTGTCTCTTATACACATCT +[I7] +N701 TAAGGCGA +N702 CGTACTAG +N703 AGGCAGAA +N704 TCCTGAGC +N705 GGACTCCT +N706 TAGGCATG +N707 CTCTCTAC +N710 CGAGGCTG +N711 AAGAGGCA +N712 GTAGAGGA +N714 GCTCATGA +N715 ATCTCAGG +[I5] +S502 CTCTCTAT +S503 TATCCTCT +S505 GTAAGGAG +S506 ACTGCATA +S507 AAGGAGTA +S508 CTAAGCCT +S510 CGTCTAAT +S511 TCTCTCCG +[DefaultLayout_SingleIndex] +A01 N701 +A02 N702 +A03 N703 +A04 N704 +A05 N705 +A06 N706 +A07 N707 +A08 N710 +A09 N711 +A10 N712 +A11 N714 +A12 N715 +B01 N701 +B02 N702 +B03 N703 +B04 N704 +B05 N705 +B06 N706 +B07 N707 +B08 N710 +B09 N711 +B10 N712 +B11 N714 +B12 N715 +C01 N701 +C02 N702 +C03 N703 +C04 N704 +C05 N705 +C06 N706 +C07 N707 +C08 N710 +C09 N711 +C10 N712 +C11 N714 +C12 N715 +D01 N701 +D02 N702 +D03 N703 +D04 N704 +D05 N705 +D06 N706 +D07 N707 +D08 N710 +D09 N711 +D10 N712 +D11 N714 +D12 N715 +E01 N701 +E02 N702 +E03 N703 +E04 N704 +E05 N705 +E06 N706 +E07 N707 +E08 N710 +E09 N711 +E10 N712 +E11 N714 +E12 N715 +F01 N701 +F02 N702 +F03 N703 +F04 N704 +F05 N705 +F06 N706 +F07 N707 +F08 N710 +F09 N711 +F10 N712 +F11 N714 +F12 N715 +G01 N701 +G02 N702 +G03 N703 +G04 N704 +G05 N705 +G06 N706 +G07 N707 +G08 N710 +G09 N711 +G10 N712 +G11 N714 +G12 N715 +H01 N701 +H02 N702 +H03 N703 +H04 N704 +H05 N705 +H06 N706 +H07 N707 +H08 N710 +H09 N711 +H10 N712 +H11 N714 +H12 N715 +[DefaultLayout_DualIndex] +A01 N701 S502 +A02 N702 S502 +A03 N703 S502 +A04 N704 S502 +A05 N705 S502 +A06 N706 S502 +A07 N707 S502 +A08 N710 S502 +A09 N711 S502 +A10 N712 S502 +A11 N714 S502 +A12 N715 S502 +B01 N701 S503 +B02 N702 S503 +B03 N703 S503 +B04 N704 S503 +B05 N705 S503 +B06 N706 S503 +B07 N707 S503 +B08 N710 S503 +B09 N711 S503 +B10 N712 S503 +B11 N714 S503 +B12 N715 S503 +C01 N701 S505 +C02 N702 S505 +C03 N703 S505 +C04 N704 S505 +C05 N705 S505 +C06 N706 S505 +C07 N707 S505 +C08 N710 S505 +C09 N711 S505 +C10 N712 S505 +C11 N714 S505 +C12 N715 S505 +D01 N701 S506 +D02 N702 S506 +D03 N703 S506 +D04 N704 S506 +D05 N705 S506 +D06 N706 S506 +D07 N707 S506 +D08 N710 S506 +D09 N711 S506 +D10 N712 S506 +D11 N714 S506 +D12 N715 S506 +E01 N701 S507 +E02 N702 S507 +E03 N703 S507 +E04 N704 S507 +E05 N705 S507 +E06 N706 S507 +E07 N707 S507 +E08 N710 S507 +E09 N711 S507 +E10 N712 S507 +E11 N714 S507 +E12 N715 S507 +F01 N701 S508 +F02 N702 S508 +F03 N703 S508 +F04 N704 S508 +F05 N705 S508 +F06 N706 S508 +F07 N707 S508 +F08 N710 S508 +F09 N711 S508 +F10 N712 S508 +F11 N714 S508 +F12 N715 S508 +G01 N701 S510 +G02 N702 S510 +G03 N703 S510 +G04 N704 S510 +G05 N705 S510 +G06 N706 S510 +G07 N707 S510 +G08 N710 S510 +G09 N711 S510 +G10 N712 S510 +G11 N714 S510 +G12 N715 S510 +H01 N701 S511 +H02 N702 S511 +H03 N703 S511 +H04 N704 S511 +H05 N705 S511 +H06 N706 S511 +H07 N707 S511 +H08 N710 S511 +H09 N711 S511 +H10 N712 S511 +H11 N714 S511 +H12 N715 S511 \ No newline at end of file diff --git a/conf/collection_index_kits/TruSeq RNA Single Indexes Set A.txt b/conf/collection_index_kits/TruSeq RNA Single Indexes Set A.txt new file mode 100644 index 000000000..bb771a323 --- /dev/null +++ b/conf/collection_index_kits/TruSeq RNA Single Indexes Set A.txt @@ -0,0 +1,119 @@ +[Version] +1 +[Name] +TruSeq RNA Single Indexes Set A +[PlateExtension] +trursinglea +[Settings] +Adapter AGATCGGAAGAGCACACGTCTGAACTCCAGTCA +AdapterRead2 AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGT +[I7] +AR002 CGATGT +AR004 TGACCA +AR005 ACAGTG +AR006 GCCAAT +AR007 CAGATC +AR012 CTTGTA +AR013 AGTCAA +AR014 AGTTCC +AR015 ATGTCA +AR016 CCGTCC +AR018 GTCCGC +AR019 GTGAAA +[DefaultLayout_SingleIndex] +A01 AR002 +A02 AR004 +A03 AR005 +A04 AR006 +A05 AR007 +A06 AR012 +A07 AR013 +A08 AR014 +A09 AR015 +A10 AR016 +A11 AR018 +A12 AR019 +B01 AR002 +B02 AR004 +B03 AR005 +B04 AR006 +B05 AR007 +B06 AR012 +B07 AR013 +B08 AR014 +B09 AR015 +B10 AR016 +B11 AR018 +B12 AR019 +C01 AR002 +C02 AR004 +C03 AR005 +C04 AR006 +C05 AR007 +C06 AR012 +C07 AR013 +C08 AR014 +C09 AR015 +C10 AR016 +C11 AR018 +C12 AR019 +D01 AR002 +D02 AR004 +D03 AR005 +D04 AR006 +D05 AR007 +D06 AR012 +D07 AR013 +D08 AR014 +D09 AR015 +D10 AR016 +D11 AR018 +D12 AR019 +E01 AR002 +E02 AR004 +E03 AR005 +E04 AR006 +E05 AR007 +E06 AR012 +E07 AR013 +E08 AR014 +E09 AR015 +E10 AR016 +E11 AR018 +E12 AR019 +F01 AR002 +F02 AR004 +F03 AR005 +F04 AR006 +F05 AR007 +F06 AR012 +F07 AR013 +F08 AR014 +F09 AR015 +F10 AR016 +F11 AR018 +F12 AR019 +G01 AR002 +G02 AR004 +G03 AR005 +G04 AR006 +G05 AR007 +G06 AR012 +G07 AR013 +G08 AR014 +G09 AR015 +G10 AR016 +G11 AR018 +G12 AR019 +H01 AR002 +H02 AR004 +H03 AR005 +H04 AR006 +H05 AR007 +H06 AR012 +H07 AR013 +H08 AR014 +H09 AR015 +H10 AR016 +H11 AR018 +H12 AR019 \ No newline at end of file diff --git a/conf/docker_install_settings.txt b/conf/docker_install_settings.txt deleted file mode 100644 index e16211355..000000000 --- a/conf/docker_install_settings.txt +++ /dev/null @@ -1,30 +0,0 @@ -### Installation path -INSTALL_PATH='/opt/iskylims' - -### (optional) Python installation path where pip and python executables are located -PYTHON_BIN_PATH='python3' # example: /opt/python/3.9.6/bin/python3 - -### Settings required to access database - -DB_USER='django' -DB_PASS='djangopass' -DB_NAME='iskylims_docker' -DB_SERVER_IP='db' -DB_PORT=3306 - -### Settings required for sending emails - -EMAIL_HOST_SERVER='localhost' -EMAIL_PORT='25' -EMAIL_HOST_USER='bioinfo' -EMAIL_HOST_PASSWORD='' -EMAIL_USE_TLS='False' - -### Settings required for accessing iSkyLIMS -LOCAL_SERVER_IP='*' # example: 172.0.0.1 -DNS_URL='*' # example: iskylims.isciii.es - -### Logs settings -LOG_TYPE="regular_folder" # can be symbolic link, or regular_folder -LOG_PATH="" # mandatory if LOG_TYPE="symbolic_link", where is the log folder so we can create a symbolic link in the repository folder. - diff --git a/conf/docker_production_settings.txt b/conf/docker_production_settings.txt new file mode 100644 index 000000000..ff4582770 --- /dev/null +++ b/conf/docker_production_settings.txt @@ -0,0 +1,64 @@ +### Production specific installation config +# Copy this file or edit the values below so they match the external +# infrastructure you are connecting to before building the image. + +### Installation path and modules settings +INSTALL_PATH='/opt/iskylims' +REQUIRED_MODULES='core drylab wetlab clinic django_utils' +MIGRATION_MODULES='core drylab wetlab django_utils' +FAKEINITIAL_MODULES='django_utils iSkyLIMS_core iSkyLIMS_wetlab iSkyLIMS_drylab' + +### Container build/runtime settings +# Runtime installation root used by the app container. +# Leave empty to reuse INSTALL_PATH. If you are not sure leave it empty. +APP_INSTALL_PATH='' +# Host directory for Apache bind-mounted configuration files. +# Leave empty to use ${APP_INSTALL_PATH}/conf. +# Example: APACHE_CONF_PATH='/srv/containers/bind/iskylims_apache_conf' +APACHE_CONF_PATH='' +# Host path for the bind-mounted Django settings.py. +# Leave empty to use ${APP_INSTALL_PATH}/iskylims/settings.py. +# If this is a directory or ends with /, container_install.sh appends settings.py. +# Example: DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims_app_setting' +DJANGO_SETTINGS_PATH='' +# UID/GID for the non-root iskylims user inside the app container. +APP_UID='1212' +APP_GID='1212' +# Shell assigned to the container runtime user during image build. +APP_SHELL='/sbin/nologin' +# Internal app port used by Gunicorn. +APP_PORT='8001' +# Django production debug flag. Keep disabled in production. +DJANGO_DEBUG='false' +# Django persistent database connection lifetime in seconds. +DB_CONN_MAX_AGE='60' +# Gunicorn runtime tuning. +WEB_CONCURRENCY='2' +GUNICORN_THREADS='2' +GUNICORN_TIMEOUT='300' +GUNICORN_KEEPALIVE='5' + +### (optional) Python installation path where pip and python executables are located +PYTHON_BIN_PATH='python3.11' + +### Settings required to access database (external MySQL server) +DB_USER='django' +DB_PASS='djangopass' +DB_NAME='iskylims' +DB_SERVER_IP='host.docker.internal' # hostname or IP of the production DB (use host.docker.internal for host DB) +DB_PORT=3306 + +### Settings required for sending emails +EMAIL_HOST_SERVER='host.docker.internal' +EMAIL_PORT='25' +EMAIL_HOST_USER='bioinformatica@isciii.es' +EMAIL_HOST_PASSWORD='' +EMAIL_USE_TLS='False' + +### Settings required for accessing iSkyLIMS +LOCAL_SERVER_IP='*' +DNS_URL='*' + +### Logs settings +LOG_TYPE="regular_folder" +LOG_PATH="" diff --git a/conf/docker_test_settings.txt b/conf/docker_test_settings.txt new file mode 100644 index 000000000..c288c3d21 --- /dev/null +++ b/conf/docker_test_settings.txt @@ -0,0 +1,65 @@ +### Installation path and modules settings +INSTALL_PATH='/opt/iskylims' +REQUIRED_MODULES='core drylab wetlab clinic django_utils' +MIGRATION_MODULES='core drylab wetlab django_utils' +FAKEINITIAL_MODULES='django_utils iSkyLIMS_core iSkyLIMS_wetlab iSkyLIMS_drylab' + +### Container build/runtime settings +# Runtime installation root used by the app container. +# Leave empty to reuse INSTALL_PATH. +APP_INSTALL_PATH='' +# Host directory for Apache bind-mounted configuration files. +# Leave empty to use ${APP_INSTALL_PATH}/conf. +# Example: APACHE_CONF_PATH='/srv/containers/bind/iskylims_apache_conf' +APACHE_CONF_PATH='' +# Host path for the bind-mounted Django settings.py. +# Leave empty to use ${APP_INSTALL_PATH}/iskylims/settings.py. +# If this is a directory or ends with /, container_install.sh appends settings.py. +# Example: DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims_app_setting' +DJANGO_SETTINGS_PATH='' +# UID/GID for the non-root iskylims user inside the app container. +APP_UID='1212' +APP_GID='1212' +# Shell assigned to the container runtime user during image build. +APP_SHELL='/bin/bash' +# Internal app port used by Django runserver/Gunicorn. +APP_PORT='8001' +# Django production debug flag. Keep disabled in production. +DJANGO_DEBUG='false' +# Django persistent database connection lifetime in seconds. +DB_CONN_MAX_AGE='60' +# Gunicorn runtime tuning. +WEB_CONCURRENCY='2' +GUNICORN_THREADS='2' +GUNICORN_TIMEOUT='300' +GUNICORN_KEEPALIVE='5' + +### (optional) Python installation path where pip and python executables are located +PYTHON_BIN_PATH='python3' # example: /opt/python/3.9.6/bin/python3 + +### Settings required to access database + +DB_USER='django' +DB_PASS='djangopass' +DB_NAME='iskylims_docker' +DB_SERVER_IP='db' +DB_PORT=3306 + +### Settings required for sending emails + +EMAIL_HOST_SERVER='host.docker.internal' +EMAIL_PORT='25' +EMAIL_HOST_USER='bioinformatica@isciii.es' +EMAIL_HOST_PASSWORD='' +EMAIL_USE_TLS='False' + +### Settings required for accessing iSkyLIMS +# example: 172.0.0.1 +LOCAL_SERVER_IP='*' +# example: iskylims.isciii.es +DNS_URL='*' + +### Logs settings +LOG_TYPE="regular_folder" # can be symbolic link, or regular_folder +LOG_PATH="" # mandatory if LOG_TYPE="symbolic_link", where is the log folder so we can create a symbolic link in the repository folder. + diff --git a/conf/first_install_tables.json b/conf/first_install_tables.json index a6982e522..94a7c0f2e 100644 --- a/conf/first_install_tables.json +++ b/conf/first_install_tables.json @@ -257,6 +257,21 @@ "sequencer_number_lanes": "2" } }, +{ + "model": "core.sequencerinlab", + "pk": 7, + "fields": { + "platform_id": 7, + "sequencer_name": "SH00752", + "sequencer_description": "MiSeqi100", + "sequencer_location": "Genomics Unit", + "sequencer_serial_number": null, + "sequencer_state": "In Use", + "sequencer_operation_start": null, + "sequencer_operation_end": null, + "sequencer_number_lanes": "1/2/4" + } +}, { "model": "core.statesforsample", "pk": 1, @@ -324,21 +339,32 @@ "model": "core.statesformolecule", "pk": 3, "fields": { - "molecule_state_name": "Defined" + "molecule_state_name": "defined", + "molecule_state_display": "Defined" } }, { "model": "core.statesformolecule", "pk": 4, "fields": { - "molecule_state_name": "Assigned Protocol" + "molecule_state_name": "assigned_parameters", + "molecule_state_display": "Assigned Parameters" } }, { "model": "core.statesformolecule", "pk": 5, "fields": { - "molecule_state_name": "Completed" + "molecule_state_name": "completed", + "molecule_state_display": "Completed" + } +}, +{ + "model": "core.statesformolecule", + "pk": 6, + "fields": { + "molecule_state_name": "sent_external", + "molecule_state_display": "Sent to external service" } }, { @@ -1512,84 +1538,100 @@ "model": "wetlab.runstates", "pk": 1, "fields": { - "run_state_name": "Pre-Recorded" + "run_state_name": "pre_recorded", + "state_display": "Pre Recorded", + "description": "Run name is defined but pending for input data.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 2, "fields": { - "run_state_name": "Recorded" + "run_state_name": "recorded", + "state_display": "Recorded", + "description": "Run name is defined.", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 3, "fields": { - "run_state_name": "Sample Sent" + "run_state_name": "sample_sent", + "state_display": "Sample Sent", + "description": "Run has copied sample sheet on remote server.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 4, "fields": { - "run_state_name": "Processing Run" + "run_state_name": "processing_run", + "state_display": "Processing Run", + "description": "Run is handled on the sequencer.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 5, "fields": { - "run_state_name": "Processed Run" + "run_state_name": "processed_run", + "state_display": "Processed Run", + "description": "Run is already processed on sequencer.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 6, "fields": { - "run_state_name": "Processing Bcl2fastq" + "run_state_name": "processing_bcl2fastq", + "state_display": "Processing Bcl2fastq", + "description": "Bcl2fastq is running", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 7, "fields": { - "run_state_name": "Processed Bcl2fastq" + "run_state_name": "processed_bcl2fastq", + "state_display": "Processed Bcl2fastq", + "description": "Bcl2fastq already completed", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 8, "fields": { - "run_state_name": "Completed" + "run_state_name": "completed", + "state_display": "Completed", + "description": "Run is completed and all processes are finished.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 9, "fields": { - "run_state_name": "Cancelled" + "run_state_name": "cancelled", + "state_display": "Cancelled", + "description": "Run was manually cancelled while running on sequencer.", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 10, "fields": { - "run_state_name": "Error" - } -}, -{ - "model": "wetlab.runstates", - "pk": 11, - "fields": { - "run_state_name": "Processing Metrics" - } -}, -{ - "model": "wetlab.runstates", - "pk": 12, - "fields": { - "run_state_name": "Processing Demultiplexing" + "run_state_name": "error", + "state_display": "Error", + "description": "There was an issue while processing the run", + "show_in_stats": true } }, { @@ -1888,6 +1930,14 @@ "run_test_folder": "NovaSeq_Test" } }, +{ + "model": "wetlab.runconfigurationtest", + "pk": 4, + "fields": { + "run_test_name": "MiSeqi100_Test", + "run_test_folder": "Miseqi100_Test" + } +}, { "model": "wetlab.configsetting", "pk": 1, @@ -1996,6 +2046,33 @@ "generated_at": "2021-01-28T22:59:03.042" } }, +{ + "model": "wetlab.configsetting", + "pk": 15, + "fields": { + "configuration_name": "ALLOW_REPEAT_SAMPLE_NAMES", + "configuration_value": "FALSE", + "generated_at": "2024-05-28T22:59:03.142" + } +}, +{ + "model": "wetlab.configsetting", + "pk": 16, + "fields": { + "configuration_name": "ALLOW_REPEAT_SAMPLE_NAMES_PER_RESEARCHER", + "configuration_value": "FALSE", + "generated_at": "2024-05-28T22:59:04.042" + } +}, +{ + "model": "wetlab.configsetting", + "pk": 17, + "fields": { + "configuration_name": "ALLOW_SAMPLE_NAMES_WITH_UNDERSCORE", + "configuration_value": "FALSE", + "generated_at": "2024-05-28T22:59:05.042" + } +}, { "model": "wetlab.collectionindexvalues", "pk": 2073, @@ -20087,9 +20164,9 @@ "model": "drylab.servicestate", "pk": 2, "fields": { - "state_value": "approved", - "state_display": "Approved", - "description": "Service request has been accepted", + "state_value": "archived", + "state_display": "Archived", + "description": null, "show_in_stats": false } }, @@ -20097,9 +20174,9 @@ "model": "drylab.servicestate", "pk": 3, "fields": { - "state_value": "rejected", - "state_display": "Rejected", - "description": "Service can not be handle because is not in porfoloio or for any other reason", + "state_value": "approved", + "state_display": "Approved", + "description": "Service request has been accepted", "show_in_stats": false } }, @@ -20107,9 +20184,9 @@ "model": "drylab.servicestate", "pk": 4, "fields": { - "state_value": "queued", - "state_display": "Queued", - "description": "Service is the the list, waiting for a bioinformatic to be handled", + "state_value": "in_progress", + "state_display": "In Progress", + "description": "Bioinformatic is working on the service", "show_in_stats": true } }, @@ -20117,10 +20194,10 @@ "model": "drylab.servicestate", "pk": 5, "fields": { - "state_value": "in_progress", - "state_display": "In Progress", - "description": "Bioinformatic is working on the service", - "show_in_stats": true + "state_value": "rejected", + "state_display": "Rejected", + "description": "Service can not be handle because is not in porfoloio or for any other reason", + "show_in_stats": false } }, { @@ -20130,17 +20207,17 @@ "state_value": "delivered", "state_display": "Delivered", "description": "Service wascompleted and results were sent to the user", - "show_in_stats": false + "show_in_stats": true } }, { "model": "drylab.servicestate", "pk": 7, "fields": { - "state_value": "archived", - "state_display": "Archived", - "description": null, - "show_in_stats": false + "state_value": "queued", + "state_display": "Queued", + "description": "Service is the the list, waiting for a bioinformatic to be handled", + "show_in_stats": true } }, { @@ -20149,10 +20226,11 @@ "fields": { "state_value": "on_hold", "state_display": "On hold", - "description": "Service is on hold, waiting for user provides more information", + "description": "Resolution is on hold, waiting for user provides more information", "show_in_stats": true } -}, +} +, { "model": "drylab.resolutionstates", "pk": 1, @@ -20166,18 +20244,18 @@ "model": "drylab.resolutionstates", "pk": 2, "fields": { - "state_value": "approved", - "state_display": "Approved", - "description": "Resolution request has been accepted" + "state_value": "in_progress", + "state_display": "In Progress", + "description": "Bioinformatic is working on the Resolution" } }, { "model": "drylab.resolutionstates", "pk": 3, "fields": { - "state_value": "rejected", - "state_display": "Rejected", - "description": "Resolution can not be handle because is not in porfoloio or for any other reason" + "state_value": "delivered", + "state_display": "Delivered", + "description": "Resolution wascompleted and results were sent to the user" } }, { @@ -20193,18 +20271,18 @@ "model": "drylab.resolutionstates", "pk": 5, "fields": { - "state_value": "in_progress", - "state_display": "In Progress", - "description": "Bioinformatic is working on the Resolution" + "state_value": "rejected", + "state_display": "Rejected", + "description": "Resolution can not be handle because is not in porfoloio or for any other reason" } }, { "model": "drylab.resolutionstates", "pk": 6, "fields": { - "state_value": "delivered", - "state_display": "Delivered", - "description": "Resolution wascompleted and results were sent to the user" + "state_value": "approved", + "state_display": "Approved", + "description": "Resolution request has been accepted" } }, { @@ -20224,5 +20302,47 @@ "state_display": "On hold", "description": "Resolution is on hold, waiting for user provides more information" } +}, +{ "model": "core.ontologymap", + "pk": 1, + "fields": { + "label": "collection_sample_date", + "ontology": "GENEPIO:0001174" + } +}, +{ "model": "core.ontologymap", + "pk": 2, + "fields": { + "label": "sample_entry_date", + "ontology": "NCIT:C93644" + } +}, +{ "model": "core.ontologymap", + "pk": 3, + "fields": { + "label": "sample_name", + "ontology": "NCIT:C70663" + } +}, +{ "model": "core.ontologymap", + "pk": 4, + "fields": { + "label": "species", + "ontology": "GENEPIO:0001386" + } +}, +{ "model": "core.ontologymap", + "pk": 5, + "fields": { + "label": "lab_request", + "ontology": "GENEPIO:0001153" + } +}, +{ "model": "core.ontologymap", + "pk": 6, + "fields": { + "label": "sequencing_date", + "ontology": "GENEPIO:0001447" + } } ] diff --git a/conf/iskylims_apache_logs.conf b/conf/iskylims_apache_logs.conf new file mode 100644 index 000000000..0ecafdbdd --- /dev/null +++ b/conf/iskylims_apache_logs.conf @@ -0,0 +1,17 @@ +ErrorLog "logs/apache_error.log" + + + LogFormat "%v %a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" combined + LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D %I %O" proxy + LogFormat "%v %{X-Forwarded-For}i %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + + SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded + CustomLog "logs/apache.access.log" combined env=!forwarded + CustomLog "logs/apache.access.log" proxy env=forwarded + diff --git a/conf/iskylims_apache_reverse_proxy.conf b/conf/iskylims_apache_reverse_proxy.conf new file mode 100644 index 000000000..07d526b39 --- /dev/null +++ b/conf/iskylims_apache_reverse_proxy.conf @@ -0,0 +1,36 @@ +# Apache reverse proxy config for iSkyLIMS production container + + + ServerName __ISKYLIMS_SERVER_NAME__ + + ProxyPreserveHost On + RequestHeader set X-Forwarded-Proto "https" + RequestHeader set X-Forwarded-Port "443" + RequestHeader set X-Forwarded-Host "__ISKYLIMS_SERVER_NAME__" + + + # Allow larger POST bodies to reach Django instead of being rejected by + # Apache first. Adjust this value if your deployment needs larger uploads. + LimitRequestBody 52428800 + + + SecRequestBodyLimit 52428800 + SecRequestBodyNoFilesLimit 52428800 + + + ProxyPass /static ! + Alias /static __APP_INSTALL_PATH__/static + + Require all granted + + + ProxyPass / http://app:__APP_PORT__/ + ProxyPassReverse / http://app:__APP_PORT__/ + ProxyTimeout __GUNICORN_TIMEOUT__ + TimeOut __GUNICORN_TIMEOUT__ + + CustomLog logs/__ISKYLIMS_LOG_NAME__-apache.access.log combined env=!forwarded + CustomLog logs/__ISKYLIMS_LOG_NAME__-apache.access.log proxy env=forwarded + ErrorLog logs/__ISKYLIMS_LOG_NAME__-apache.error.log + + diff --git a/conf/ontology_maps.json b/conf/ontology_maps.json index 92215ec30..a4b6d9ebf 100644 --- a/conf/ontology_maps.json +++ b/conf/ontology_maps.json @@ -20,7 +20,7 @@ "pk": 3, "fields": { "label": "species", - "ontology": "GENEPIO:0001386" + "ontology": "SNOMED:410607006" } }, { @@ -28,7 +28,7 @@ "pk": 4, "fields": { "label": "sample_name", - "ontology": "GENEPIO:0001123" + "ontology": "GENEPIO:0000079" } }, { @@ -36,7 +36,7 @@ "pk": 6, "fields": { "label": "sampleEntryDate", - "ontology": "GENEPIO:0001179" + "ontology": "SNOMED:281271004" } }, { @@ -44,7 +44,7 @@ "pk": 7, "fields": { "label": "collectionSampleDate", - "ontology": "GENEPIO:0001174" + "ontology": "SNOMED:399445004" } } ] diff --git a/conf/requirements.txt b/conf/requirements.txt index a16372b77..b4c5e06f0 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -1,26 +1,28 @@ -wheel==0.37.1 -asn1crypto==1.5.0 -bcrypt==4.0.1 -biopython==1.79 -cryptography==38.0.3 -Django==4.2 -django-crispy-forms==2.0 +wheel==0.46.2 +asn1crypto==1.5.1 +bcrypt==4.2.0 +biopython==1.84 +cryptography==44.0.3 +Django==4.2.28 +django-crispy-forms==2.3 crispy-bootstrap5==0.7 django-crontab==0.7.1 -django-js-asset==2.0.0 -django-mptt==0.14.0 -django-mptt-admin==2.4.1 -django-cleanup==7.0.0 +django-js-asset==2.2.0 +django-mptt==0.16.0 +django-mptt-admin==2.6.2 +django-cleanup==8.1.0 interop>1.1.22 -mod_wsgi==4.9.4 -mysqlclient==2.0.3 -paramiko==3.1.0 -jsonschema==4.17.3 +mod_wsgi==5.0.0 +gunicorn==22.0.0 +mysqlclient==2.2.6 +paramiko==3.4.1 +jsonschema==4.23.0 pysmb==1.2.9.1 -django_extensions==3.2.1 -djangorestframework==3.14.0 -drf-yasg==1.21.5 +django_extensions==3.2.3 +djangorestframework==3.15.2 +drf-yasg==1.21.7 xlrd==2.0.1 -pandas==1.5.3 +pandas==2.2.2 numpy==1.26.4 -openpyxl==3.1.1 +openpyxl==3.1.5 +setuptools==78.1.1 diff --git a/conf/template_install_settings.txt b/conf/template_install_settings.txt index 500a09a1a..3b3cc004a 100644 --- a/conf/template_install_settings.txt +++ b/conf/template_install_settings.txt @@ -1,5 +1,38 @@ -### Installation path +### Installation path and modules settings INSTALL_PATH='/opt/iskylims' +REQUIRED_MODULES='core drylab wetlab clinic django_utils' +MIGRATION_MODULES='core drylab wetlab django_utils' +FAKEINITIAL_MODULES='django_utils iSkyLIMS_core iSkyLIMS_wetlab iSkyLIMS_drylab' + +### Container build/runtime settings +# Runtime installation root used by the app container. +# Leave empty to reuse INSTALL_PATH. +APP_INSTALL_PATH='' +# Host directory for Apache bind-mounted configuration files. +# Leave empty to use ${APP_INSTALL_PATH}/conf. +# Example: APACHE_CONF_PATH='/srv/containers/bind/iskylims_apache_conf' +APACHE_CONF_PATH='' +# Host path for the bind-mounted Django settings.py. +# Leave empty to use ${APP_INSTALL_PATH}/iskylims/settings.py. +# If this is a directory or ends with /, container_install.sh appends settings.py. +# Example: DJANGO_SETTINGS_PATH='/srv/containers/bind/iskylims_app_setting' +DJANGO_SETTINGS_PATH='' +# UID/GID for the non-root iskylims user inside the app container. +APP_UID='1212' +APP_GID='1212' +# Shell assigned to the container runtime user during image build. +APP_SHELL='/sbin/nologin' +# Internal app port used by Gunicorn. +APP_PORT='8001' +# Django production debug flag. Keep disabled in production. +DJANGO_DEBUG='false' +# Django persistent database connection lifetime in seconds. +DB_CONN_MAX_AGE='60' +# Gunicorn runtime tuning. +WEB_CONCURRENCY='2' +GUNICORN_THREADS='2' +GUNICORN_TIMEOUT='300' +GUNICORN_KEEPALIVE='5' ### (optional) Python installation path where pip and python executables are located PYTHON_BIN_PATH='python3' # example: /opt/python/3.9.6/bin/python3 @@ -14,17 +47,18 @@ DB_PORT=3306 ### Settings required for sending emails -EMAIL_HOST_SERVER='localhost' +EMAIL_HOST_SERVER='host.docker.internal' EMAIL_PORT='25' -EMAIL_HOST_USER='bioinfo' +EMAIL_HOST_USER='bioinformatica@isciii.es' EMAIL_HOST_PASSWORD='' EMAIL_USE_TLS='False' ### Settings required for accessing iSkyLIMS -LOCAL_SERVER_IP='' # example: 172.0.0.1 -DNS_URL='' # example: iskylims.isciii.es +# example: 172.0.0.1 +LOCAL_SERVER_IP='' +# example: iskylims.isciii.es +DNS_URL='' ### Logs settings LOG_TYPE="symbolic_link" # can be symbolic link, or regular_folder LOG_PATH="" # mandatory if LOG_TYPE="symbolic_link", where is the log folder so we can create a symbolic link in the repository folder. - diff --git a/conf/template_settings.txt b/conf/template_settings.txt index 8cf15d092..6367cb736 100644 --- a/conf/template_settings.txt +++ b/conf/template_settings.txt @@ -191,7 +191,7 @@ CRONJOBS = [ CRONTAB_COMMAND_SUFFIX = "2>&1" DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -DATA_UPLOAD_MAX_MEMORY_SIZE = 7000000 +DATA_UPLOAD_MAX_MEMORY_SIZE = 10000000 # Needed when using a proxy for https forwading SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') \ No newline at end of file diff --git a/container_install.sh b/container_install.sh new file mode 100644 index 000000000..cf9214239 --- /dev/null +++ b/container_install.sh @@ -0,0 +1,887 @@ +#!/usr/bin/bash + +ISKYLIMS_VERSION="3.1.0" + +usage() { +cat << EOF +This script installs and upgrades the iskylims app. + +Usage : $0 [--demo_data] [--git_revision] [--compose_file] [--install_conf] [--action] [--script] [--script_before] [--script_after] [--engine] [--test] + Optional input data: + --demo_data | Provide already downloaded demo data from Zenodo + --git_revision | Specify the Git revision to install (default: main, or 'current' to use copied local sources) + --compose_file | Compose file to use (overrides default) + --install_conf | Settings file consumed during container image build (mandatory for production) + --install_conf_map | Service-specific settings file: service,path (can be repeated) + --action | install (default) or upgrade, to control DB initialisation steps + --script | Run a Django migration script after migrations (can be repeated) + --script_before | Run a Django migration script before migrations (can be repeated) + --script_after | Run a Django migration script after migrations (can be repeated) + --skip_demo_data | Skip downloading/copying demo data to samba container + --skip_test_data | Skip loading test fixtures (test/test_data.json) + --engine | Container engine to use: docker (default) or podman + --test | Use development/test compose file and sample data + +Examples: + Deploy production container pointing to an external DB/Samba: + bash $0 --install_conf conf/my_prod_settings.txt + + Deploy production with service-specific settings mapping: + bash $0 --install_conf_map app,conf/docker_production_settings.txt + + Upgrade an existing production deployment using the same database: + bash $0 --install_conf conf/my_prod_settings.txt --action upgrade + + Install demo container system with local services + bash $0 --test + + Install test stack from current local committed sources without checking out a branch in-container + bash $0 --test --git_revision current + + Provide already downloaded data from Zenodo (compressed) for test environment + bash $0 --demo_data /path/to/iskylims_demo_data.tar.gz + +EOF +} + +# translate long options to short +reset=true + +for arg in "$@" +do + if [ -n "$reset" ]; then + unset reset + set -- # this resets the "$@" array so we can rebuild it + fi + case "$arg" in + # OPTIONAL + --demo_data) set -- "$@" -d ;; + --git_revision) set -- "$@" -g ;; + --compose_file) set -- "$@" -c ;; + --install_conf) set -- "$@" -s ;; + --install_conf_map) set -- "$@" -j ;; + --action) set -- "$@" -a ;; + --script) set -- "$@" -m ;; + --script_before) set -- "$@" -b ;; + --script_after) set -- "$@" -f ;; + --skip_demo_data) set -- "$@" -n ;; + --skip_test_data) set -- "$@" -t ;; + --test) set -- "$@" -p ;; + --engine) set -- "$@" -e ;; + + # ADDITIONAL + --help) set -- "$@" -h ;; + --version) set -- "$@" -v ;; + # PASSING VALUE IN PARAMETER + *) set -- "$@" "$arg" ;; + esac +done + +# SETTING DEFAULT VALUES +demo_data=false +git_revision="main" +compose_file="" +install_conf="" +install_conf_container="" +install_conf_map_entries=() +skip_demo_data="" +skip_test_data="" +mode="production" +action="install" +run_script=false +run_script_before=false +migration_script=() +migration_script_before=() +engine="docker" + +ENGINE_CMD=() +COMPOSE_CMD=() + +set_engine() { + if [ "$engine" = "docker" ]; then + if ! command -v docker >/dev/null 2>&1; then + echo "docker not found. Install docker or use --engine podman." + exit 1 + fi + ENGINE_CMD=("docker") + COMPOSE_CMD=("docker" "compose") + else + if ! command -v podman >/dev/null 2>&1; then + echo "podman not found. Install podman or use --engine docker." + exit 1 + fi + ENGINE_CMD=("podman") + if command -v podman-compose >/dev/null 2>&1; then + COMPOSE_CMD=("podman-compose") + elif podman compose version >/dev/null 2>&1; then + COMPOSE_CMD=("podman" "compose") + else + echo "podman compose not available. Install podman-compose or use --engine docker." + exit 1 + fi + fi +} + +engine_exec() { + "${ENGINE_CMD[@]}" "$@" +} + +compose_exec() { + "${COMPOSE_CMD[@]}" "$@" +} + +copy_with_podman_fallback() { + local src="$1" + local dst="$2" + + if cp "$src" "$dst" 2>/dev/null; then + return 0 + fi + + if [ "$engine" = "podman" ]; then + if podman unshare cp "$src" "$dst"; then + return 0 + fi + fi + + echo "Failed to copy '$src' to '$dst'" >&2 + return 1 +} + +normalize_apache_server_name() { + local value="$1" + + value="${value#http://}" + value="${value#https://}" + value="${value%%/*}" + value="${value%%:*}" + + if [ -z "$value" ] || [ "$value" = "*" ]; then + value="localhost" + fi + + echo "$value" +} + +generate_django_secret_key() { + if command -v python3 >/dev/null 2>&1; then + python3 -c "import secrets; print(''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for _ in range(50)))" + else + LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c 50 + printf "\n" + fi +} + +sed_replacement_escape() { + printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g' +} + +render_django_settings_file() { + local settings_path="$1" + local secret_line="" + local tmp_file="" + local db_user db_pass db_name db_host db_port + local email_host email_port email_user email_pass email_tls + local local_server_ip dns_url + + if [ -f "$settings_path" ]; then + secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$settings_path" | tail -n 1)" + fi + if [ -z "$secret_line" ] || [[ "$secret_line" =~ SECRET_KEY[[:space:]]*=[[:space:]]*SECRET ]]; then + secret_line="SECRET_KEY = '$(generate_django_secret_key)'" + fi + + db_user="$(read_install_conf_value DB_USER "$host_install_conf_path")" + db_pass="$(read_install_conf_value DB_PASS "$host_install_conf_path")" + db_name="$(read_install_conf_value DB_NAME "$host_install_conf_path")" + db_host="$(read_install_conf_value DB_SERVER_IP "$host_install_conf_path")" + db_port="$(read_install_conf_value DB_PORT "$host_install_conf_path")" + email_host="$(read_install_conf_value EMAIL_HOST_SERVER "$host_install_conf_path")" + email_port="$(read_install_conf_value EMAIL_PORT "$host_install_conf_path")" + email_user="$(read_install_conf_value EMAIL_HOST_USER "$host_install_conf_path")" + email_pass="$(read_install_conf_value EMAIL_HOST_PASSWORD "$host_install_conf_path")" + email_tls="$(read_install_conf_value EMAIL_USE_TLS "$host_install_conf_path")" + local_server_ip="$(read_install_conf_value LOCAL_SERVER_IP "$host_install_conf_path")" + dns_url="$(read_install_conf_value DNS_URL "$host_install_conf_path")" + + tmp_file="$(mktemp)" + cp "$repo_root/conf/template_settings.txt" "$tmp_file" + sed -i \ + -e "s|^SECRET_KEY.*|$(sed_replacement_escape "$secret_line")|" \ + -e "s|djangouser|$(sed_replacement_escape "$db_user")|g" \ + -e "s|djangopass|$(sed_replacement_escape "$db_pass")|g" \ + -e "s|djangohost|$(sed_replacement_escape "$db_host")|g" \ + -e "s|djangoport|$(sed_replacement_escape "$db_port")|g" \ + -e "s|djangodbname|$(sed_replacement_escape "$db_name")|g" \ + -e "s|emailhostserver|$(sed_replacement_escape "$email_host")|g" \ + -e "s|emailport|$(sed_replacement_escape "$email_port")|g" \ + -e "s|emailhostuser|$(sed_replacement_escape "$email_user")|g" \ + -e "s|emailhostpassword|$(sed_replacement_escape "$email_pass")|g" \ + -e "s|emailhosttls|$(sed_replacement_escape "$email_tls")|g" \ + -e "s|localserverip|$(sed_replacement_escape "$local_server_ip")|g" \ + -e "s|localhost|$(sed_replacement_escape "$dns_url")|g" \ + "$tmp_file" + + if copy_with_podman_fallback "$tmp_file" "$settings_path"; then + rm -f "$tmp_file" + return 0 + fi + + rm -f "$tmp_file" + return 1 +} + +normalize_settings_bind_path() { + local value="$1" + + if [ -z "$value" ]; then + echo "$app_install_path/iskylims/settings.py" + return 0 + fi + + if [ -d "$value" ] || [[ "$value" = */ ]]; then + echo "${value%/}/settings.py" + return 0 + fi + + echo "$value" +} + +render_apache_config() { + local src="$1" + local dst="$2" + local tmp_file="" + + tmp_file="$(mktemp)" + sed \ + -e "s|__ISKYLIMS_SERVER_NAME__|$apache_server_name|g" \ + -e "s|__ISKYLIMS_LOG_NAME__|$apache_log_name|g" \ + -e "s|__APP_INSTALL_PATH__|$app_install_path|g" \ + -e "s|__APP_PORT__|$app_port|g" \ + -e "s|__GUNICORN_TIMEOUT__|$gunicorn_timeout|g" \ + "$src" > "$tmp_file" + + if copy_with_podman_fallback "$tmp_file" "$dst"; then + rm -f "$tmp_file" + return 0 + fi + + rm -f "$tmp_file" + return 1 +} + +prepare_django_settings_bind_mount() { + local settings_path="$1" + + if [ "$mode" != "production" ]; then + return 0 + fi + + if [ -d "$settings_path" ]; then + echo "DJANGO_SETTINGS_PATH must resolve to a file path, but '$settings_path' is a directory." >&2 + echo "Use a full path like '$settings_path/settings.py' or remove the directory and rerun." >&2 + return 1 + fi + + mkdir -p "$(dirname "$settings_path")" + if [ ! -f "$settings_path" ] || grep -Eq "SECRET_KEY[[:space:]]*=[[:space:]]*SECRET|emailhosttls|djangouser|djangopass|djangohost|djangodbname" "$settings_path"; then + render_django_settings_file "$settings_path" + fi +} + +# PARSE VARIABLE ARGUMENTS WITH getopts +options=":d:g:c:s:j:a:m:b:f:e:vhntp" +while getopts $options opt; do + case $opt in + d) + demo_data=$OPTARG + ;; + g) + git_revision=$OPTARG + ;; + c) + compose_file=$OPTARG + ;; + s) + install_conf=$OPTARG + ;; + j) + install_conf_map_entries+=("$OPTARG") + ;; + a) + action=$OPTARG + if [[ "$action" != "install" && "$action" != "upgrade" ]]; then + echo "Invalid action '$action'. Use install or upgrade." + exit 1 + fi + ;; + m) + run_script=true + migration_script+=("$OPTARG") + ;; + b) + run_script_before=true + migration_script_before+=("$OPTARG") + ;; + e) + engine=$OPTARG + if [[ "$engine" != "docker" && "$engine" != "podman" ]]; then + echo "Invalid engine '$engine'. Use docker or podman." + exit 1 + fi + ;; + f) + run_script=true + migration_script+=("$OPTARG") + ;; + n) + skip_demo_data=true + ;; + t) + skip_test_data=true + ;; + p) + mode="test" + ;; + h) + usage + exit 1 + ;; + v) + echo $ISKYLIMS_VERSION + exit 1 + ;; + \?) + echo "Invalid Option: -$OPTARG" 1>&2 + usage + exit 1 + ;; + : ) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + * ) + echo "Unimplemented option: -$OPTARG" >&2; + exit 1 + ;; + esac +done +shift $((OPTIND-1)) + +if [ "$mode" = "test" ]; then + if [ -z "$compose_file" ]; then + compose_file="docker-compose.test.yml" + fi +else + if [ -z "$compose_file" ]; then + compose_file="docker-compose.prod.yml" + fi +fi + +app_service="${APP_SERVICE:-app}" +selected_install_conf="$install_conf" +for map_entry in "${install_conf_map_entries[@]}"; do + svc_name="${map_entry%%,*}" + conf_name="${map_entry#*,}" + if [ -z "$svc_name" ] || [ -z "$conf_name" ] || [ "$svc_name" = "$map_entry" ]; then + echo "Invalid --install_conf_map value '$map_entry'. Expected format: service,path" + exit 1 + fi + if [ "$svc_name" != "app" ]; then + echo "Unknown service '$svc_name' in --install_conf_map. Valid service: app" + exit 1 + fi + selected_install_conf="$conf_name" +done + +if [ "$mode" = "test" ] && [ -z "$selected_install_conf" ]; then + selected_install_conf="conf/docker_test_settings.txt" +fi +install_conf="$selected_install_conf" + +if [ "$mode" = "production" ] && [ -z "$install_conf" ]; then + echo "Production deployments require --install_conf or --install_conf_map app,." + exit 1 +fi + +if [ -z "$skip_demo_data" ]; then + if [ "$mode" = "test" ]; then + skip_demo_data=false + else + skip_demo_data=true + fi +fi + +if [ -z "$skip_test_data" ]; then + if [ "$mode" = "test" ]; then + skip_test_data=false + else + skip_test_data=true + fi +fi + +if [ "$action" = "upgrade" ]; then + skip_demo_data=true + skip_test_data=true +fi + +if [ ! -f "$compose_file" ]; then + echo "Compose file '$compose_file' not found" + exit 1 +fi + +if [ ! -f "$install_conf" ]; then + echo "Install configuration '$install_conf' not found" + exit 1 +fi + +repo_root="$(pwd)" +build_context_dir="$repo_root" +if [ ! -d "$build_context_dir" ]; then + echo "Build context directory '$build_context_dir' not found" + exit 1 +fi + +temp_install_conf="" +if [[ "$install_conf" = /* ]] && [[ "$install_conf" != "$build_context_dir/"* ]]; then + temp_install_conf="$build_context_dir/.tmp_docker_install_conf_app_$$.txt" + echo "Copying $install_conf into temporary file $temp_install_conf for Docker build/runtime." + cp "$install_conf" "$temp_install_conf" + install_conf="$temp_install_conf" + cleanup_temp_conf() { + if [ -n "$temp_install_conf" ] && [ -f "$temp_install_conf" ]; then + rm -f "$temp_install_conf" + fi + } + trap cleanup_temp_conf EXIT +fi + +if [[ "$install_conf" = "$build_context_dir/"* ]]; then + install_conf_container="${install_conf#$build_context_dir/}" +else + install_conf_container="$install_conf" +fi + +host_install_conf_path="$install_conf" +if [[ "$host_install_conf_path" != /* ]]; then + host_install_conf_path="$repo_root/$host_install_conf_path" +fi + +read_install_conf_value() { + local key="$1" + local file="$2" + + bash -c ' + set -a + . "$1" + key="$2" + printf "%s" "${!key-}" + ' _ "$file" "$key" +} + +config_value_or_default() { + local key="$1" + local default_value="$2" + local config_value="" + local env_value="${!key:-}" + + if [ -n "$env_value" ]; then + echo "$env_value" + return 0 + fi + + config_value="$(read_install_conf_value "$key" "$host_install_conf_path")" + if [ -n "$config_value" ]; then + echo "$config_value" + else + echo "$default_value" + fi +} + +write_compose_env_file() { + if [ "$mode" != "production" ]; then + return 0 + fi + + cat > "$compose_env_file" << EOF +# Generated by container_install.sh from $install_conf_container. +# Used by Docker Compose/Podman Compose for docker-compose.prod.yml interpolation. +INSTALL_TYPE=dep +GIT_REVISION=$git_revision +INSTALL_CONF=$install_conf_container +APP_INSTALL_PATH=$app_install_path +APACHE_CONF_PATH=$apache_conf_path +DJANGO_SETTINGS_PATH=$django_settings_path +APP_UID=$app_uid +APP_GID=$app_gid +APP_SHELL=$app_shell +APP_PORT=$app_port +DJANGO_DEBUG=$django_debug +DB_CONN_MAX_AGE=$db_conn_max_age +WEB_CONCURRENCY=$web_concurrency +GUNICORN_THREADS=$gunicorn_threads +GUNICORN_TIMEOUT=$gunicorn_timeout +GUNICORN_KEEPALIVE=$gunicorn_keepalive +EOF + + echo "Wrote Compose environment file: $compose_env_file" +} + +compose_with_env_exec() { + if [ "$mode" = "production" ] && [ -f "$compose_env_file" ]; then + compose_exec --env-file "$compose_env_file" "$@" + else + compose_exec "$@" + fi +} + +set_engine + +app_repo_path="${APP_REPO_PATH:-/srv/iskylims}" +config_install_path="$(read_install_conf_value "INSTALL_PATH" "$host_install_conf_path")" +config_app_install_path="$(read_install_conf_value "APP_INSTALL_PATH" "$host_install_conf_path")" +app_install_path="${APP_INSTALL_PATH:-${config_app_install_path:-${config_install_path:-/opt/iskylims}}}" +config_apache_conf_path="$(read_install_conf_value "APACHE_CONF_PATH" "$host_install_conf_path")" +apache_conf_path="${APACHE_CONF_PATH:-${config_apache_conf_path:-}}" +if [ -z "$apache_conf_path" ]; then + apache_conf_path="$app_install_path/conf" +fi +config_django_settings_path="$(read_install_conf_value "DJANGO_SETTINGS_PATH" "$host_install_conf_path")" +django_settings_path="$(normalize_settings_bind_path "${DJANGO_SETTINGS_PATH:-${config_django_settings_path:-}}")" +app_uid="$(config_value_or_default APP_UID 1212)" +app_gid="$(config_value_or_default APP_GID 1212)" +app_shell="$(config_value_or_default APP_SHELL /sbin/nologin)" +app_port="$(config_value_or_default APP_PORT 8001)" +django_debug="$(config_value_or_default DJANGO_DEBUG false)" +db_conn_max_age="$(config_value_or_default DB_CONN_MAX_AGE 60)" +web_concurrency="$(config_value_or_default WEB_CONCURRENCY 2)" +gunicorn_threads="$(config_value_or_default GUNICORN_THREADS 2)" +gunicorn_timeout="$(config_value_or_default GUNICORN_TIMEOUT 300)" +gunicorn_keepalive="$(config_value_or_default GUNICORN_KEEPALIVE 5)" +config_dns_url="$(read_install_conf_value "DNS_URL" "$host_install_conf_path")" +apache_server_name="$(normalize_apache_server_name "${APACHE_SERVER_NAME:-${config_dns_url:-localhost}}")" +apache_log_name="$(printf '%s' "$apache_server_name" | tr -c 'A-Za-z0-9._-' '_' | sed 's/_$//')" +compose_env_file="$repo_root/.env.prod.file" +app_container="" +local_head_hash="" +local_head_short="" +app_image_name="${APP_IMAGE_NAME:-iskylims_app}" +image_id_before_build="" +image_id_after_build="" + +# Check if a service exists in the compose file +# +# Parameters: +# $1 - Service name to check +# +# Returns: +# 0 if the service exists, 1 otherwise +service_exists() { + compose_with_env_exec -f "$compose_file" ps --services 2>/dev/null | grep -Fxq "$1" +} + +# Return the name of the container for a given service name. +# The container name is different based on whether we are in test mode or not. +# +# Parameters: +# $1 - Service name to return the container name for +# +# Returns: +# The name of the container for the given service name +service_container_name() { + local service_name="$1" + if [ "$mode" = "test" ]; then + case "$service_name" in + db) echo "db" ;; + samba) echo "samba" ;; + app) echo "iskylims_app" ;; + *) echo "" ;; + esac + else + case "$service_name" in + app) echo "iskylims_app" ;; + samba) echo "samba" ;; + *) echo "" ;; + esac + fi +} + +# Resolve the container ID for the target app service. +# +# Returns: +# Sets global variable `app_container` to a valid container name/ID. +# +# Errors: +# Exits if unable to resolve a container for `app_service`. +resolve_app_container() { + local container_name + container_name="$(service_container_name "$app_service")" + + if [ -n "$container_name" ] && engine_exec inspect -f '{{.Id}}' "$container_name" >/dev/null 2>&1; then + app_container="$container_name" + else + app_container="$(engine_exec ps -a --filter "label=com.docker.compose.service=${app_service}" --format '{{.ID}}' | head -n 1)" + fi + + if [ -z "$app_container" ]; then + echo "Error: unable to resolve container ID for service '$app_service'." >&2 + exit 1 + fi +} + +# Ensure target app service container exists and is running. +# +# Errors: +# Exits if container does not exist or is not running. +ensure_app_running() { + resolve_app_container + if ! engine_exec inspect -f '{{.State.Running}}' "$app_container" >/dev/null 2>&1; then + echo "Error: service '$app_service' container does not exist." + exit 1 + fi + if [ "$(engine_exec inspect -f '{{.State.Running}}' "$app_container")" != "true" ]; then + echo "Error: service '$app_service' container is not running. Showing logs:" + engine_exec logs --tail 200 "$app_container" + exit 1 + fi +} + +print_local_source_diagnostics() { + echo "Local source diagnostics:" + if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + local_head_hash="$(git -C "$repo_root" rev-parse HEAD)" + local_head_short="$(git -C "$repo_root" rev-parse --short HEAD)" + echo " local HEAD: $(git -C "$repo_root" log -1 --oneline)" + echo " local HEAD hash: $local_head_hash" + else + echo " local git metadata unavailable" + fi +} + +print_existing_artifact_diagnostics() { + echo "Image diagnostics before build:" + if engine_exec image inspect "$app_image_name" >/dev/null 2>&1; then + image_id_before_build="$(engine_exec image inspect -f '{{.Id}}' "$app_image_name" 2>/dev/null || true)" + echo " image before build: $image_id_before_build" + else + image_id_before_build="" + echo " image before build: none" + fi +} + +print_image_after_build() { + echo "Image diagnostics after build:" + if engine_exec image inspect "$app_image_name" >/dev/null 2>&1; then + image_id_after_build="$(engine_exec image inspect -f '{{.Id}}' "$app_image_name" 2>/dev/null || true)" + echo " image after build: $image_id_after_build" + if [ -n "$image_id_before_build" ] && [ "$image_id_before_build" = "$image_id_after_build" ]; then + echo " image id check: unchanged" + elif [ -n "$image_id_before_build" ] && [ "$image_id_before_build" != "$image_id_after_build" ]; then + echo " image id check: changed" + else + echo " image id check: created" + fi + else + echo " image after build: not found" + fi +} + +print_container_source_diagnostics() { + local label="$1" + local container_repo_head_hash="" + local container_repo_head_short="" + echo "$label" + container_repo_head_hash="$(engine_exec exec "$app_container" sh -lc " + if [ -d '$app_repo_path/.git' ]; then + cd '$app_repo_path' && git rev-parse HEAD + fi + " 2>/dev/null | tail -n 1)" + container_repo_head_short="$(engine_exec exec "$app_container" sh -lc " + if [ -d '$app_repo_path/.git' ]; then + cd '$app_repo_path' && git rev-parse --short HEAD + fi + " 2>/dev/null | tail -n 1)" + if [ -n "$container_repo_head_hash" ]; then + echo " container /srv HEAD hash: $container_repo_head_hash" + fi + if [ -n "$local_head_hash" ] && [ -n "$container_repo_head_hash" ]; then + if [ "$local_head_hash" = "$container_repo_head_hash" ]; then + echo " HEAD check: OK local=$local_head_short container=$container_repo_head_short" + else + echo " HEAD check: MISMATCH local=$local_head_short container=$container_repo_head_short" + fi + fi + engine_exec exec "$app_container" sh -lc " + echo ' /srv/iskylims HEAD:' + if [ -d '$app_repo_path/.git' ]; then + cd '$app_repo_path' && git log -1 --oneline + else + echo 'not a git checkout' + fi + " || true +} + +# Remove stale test containers left over from previous runs. +# +# This function will only be executed in "test" mode when the engine is "podman". +# It only removes known test container names and never removes volumes. +cleanup_stale_test_containers() { + if [ "$mode" != "test" ] || [ "$engine" != "podman" ]; then + return 0 + fi + + local svc cname cstate + for svc in db app samba; do + cname="$(service_container_name "$svc")" + if [ -z "$cname" ]; then + continue + fi + if engine_exec inspect -f '{{.Id}}' "$cname" >/dev/null 2>&1; then + cstate="$(engine_exec inspect -f '{{.State.Status}}' "$cname" 2>/dev/null || true)" + if [ "$cstate" != "running" ]; then + echo "Removing stale test container '$cname' (state: ${cstate:-unknown})" + engine_exec rm -f "$cname" >/dev/null 2>&1 || true + fi + fi + done +} + +cleanup_stale_test_containers + +print_local_source_diagnostics +print_existing_artifact_diagnostics +echo "Deploying containers (compose file: $compose_file) with a pre-staged app image and GIT_REVISION=$git_revision..." +mkdir -p "$app_install_path/conf" "$apache_conf_path" "/var/log/local/iskylims/apache" "/var/log/local/iskylims/apps" +prepare_django_settings_bind_mount "$django_settings_path" +if [ -f "$repo_root/conf/iskylims_apache_reverse_proxy.conf" ]; then + render_apache_config \ + "$repo_root/conf/iskylims_apache_reverse_proxy.conf" \ + "$apache_conf_path/iskylims_apache_reverse_proxy.conf" +fi +if [ -f "$repo_root/conf/iskylims_apache_logs.conf" ]; then + render_apache_config \ + "$repo_root/conf/iskylims_apache_logs.conf" \ + "$apache_conf_path/iskylims_apache_logs.conf" +fi +write_compose_env_file +INSTALL_TYPE="dep" GIT_REVISION="$git_revision" INSTALL_CONF="$install_conf_container" APP_INSTALL_PATH="$app_install_path" APACHE_CONF_PATH="$apache_conf_path" DJANGO_SETTINGS_PATH="$django_settings_path" APP_UID="$app_uid" APP_GID="$app_gid" APP_SHELL="$app_shell" APP_PORT="$app_port" DJANGO_DEBUG="$django_debug" DB_CONN_MAX_AGE="$db_conn_max_age" WEB_CONCURRENCY="$web_concurrency" GUNICORN_THREADS="$gunicorn_threads" GUNICORN_TIMEOUT="$gunicorn_timeout" GUNICORN_KEEPALIVE="$gunicorn_keepalive" \ + compose_with_env_exec -f "$compose_file" build --no-cache \ + --build-arg INSTALL_TYPE="dep" \ + --build-arg GIT_REVISION="$git_revision" \ + --build-arg INSTALL_CONF="$install_conf_container" \ + --build-arg APP_INSTALL_PATH="$app_install_path" \ + --build-arg APP_UID="$app_uid" \ + --build-arg APP_GID="$app_gid" \ + --build-arg APP_SHELL="$app_shell" +print_image_after_build +APP_INSTALL_PATH="$app_install_path" APACHE_CONF_PATH="$apache_conf_path" DJANGO_SETTINGS_PATH="$django_settings_path" APP_UID="$app_uid" APP_GID="$app_gid" APP_SHELL="$app_shell" APP_PORT="$app_port" DJANGO_DEBUG="$django_debug" DB_CONN_MAX_AGE="$db_conn_max_age" WEB_CONCURRENCY="$web_concurrency" GUNICORN_THREADS="$gunicorn_threads" GUNICORN_TIMEOUT="$gunicorn_timeout" GUNICORN_KEEPALIVE="$gunicorn_keepalive" compose_with_env_exec -f "$compose_file" up -d + +echo "Waiting 20 seconds for starting database and web services..." +sleep 20 +ensure_app_running +print_container_source_diagnostics "Container diagnostics after startup:" + +container_install_conf_path="$install_conf_container" +if [[ "$container_install_conf_path" != /* ]]; then + container_install_conf_path="$app_repo_path/$container_install_conf_path" +fi + +if ! engine_exec exec -it "$app_container" test -f "$container_install_conf_path"; then + echo "Copying install configuration into container at $container_install_conf_path" + engine_exec cp "$host_install_conf_path" "${app_container}:$container_install_conf_path" +fi + +script_args_before="" +if [ "$run_script_before" = true ]; then + for val in "${migration_script_before[@]}"; do + script_args_before+=" --script_before $(printf '%q' "$val")" + done +fi + +script_args_after="" +if [ "$run_script" = true ]; then + for val in "${migration_script[@]}"; do + script_args_after+=" --script_after $(printf '%q' "$val")" + done +fi + +if [ "$action" = "upgrade" ]; then + echo "Running install.sh bootstrap inside the container (upgrade mode)" + engine_exec exec -it "$app_container" bash -c "cd $app_repo_path && bash install.sh --bootstrap upgrade --git_revision \"$git_revision\" --conf \"$install_conf_container\" --tables --skip_apache_restart$script_args_before$script_args_after" +else + echo "Running install.sh bootstrap inside the container (install mode)" + engine_exec exec -it "$app_container" bash -c "cd $app_repo_path && bash install.sh --bootstrap install --git_revision \"$git_revision\" --conf \"$install_conf_container\" --skip_apache_restart$script_args_before$script_args_after" +fi + +print_container_source_diagnostics "Container diagnostics after bootstrap:" + +if ! engine_exec exec -it "$app_container" test -f "$app_install_path/manage.py"; then + echo "Error: $app_install_path/manage.py not found after bootstrap. Showing logs:" + engine_exec logs --tail 200 "$app_container" + exit 1 +fi + +if [ "$skip_test_data" = false ]; then + engine_exec exec -it "$app_container" python3 manage.py loaddata test/test_data.json + engine_exec exec -it "$app_container" python3 manage.py shell -c " +from django.contrib.auth.models import Group, User +admin = User.objects.get(username='admin') +admin.groups.add( + Group.objects.get(name='WetlabManager'), + Group.objects.get(name='ServiceManager'), +) +print('admin groups:', list(admin.groups.values_list('name', flat=True))) +" +else + echo "Skipping test data fixtures as requested" +fi + +if [ "$skip_demo_data" = false ] && service_exists "samba"; then + echo "Downloading and copying test files to the Samba container" + if [ "$demo_data" == "false" ]; then + wget https://zenodo.org/record/8091169/files/iskylims_demo_data.tar.gz + demo_data="./iskylims_demo_data.tar.gz" + fi + engine_exec cp "$demo_data" samba:/mnt + engine_exec exec -it samba tar -xf /mnt/iskylims_demo_data.tar.gz -C /mnt + # Ensure extracted demo data can be traversed/read through SMB by non-owner users. + engine_exec exec -it samba sh -lc ' + for root in /mnt/test_ngs_data /mnt/Runs; do + if [ -d "$root" ]; then + find "$root" -type d -exec chmod o+rx {} + + find "$root" -type f -exec chmod o+r {} + + fi + done + ' + + echo "Deleting compressed test file" + engine_exec exec -it samba rm /mnt/iskylims_demo_data.tar.gz + + if [ "$demo_data" == "false" ]; then + rm -f "$demo_data" + fi +else + echo "Skipping Samba demo data load (flag enabled or service not present)" +fi + +echo "Skipping crontab add/start (cron is managed by the container entrypoint)" + +dns_url="" +local_ip="" +if [ -f "$host_install_conf_path" ]; then + dns_url=$(grep -E "^DNS_URL=" "$host_install_conf_path" | tail -n 1 | cut -d= -f2- | sed "s/^['\"]//;s/['\"]$//") + local_ip=$(grep -E "^LOCAL_SERVER_IP=" "$host_install_conf_path" | tail -n 1 | cut -d= -f2- | sed "s/^['\"]//;s/['\"]$//") +fi + +access_urls=() +if [ -n "$dns_url" ] && [ "$dns_url" != "*" ]; then + access_urls+=("http://${dns_url}:${app_port}") +fi +if [ -n "$local_ip" ] && [ "$local_ip" != "*" ]; then + access_urls+=("http://${local_ip}:${app_port}") +fi +if [ ${#access_urls[@]} -eq 0 ]; then + access_urls+=("http://localhost:${app_port}") +fi + +echo "You can now access iSkyLIMS via: ${access_urls[*]}" diff --git a/core/.gitignore b/core/.gitignore deleted file mode 100644 index f5e99c72e..000000000 --- a/core/.gitignore +++ /dev/null @@ -1,103 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class -migrations/ -tests/ - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/core/admin.py b/core/admin.py index c12a2e021..76788e821 100644 --- a/core/admin.py +++ b/core/admin.py @@ -77,11 +77,11 @@ class MoleculePreparationAdmin(admin.ModelAdmin): "extraction_type", "protocol_used", "molecule_extraction_date", - "molecule_used_for", + "sample_continues_on", "reused_number", ) list_filter = ("generated_at",) - search_fields = ("sample__startswith",) + search_fields = ("sample__sample_name__startswith",) class MoleculeUsedForAdmin(admin.ModelAdmin): diff --git a/core/core_config.py b/core/core_config.py index 5a9bc8a55..a0e34e326 100644 --- a/core/core_config.py +++ b/core/core_config.py @@ -22,17 +22,17 @@ ] HEADING_FOR_MOLECULE_ADDING_PARAMETERS = [ "Sample", - "Molecule Code ID", + "Extraction Code ID", "Lot Commercial Kit", ] # ########### Headings to confirm the sucessful recorded -HEADING_CONFIRM_MOLECULE_RECORDED = ["Molecule Code ID", "Used Protocol"] +HEADING_CONFIRM_MOLECULE_RECORDED = ["Extraction Code ID", "Used Protocol"] -# ## Heading values when showing pending samples at handling molecules +# ## Heading values when showing pending samples at manage molecules HEADING_FOR_DEFINED_SAMPLES = [ - "Sample extraction date", + "Sample defined date", "Sample Code ID", "Sample", "To be included", @@ -41,7 +41,7 @@ # ## Heading values when showing pending samples HEADING_FOR_PENDING_MOLECULES = [ "Sample", - "Molecule Code ID", + "Extraction Code ID", "Used Protocol", "Molecule Extraction Date", "Select Molecule", @@ -62,7 +62,7 @@ ] # ## Heading for display information on molecule definition HEADING_FOR_MOLECULE_DEFINITION = [ - "Molecule CodeID", + "Extraction Code ID", "Molecule State", "Extraction Date", "Extraction Type", @@ -74,8 +74,8 @@ HEADING_FOR_SELECTING_MOLECULE_USE = [ "Sample Name", - "Molecule CodeID", - "Molecule use for", + "Extraction Code ID", + "Sample continues on", ] # ################ PROTOCOL PARAMETER SETTINGS ############################## @@ -84,6 +84,7 @@ "Parameter name", "Order", "Used", + "Downloadable", "Parameter Type", "Option Values", "Min Value", @@ -95,8 +96,11 @@ "New field name", "Order", "Used", + "Downloadable", "Parameter Type", "Option Values", + "Min Value", + "Max Value", "Description", ] @@ -104,7 +108,7 @@ "Field name", "Order", "Used", - "Searchable", + "Downloadable", "Field type", "Option Values", "Description", @@ -116,7 +120,7 @@ "Change field name", "Order", "Used", - "Searchable", + "Downloadable", "Field type", "Option Values", "Description", @@ -188,7 +192,7 @@ ] HEADING_FOR_DISPLAY_IN_SAMPLE_INFO_USER_KIT_DATA = [ - "Molecule Code ID", + "Extraction Code ID", "Lot number", "Commercial kit name", "Expiration Date", @@ -237,6 +241,8 @@ ERROR_SAMPLE_ALREADY_DEFINED = ["Sample", "already exist in the database"] +ERROR_NO_USED_FIELD_ARE_ARE_SET = ["No field is set as used"] + # ###################### Batch file ############################################### ERROR_EMPTY = [ "The uploaded table or batch file does not have any sample. Upload a valid batch file" @@ -328,3 +334,23 @@ ] ERROR_PROJECT_FIELD_NOOPTION = ["Project field", "only has the following options:"] ERROR_PROJECT_FIELD_EMPTY = ["Project field", "is empty"] + +LAB_REQUEST_ONTOLOGY_MAP = { + "GENEPIO:0001153": ("lab_name", "collecting_institution"), + "SNOMED:423901009": ("lab_code_1", "collecting_institution_code_1"), + "NCIT:C101703": ("lab_code_2", "collecting_institution_code_2"), + "OBI:0001890": ("lab_email", "collecting_institution_email"), + "NCIT:C40978": ("lab_phone", "collecting_institution_phone"), + "GENEPIO:0001158": ("address", "collecting_institution_address"), + "GENEPIO:0001803": ("autonom_cod", "autonom_cod"), + "GENEPIO:0001185": ("geo_loc_state", "geo_loc_state"), + "GENEPIO:0001189": ("geo_loc_city", "geo_loc_city"), + "NCIT:C25621": ("post_code", "post_code"), + "mesh:D009935": ("dep_func", "dep_func"), + "NCIT:C93878": ("center_class_code", "center_class_code"), + "NCIT:C188820": ("lab_function", "collecting_institution_function"), + "EFO:0005020": ("lab_geo_loc_latitude", "lab_geo_loc_latitude"), + "EFO:0005021": ("lab_geo_loc_longitude", "lab_geo_loc_longitude"), + "OBI:0001620": ("geo_loc_latitude", "geo_loc_latitude"), + "OBI:0001621": ("geo_loc_longitude", "geo_loc_longitude"), +} diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 000000000..6deb3f11e --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,1149 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="City", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("city_name", models.CharField(max_length=80)), + ("geo_loc_latitude", models.CharField(max_length=80)), + ("geo_loc_longitude", models.CharField(max_length=80)), + ("apps_name", models.CharField(max_length=40, null=True)), + ], + options={ + "db_table": "core_city", + }, + ), + migrations.CreateModel( + name="CommercialKits", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=150)), + ("provider", models.CharField(max_length=30)), + ("cat_number", models.CharField(blank=True, max_length=40, null=True)), + ( + "description", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ], + options={ + "db_table": "core_commercial_kits", + }, + ), + migrations.CreateModel( + name="Contact", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("contact_name", models.CharField(max_length=80)), + ("contact_mail", models.CharField(max_length=40, null=True)), + ], + options={ + "db_table": "core_contact", + }, + ), + migrations.CreateModel( + name="LabRequest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("lab_name", models.CharField(max_length=80)), + ("lab_name_coding", models.CharField(max_length=50)), + ("lab_unit", models.CharField(max_length=50)), + ("lab_contact_name", models.CharField(max_length=50)), + ("lab_phone", models.CharField(max_length=20)), + ("lab_email", models.CharField(max_length=70)), + ("address", models.CharField(max_length=255)), + ("apps_name", models.CharField(max_length=40, null=True)), + ( + "lab_city", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.city", + ), + ), + ], + options={ + "db_table": "core_lab_request", + }, + ), + migrations.CreateModel( + name="MoleculeType", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("molecule_type", models.CharField(max_length=30)), + ("apps_name", models.CharField(max_length=40, null=True)), + ], + options={ + "db_table": "core_molecule_type", + }, + ), + migrations.CreateModel( + name="MoleculeUsedFor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("used_for", models.CharField(max_length=50)), + ("apps_name", models.CharField(max_length=50)), + ("massive_use", models.BooleanField(default=False)), + ], + options={ + "db_table": "core_molecule_used_for", + }, + ), + migrations.CreateModel( + name="OntologyMap", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("label", models.CharField(max_length=255)), + ("ontology", models.CharField(blank=True, max_length=50, null=True)), + ], + options={ + "db_table": "core_ontology_map", + }, + ), + migrations.CreateModel( + name="PatientCore", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("patient_name", models.CharField(max_length=255, null=True)), + ("patient_surname", models.CharField(max_length=255, null=True)), + ("patient_code", models.CharField(max_length=255, null=True)), + ], + options={ + "db_table": "core_patient_core", + }, + ), + migrations.CreateModel( + name="PatientProjects", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("project_name", models.CharField(max_length=50)), + ( + "project_manager", + models.CharField(blank=True, max_length=50, null=True), + ), + ( + "project_contact", + models.CharField(blank=True, max_length=50, null=True), + ), + ( + "project_description", + models.CharField(blank=True, max_length=255, null=True), + ), + ("apps_name", models.CharField(max_length=40)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "core_patient_projects", + }, + ), + migrations.CreateModel( + name="PatientSex", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sex", models.CharField(max_length=16)), + ], + options={ + "db_table": "core_patient_sex", + }, + ), + migrations.CreateModel( + name="SampleProjectFieldClassification", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("classification_name", models.CharField(max_length=80)), + ("classification_display", models.CharField(max_length=100)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "core_sample_projects_field_classification", + }, + ), + migrations.CreateModel( + name="SampleProjects", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sample_project_name", models.CharField(max_length=255)), + ( + "sample_project_manager", + models.CharField(blank=True, max_length=50, null=True), + ), + ( + "sample_project_contact", + models.CharField(blank=True, max_length=250, null=True), + ), + ( + "sample_project_description", + models.CharField(blank=True, max_length=255, null=True), + ), + ("apps_name", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "core_sample_projects", + }, + ), + migrations.CreateModel( + name="SampleProjectsFields", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sample_project_field_name", models.CharField(max_length=80)), + ( + "sample_project_field_description", + models.CharField(blank=True, max_length=400, null=True), + ), + ("sample_project_field_order", models.IntegerField()), + ("sample_project_field_used", models.BooleanField()), + ("sample_project_field_type", models.CharField(max_length=20)), + ( + "sample_project_option_list", + models.CharField(blank=True, max_length=255, null=True), + ), + ("sample_project_searchable", models.BooleanField(default=False)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "sample_project_field_classification_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sampleprojectfieldclassification", + ), + ), + ( + "sample_projects_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="sample_project_fields", + to="core.sampleprojects", + ), + ), + ], + options={ + "db_table": "core_sample_projects_fields", + }, + ), + migrations.CreateModel( + name="SampleType", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sample_type", models.CharField(max_length=50)), + ("apps_name", models.CharField(max_length=50)), + ( + "mandatory_fields", + models.CharField(blank=True, max_length=300, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ], + options={ + "db_table": "core_sample_type", + }, + ), + migrations.CreateModel( + name="SequencingPlatform", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("platform_name", models.CharField(max_length=30)), + ("company_name", models.CharField(max_length=30)), + ("sequencing_technology", models.CharField(max_length=30)), + ], + options={ + "db_table": "core_sequencing_platform", + }, + ), + migrations.CreateModel( + name="Species", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("species_name", models.CharField(max_length=50)), + ( + "ref_genome_name", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "ref_genome_size", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "ref_genome_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("apps_name", models.CharField(max_length=50, null=True)), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ], + options={ + "db_table": "core_species", + }, + ), + migrations.CreateModel( + name="StateInCountry", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_name", models.CharField(max_length=80)), + ("apps_name", models.CharField(max_length=40, null=True)), + ], + options={ + "db_table": "core_state_in_country", + }, + ), + migrations.CreateModel( + name="StatesForMolecule", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("molecule_state_name", models.CharField(max_length=50)), + ], + options={ + "db_table": "core_states_for_molecule", + }, + ), + migrations.CreateModel( + name="StatesForSample", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sample_state_name", models.CharField(max_length=50)), + ], + options={ + "db_table": "core_states_for_sample", + }, + ), + migrations.CreateModel( + name="UserLotCommercialKits", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uses_number", models.IntegerField(default=0, null=True)), + ("chip_lot", models.CharField(max_length=50)), + ("latest_used_date", models.DateTimeField(blank=True, null=True)), + ("expiration_date", models.DateField()), + ("run_out", models.BooleanField(default=False)), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ( + "based_commercial", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.commercialkits", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "core_user_lot_commercial_kits", + }, + ), + migrations.CreateModel( + name="SequencingConfiguration", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("configuration_name", models.CharField(max_length=255)), + ( + "platform_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencingplatform", + ), + ), + ], + options={ + "db_table": "core_sequencing_configuration", + }, + ), + migrations.CreateModel( + name="SequencerInLab", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sequencer_name", models.CharField(max_length=255)), + ( + "sequencer_description", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "sequencer_location", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "sequencer_serial_number", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "sequencer_state", + models.CharField(blank=True, max_length=50, null=True), + ), + ("sequencer_operation_start", models.DateField(blank=True, null=True)), + ("sequencer_operation_end", models.DateField(blank=True, null=True)), + ( + "sequencer_number_lanes", + models.CharField(blank=True, max_length=5, null=True), + ), + ( + "platform_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencingplatform", + ), + ), + ], + options={ + "db_table": "core_sequencer_in_lab", + }, + ), + migrations.CreateModel( + name="SamplesProjectsTableOptions", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("option_value", models.CharField(max_length=120)), + ( + "sample_project_field", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="opt_value_prop", + to="core.sampleprojectsfields", + ), + ), + ], + options={ + "db_table": "core_sample_projects_table_options", + }, + ), + migrations.CreateModel( + name="Samples", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sample_name", + models.CharField( + max_length=255, null=True, verbose_name="Sample Name" + ), + ), + ( + "sample_location", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Sample location", + ), + ), + ( + "sample_entry_date", + models.DateTimeField( + blank=True, null=True, verbose_name="Sample defined date" + ), + ), + ( + "collection_sample_date", + models.DateTimeField( + blank=True, null=True, verbose_name="Sample collection date" + ), + ), + ( + "unique_sample_id", + models.CharField( + max_length=8, null=True, verbose_name="Unique sample id" + ), + ), + ( + "sample_code_id", + models.CharField( + max_length=60, null=True, verbose_name="Sample code id" + ), + ), + ( + "reused_number", + models.IntegerField( + default=0, verbose_name="Number of type reused" + ), + ), + ( + "sequencing_date", + models.DateTimeField( + blank=True, null=True, verbose_name="Sequencing date" + ), + ), + ( + "completed_date", + models.DateTimeField( + blank=True, null=True, verbose_name="Completion date" + ), + ), + ( + "generated_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Generated at" + ), + ), + ( + "only_recorded", + models.BooleanField( + blank=True, + default=False, + null=True, + verbose_name="Only recorded?", + ), + ), + ( + "lab_request", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.labrequest", + verbose_name="Laboratory", + ), + ), + ( + "patient_core", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientcore", + verbose_name="Patient Code ID", + ), + ), + ( + "sample_project", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sampleprojects", + verbose_name="Sample Project", + ), + ), + ( + "sample_state", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.statesforsample", + verbose_name="Sample state", + ), + ), + ( + "sample_type", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sampletype", + verbose_name="Sample type", + ), + ), + ( + "sample_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="Username", + ), + ), + ( + "species", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.species", + verbose_name="Species", + ), + ), + ], + options={ + "db_table": "core_samples", + }, + ), + migrations.CreateModel( + name="SampleProjectsFieldsValue", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sample_project_field_value", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "sample_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_values", + to="core.samples", + verbose_name="Sample Name", + ), + ), + ( + "sample_project_field_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sampleprojectsfields", + ), + ), + ], + options={ + "db_table": "core_sample_projects_fields_value", + }, + ), + migrations.AddField( + model_name="sampleprojectfieldclassification", + name="sample_projects_id", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sampleprojects", + ), + ), + migrations.CreateModel( + name="ProtocolType", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("protocol_type", models.CharField(max_length=40)), + ("apps_name", models.CharField(max_length=40)), + ( + "molecule", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.moleculetype", + ), + ), + ], + options={ + "db_table": "core_protocol_type", + }, + ), + migrations.CreateModel( + name="Protocols", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=40)), + ( + "description", + models.CharField(blank=True, max_length=160, null=True), + ), + ( + "type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.protocoltype", + ), + ), + ], + options={ + "db_table": "core_protocols", + }, + ), + migrations.CreateModel( + name="ProtocolParameters", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("parameter_name", models.CharField(max_length=255)), + ( + "parameter_description", + models.CharField(blank=True, max_length=400, null=True), + ), + ("parameter_order", models.IntegerField()), + ("parameter_used", models.BooleanField()), + ("parameter_type", models.CharField(default="string", max_length=20)), + ( + "parameter_option_values", + models.CharField(blank=True, max_length=400, null=True), + ), + ( + "parameter_max_value", + models.CharField(blank=True, max_length=50, null=True), + ), + ( + "parameter_min_value", + models.CharField(blank=True, max_length=50, null=True), + ), + ( + "protocol_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.protocols" + ), + ), + ], + options={ + "db_table": "core_protocol_parameters", + }, + ), + migrations.CreateModel( + name="PatientProjectsFields", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("project_field_name", models.CharField(max_length=50)), + ( + "project_field_description", + models.CharField(blank=True, max_length=400, null=True), + ), + ("project_field_order", models.IntegerField()), + ("project_field_used", models.BooleanField()), + ( + "patient_projects_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientprojects", + ), + ), + ], + options={ + "db_table": "core_patient_projects_fields", + }, + ), + migrations.CreateModel( + name="PatientProjectFieldValue", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("project_field_value", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "patient_core_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientcore", + ), + ), + ( + "project_field_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientprojectsfields", + ), + ), + ], + options={ + "db_table": "core_patient_project_field_value", + }, + ), + migrations.AddField( + model_name="patientcore", + name="patient_projects", + field=models.ManyToManyField(blank=True, to="core.patientprojects"), + ), + migrations.AddField( + model_name="patientcore", + name="patient_sex", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.patientsex", + ), + ), + migrations.CreateModel( + name="MoleculePreparation", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("molecule_code_id", models.CharField(max_length=255)), + ("extraction_type", models.CharField(max_length=50)), + ("molecule_extraction_date", models.DateTimeField(null=True)), + ("reused_number", models.IntegerField(default=0)), + ( + "used_for_massive_sequencing", + models.BooleanField(blank=True, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "molecule_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.moleculetype", + ), + ), + ( + "molecule_used_for", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.moleculeusedfor", + ), + ), + ( + "molecule_user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "protocol_used", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.protocols" + ), + ), + ( + "sample", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.samples" + ), + ), + ( + "state", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.statesformolecule", + ), + ), + ( + "user_lot_kit_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.userlotcommercialkits", + ), + ), + ], + options={ + "db_table": "core_molecule_preparation", + }, + ), + migrations.CreateModel( + name="MoleculeParameterValue", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("parameter_value", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "molecule_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.moleculepreparation", + ), + ), + ( + "molecule_parameter_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.protocolparameters", + ), + ), + ], + options={ + "db_table": "core_molecule_parameter_value", + }, + ), + migrations.AddField( + model_name="commercialkits", + name="platform_kits", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencingplatform", + ), + ), + migrations.AddField( + model_name="commercialkits", + name="protocol_kits", + field=models.ManyToManyField(blank=True, to="core.protocols"), + ), + migrations.AddField( + model_name="city", + name="belongs_to_state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.stateincountry", + ), + ), + ] diff --git a/core/migrations/0002_rename_molecule_used_for_moleculepreparation_sample_continues_on_and_more.py b/core/migrations/0002_rename_molecule_used_for_moleculepreparation_sample_continues_on_and_more.py new file mode 100644 index 000000000..9d3be354a --- /dev/null +++ b/core/migrations/0002_rename_molecule_used_for_moleculepreparation_sample_continues_on_and_more.py @@ -0,0 +1,102 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="moleculepreparation", + old_name="molecule_used_for", + new_name="sample_continues_on", + ), + migrations.RenameField( + model_name="sampleprojectsfields", + old_name="sample_project_searchable", + new_name="sample_project_downloadable", + ), + migrations.AddField( + model_name="city", + name="geo_loc_city_cod", + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="autonom_cod", + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="center_class_code", + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="dep_func", + field=models.CharField(blank=True, max_length=80, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="lab_code_1", + field=models.CharField(blank=True, max_length=20, null=True, unique=True), + ), + migrations.AddField( + model_name="labrequest", + name="lab_code_2", + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="lab_function", + field=models.CharField(blank=True, max_length=120, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="lab_geo_loc_latitude", + field=models.CharField(blank=True, max_length=30, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="lab_geo_loc_longitude", + field=models.CharField(blank=True, max_length=30, null=True), + ), + migrations.AddField( + model_name="labrequest", + name="post_code", + field=models.CharField(blank=True, max_length=10, null=True), + ), + migrations.AddField( + model_name="protocolparameters", + name="parameter_download", + field=models.BooleanField(blank=True, default=False, null=True), + ), + migrations.AddField( + model_name="stateincountry", + name="geo_loc_state_cod", + field=models.CharField( + blank=True, + help_text="Geographic code of the CCAA", + max_length=10, + null=True, + ), + ), + migrations.AddField( + model_name="statesformolecule", + name="molecule_state_display", + field=models.CharField(blank=True, max_length=80, null=True), + ), + migrations.AlterField( + model_name="labrequest", + name="lab_name", + field=models.CharField(max_length=100), + ), + migrations.AlterModelTable( + name="moleculeusedfor", + table="core_sample_continues_on", + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/core/models.py b/core/models.py index 407c451b0..8de191762 100644 --- a/core/models.py +++ b/core/models.py @@ -10,9 +10,18 @@ def create_new_state(self, data): new_state = self.create(state_name=data["state"], apps_name=data["apps_name"]) return new_state + def get_by_geo_code(self, code): + """ + StateInCountry.objects.get_by_geo_code("ES-AN") + """ + return self.get(geo_loc_state_cod=code) + class StateInCountry(models.Model): state_name = models.CharField(max_length=80) + geo_loc_state_cod = models.CharField( + max_length=10, null=True, blank=True, help_text="Geographic code of the CCAA" + ) apps_name = models.CharField(max_length=40, null=True) class Meta: @@ -24,6 +33,9 @@ def __str__(self): def get_state_name(self): return "%s" % (self.state_name) + def get_geo_loc_state_cod(self): + return "%s" % (self.geo_loc_state_cod) + def get_state_id(self): return "%s" % (self.pk) @@ -55,7 +67,8 @@ def create_new_city(self, data): state_obj = None new_city = self.create( belongs_to_state=state_obj, - city_name=data["cityName"], + city_name=data["city_name"], + geo_loc_city_cod=data.get("geo_loc_city_cod"), geo_loc_latitude=data["latitude"], geo_loc_longitude=data["longitude"], apps_name=data["apps_name"], @@ -68,6 +81,7 @@ class City(models.Model): StateInCountry, on_delete=models.CASCADE, null=True, blank=True ) city_name = models.CharField(max_length=80) + geo_loc_city_cod = models.CharField(max_length=10, null=True, blank=True) geo_loc_latitude = models.CharField(max_length=80) geo_loc_longitude = models.CharField(max_length=80) apps_name = models.CharField(max_length=40, null=True) @@ -84,6 +98,9 @@ def get_city_name(self): def get_city_id(self): return "%s" % (self.pk) + def get_geo_loc_city_cod(self): + return self.geo_loc_city_cod or "" + def get_coordenates(self): return {"latitude": self.geo_loc_latitude, "longitude": self.geo_loc_longitude} @@ -99,12 +116,21 @@ class LabRequestManager(models.Manager): def create_lab_request(self, data): city_obj = City.objects.filter(pk__exact=data["city"]).last() new_lab_request = self.create( - lab_name=data["labName"], - lab_name_coding=data["labNameCoding"], - lab_unit=data["labUnit"], - lab_contact_name=data["labContactName"], - lab_phone=data["labPhone"], - lab_email=data["labEmail"], + lab_name=data["lab_name"], + lab_name_coding=data["lab_name_coding"], + lab_code_1=data.get("lab_code_1"), + lab_code_2=data.get("lab_code_2"), + lab_geo_loc_latitude=data.get("lab_geo_loc_latitude"), + lab_geo_loc_longitude=data.get("lab_geo_loc_longitude"), + lab_unit=data["lab_unit"], + autonom_cod=data.get("autonom_cod"), + post_code=data.get("post_code"), + dep_func=data.get("dep_func"), + center_class_code=data.get("center_class_code"), + lab_function=data.get("lab_function"), + lab_contact_name=data["lab_contact_name"], + lab_phone=data["lab_phone"], + lab_email=data["lab_email"], address=data["address"], apps_name=data["apps_name"], lab_city=city_obj, @@ -114,13 +140,28 @@ def create_lab_request(self, data): class LabRequest(models.Model): lab_city = models.ForeignKey(City, on_delete=models.CASCADE, null=True, blank=True) - lab_name = models.CharField(max_length=80) + lab_name = models.CharField(max_length=100) lab_name_coding = models.CharField(max_length=50) lab_unit = models.CharField(max_length=50) lab_contact_name = models.CharField(max_length=50) lab_phone = models.CharField(max_length=20) lab_email = models.CharField(max_length=70) address = models.CharField(max_length=255) + lab_code_1 = models.CharField( + max_length=20, + null=True, + blank=True, + unique=True, + ) + lab_code_2 = models.CharField(max_length=20, null=True, blank=True) + autonom_cod = models.CharField(max_length=20, null=True, blank=True) + post_code = models.CharField(max_length=10, null=True, blank=True) + dep_func = models.CharField(max_length=80, null=True, blank=True) + center_class_code = models.CharField(max_length=20, null=True, blank=True) + lab_function = models.CharField(max_length=120, null=True, blank=True) + lab_geo_loc_latitude = models.CharField(max_length=30, null=True, blank=True) + lab_geo_loc_longitude = models.CharField(max_length=30, null=True, blank=True) + apps_name = models.CharField(max_length=40, null=True) class Meta: @@ -138,6 +179,12 @@ def get_id(self): def get_lab_request_code(self): return "%s" % (self.lab_name_coding) + def get_lab_code_1(self): + return self.lab_code_1 or "" + + def get_lab_code_2(self): + return self.lab_code_2 or "" + def get_all_data(self): data = [] data.append(self.lab_name) @@ -147,6 +194,8 @@ def get_all_data(self): data.append(self.lab_phone) data.append(self.lab_email) data.append(self.address) + data.append(self.lab_code_1) + data.append(self.lab_code_2) return data def get_fields_and_data(self): @@ -278,6 +327,7 @@ def create_protocol_parameter(self, prot_param_data): parameter_min_value=prot_param_data["Min Value"], parameter_option_values=prot_param_data["Option Values"], parameter_type=prot_param_data["Parameter Type"], + parameter_download=prot_param_data["Downloadable"], ) return new_prot_parameter @@ -292,6 +342,7 @@ class ProtocolParameters(models.Model): parameter_option_values = models.CharField(max_length=400, null=True, blank=True) parameter_max_value = models.CharField(max_length=50, null=True, blank=True) parameter_min_value = models.CharField(max_length=50, null=True, blank=True) + parameter_download = models.BooleanField(default=False, null=True, blank=True) class Meta: db_table = "core_protocol_parameters" @@ -312,10 +363,19 @@ def get_parameter_type(self): return "%s" % (self.parameter_type) def get_all_parameter_info(self): + if self.parameter_used: + used = "true" + else: + used = "false" + if self.parameter_download: + download = "true" + else: + download = "false" param_info = [] param_info.append(self.parameter_name) param_info.append(self.parameter_order) - param_info.append(self.parameter_used) + param_info.append(used) + param_info.append(download) param_info.append(self.parameter_type) param_info.append(self.parameter_option_values) param_info.append(self.parameter_min_value) @@ -328,17 +388,23 @@ def get_protocol_fields_for_javascript(self): used = "true" else: used = "false" + if self.parameter_download: + download = "true" + else: + download = "false" if self.parameter_option_values is None: parameter_option_values = "" else: parameter_option_values = self.parameter_option_values field_data = [] field_data.append(self.parameter_name) - field_data.append(self.parameter_order) field_data.append(used) + field_data.append(download) field_data.append(self.parameter_type) field_data.append(parameter_option_values) + field_data.append(self.parameter_min_value) + field_data.append(self.parameter_max_value) field_data.append(self.parameter_description) return field_data @@ -347,8 +413,11 @@ def update_protocol_fields(self, prot_param_data): self.parameter_description = prot_param_data["Description"] self.parameter_order = prot_param_data["Order"] self.parameter_used = prot_param_data["Used"] + self.parameter_download = prot_param_data["Downloadable"] self.parameter_option_values = prot_param_data["Option Values"] self.parameter_type = prot_param_data["Parameter Type"] + self.parameter_max_value = prot_param_data["Max Value"] + self.parameter_min_value = prot_param_data["Min Value"] self.save() objects = ProtocolParametersManager() @@ -372,6 +441,7 @@ def get_id(self): class StatesForMolecule(models.Model): molecule_state_name = models.CharField(max_length=50) + molecule_state_display = models.CharField(max_length=80, null=True, blank=True) class Meta: db_table = "core_states_for_molecule" @@ -928,7 +998,7 @@ def create_sample_project_fields(self, project_field_data): sample_project_field_order=project_field_data["Order"], sample_project_field_used=project_field_data["Used"], sample_project_field_type=project_field_data["Field type"], - sample_project_searchable=project_field_data["Searchable"], + sample_project_downloadable=project_field_data.get("Downloadable", False), # do not include optional values. Set to empty sample_project_option_list="", ) @@ -957,7 +1027,7 @@ class SampleProjectsFields(models.Model): sample_project_field_used = models.BooleanField() sample_project_field_type = models.CharField(max_length=20) sample_project_option_list = models.CharField(max_length=255, null=True, blank=True) - sample_project_searchable = models.BooleanField(default=False) + sample_project_downloadable = models.BooleanField(default=False) generated_at = models.DateTimeField(auto_now_add=True) class Meta: @@ -1005,7 +1075,7 @@ def get_sample_project_fields_name(self, include_search=None): used = "true" else: used = "false" - if self.sample_project_searchable: + if self.sample_project_downloadable: searchable = "true" else: searchable = "false" @@ -1035,7 +1105,7 @@ def update_sample_project_fields(self, project_field_data): self.sample_project_field_order = project_field_data["Order"] self.sample_project_field_used = project_field_data["Used"] self.sample_project_field_type = project_field_data["Field type"] - self.sample_project_searchable = project_field_data["Searchable"] + self.sample_project_downloadable = project_field_data["Downloadable"] self.sample_project_field_classification_id = project_field_data[ "SampleProjectFieldClassificationID" ] @@ -1286,7 +1356,7 @@ def get_info_for_searching(self): sample_info.append(self.sample_type.get_name()) try: sample_info.append(self.species.get_name()) - except KeyError: + except (KeyError, AttributeError): sample_info.append("Not defined") return sample_info @@ -1307,7 +1377,10 @@ def get_info_for_display(self): sample_info.append(collection_sample_date) sample_info.append(sample_entry_date) sample_info.append(self.sample_type.get_name()) - sample_info.append(self.species.get_name()) + try: + sample_info.append(self.species.get_name()) + except (KeyError, AttributeError): + sample_info.append("Not defined") sample_info.append(self.reused_number) sample_info.append(self.sample_user.username) return sample_info @@ -1376,7 +1449,11 @@ def get_sample_type(self): return "%s" % (self.sample_type.get_name()) def get_species(self): - return "%s" % (self.species.get_name()) + try: + species = self.species.get_name() + except (KeyError, AttributeError): + species = "Not defined" + return "%s" % (species) def get_register_user(self): if self.sample_user is None: @@ -1465,7 +1542,7 @@ class MoleculeUsedFor(models.Model): massive_use = models.BooleanField(default=False) class Meta: - db_table = "core_molecule_used_for" + db_table = "core_sample_continues_on" def __str__(self): return "%s" % (self.used_for) @@ -1481,6 +1558,7 @@ def get_massive(self): class MoleculePreparationManager(models.Manager): def create_molecule(self, molecule_data): + req_upd_sample_state = False molecule_used_obj = MoleculeType.objects.filter( molecule_type__exact=molecule_data["molecule_type"] ).last() @@ -1491,17 +1569,27 @@ def create_molecule(self, molecule_data): protocol_used_obj = Protocols.objects.filter( name__exact=molecule_data["protocol_used"], type__exact=protocol_type_obj ).last() + if ProtocolParameters.objects.filter(protocol_id=protocol_used_obj).exists(): + m_state = StatesForMolecule.objects.get( + molecule_state_name__exact="defined" + ) + else: + m_state = StatesForMolecule.objects.get( + molecule_state_name__exact="assigned_parameters" + ) + req_upd_sample_state = True new_molecule = self.create( protocol_used=protocol_used_obj, sample=molecule_data["sample"], molecule_type=molecule_used_obj, - state=StatesForMolecule.objects.get(molecule_state_name__exact="Defined"), + state=m_state, molecule_code_id=molecule_data["molecule_code_id"], molecule_extraction_date=molecule_data["molecule_extraction_date"], extraction_type=molecule_data["extraction_type"], molecule_user=User.objects.get(username__exact=molecule_data["user"]), ) - + if req_upd_sample_state is True: + new_molecule.sample.set_state("Pending for use") return new_molecule @@ -1518,7 +1606,7 @@ class MoleculePreparation(models.Model): UserLotCommercialKits, on_delete=models.CASCADE, null=True, blank=True ) - molecule_used_for = models.ForeignKey( + sample_continues_on = models.ForeignKey( MoleculeUsedFor, on_delete=models.CASCADE, null=True, blank=True ) @@ -1543,10 +1631,10 @@ def get_info_for_display(self): molecule_info.append(extraction_date) molecule_info.append(self.extraction_type) molecule_info.append(self.molecule_type.get_name()) - if self.molecule_used_for is None: + if self.sample_continues_on is None: molecule_info.append("Not defined yet") else: - molecule_info.append(self.molecule_used_for.get_molecule_use_name()) + molecule_info.append(self.sample_continues_on.get_molecule_use_name()) molecule_info.append(self.protocol_used.get_name()) molecule_info.append(self.reused_number) return molecule_info @@ -1590,10 +1678,10 @@ def get_user_lot_kit_obj(self): return self.user_lot_kit_id def set_molecule_use(self, use_for_molecule, app_name): - self.molecule_used_for_obj = MoleculeUsedFor.objects.filter( + self.sample_continues_on_obj = MoleculeUsedFor.objects.filter( used_for__exact=use_for_molecule, apps_name__exact=app_name ).last() - self.used_for_massive_sequencing = self.molecule_used_for_obj.get_massive() + self.used_for_massive_sequencing = self.sample_continues_on_obj.get_massive() self.save() return self @@ -1703,7 +1791,7 @@ def create_sequencer_in_lab(self, sequencer_value): platform_obj = SequencingPlatform.objects.get( pk__exact=sequencer_value["platformID"] ) - except models.SequencingPlatform.DoesNotExist: + except SequencingPlatform.DoesNotExist: platform_obj = None new_sequencer = self.create( platform_id=platform_obj, diff --git a/core/scripts/migrate_optional_values.py b/core/scripts/migrate_optional_values.py index 31eca01ea..3dcc9fdeb 100644 --- a/core/scripts/migrate_optional_values.py +++ b/core/scripts/migrate_optional_values.py @@ -1,6 +1,7 @@ import core.models """ + Upgrade: 2.3.0 -> 3.0.0 The script is applicable for the upgrade from 2.3.0 to 3.0.0. Because the new version changes the way that optional values are stored. Instead of having a field "sample_project_option_list" now values are diff --git a/core/scripts/migrate_sample_type.py b/core/scripts/migrate_sample_type.py index 139865a81..df8ee8543 100644 --- a/core/scripts/migrate_sample_type.py +++ b/core/scripts/migrate_sample_type.py @@ -1,7 +1,7 @@ import core.models - """ + Upgrade: 2.3.0 -> 3.0.0 The script is applicable for the upgrade from 2.3.0 to 3.0.0. Because the new version changes the value that is stored now is the field name and not the number and instead of optional values now are the diff --git a/core/scripts/rename_app_name.py b/core/scripts/rename_app_name.py index 51f312bc4..549928907 100644 --- a/core/scripts/rename_app_name.py +++ b/core/scripts/rename_app_name.py @@ -1,7 +1,7 @@ import core.models - """ + Upgrade: 2.3.0 -> 3.0.0 The script is applicable for the upgrade from 2.3.0 to 3.0.0. Because the application in iSkylims have been renamed, this required that some tables where was indicated the application name must be diff --git a/core/templates/core/footer.html b/core/templates/core/footer.html index 999f9cdce..6e70abfdc 100644 --- a/core/templates/core/footer.html +++ b/core/templates/core/footer.html @@ -6,30 +6,33 @@
Contact us
    -
  • +
  • + +
Social networks
- - - + +
Powered by
- +
-

Version 3.0.0

-
- - - +

Version 3.1.0

+ + + + diff --git a/core/utils/commercial_kits.py b/core/utils/commercial_kits.py index dc7ad1585..9bb0dd774 100644 --- a/core/utils/commercial_kits.py +++ b/core/utils/commercial_kits.py @@ -137,14 +137,14 @@ def get_commercial_kit_basic_data(kit_obj): kit_data = {} if kit_obj.platform_kit_obj(): kit_data["data"] = kit_obj.get_commercial_platform_basic_data() - kit_data[ - "heading" - ] = core.core_config.HEADING_FOR_NEW_SAVED_COMMERCIAL_PLATFORM_KIT + kit_data["heading"] = ( + core.core_config.HEADING_FOR_NEW_SAVED_COMMERCIAL_PLATFORM_KIT + ) else: kit_data["data"] = kit_obj.get_commercial_protocol_basic_data() - kit_data[ - "heading" - ] = core.core_config.HEADING_FOR_NEW_SAVED_COMMERCIAL_PROTOCOL_KIT + kit_data["heading"] = ( + core.core_config.HEADING_FOR_NEW_SAVED_COMMERCIAL_PROTOCOL_KIT + ) kit_data["protocol_kit"] = True return kit_data @@ -259,18 +259,18 @@ def get_user_lot_kit_data(register_user_obj=None, expired=False): ) if len(user_exp_kit_data["data"]) > 0: - user_exp_kit_data[ - "heading" - ] = core.core_config.HEADING_FOR_USER_LOT_KIT_INVENTORY + user_exp_kit_data["heading"] = ( + core.core_config.HEADING_FOR_USER_LOT_KIT_INVENTORY + ) return user_exp_kit_data def get_lot_user_commercial_kit_basic_data(kit_obj): lot_kit_data = {} lot_kit_data["data"] = kit_obj.get_basic_data() - lot_kit_data[ - "heading" - ] = core.core_config.HEADING_FOR_LOT_USER_COMMERCIAL_KIT_BASIC_DATA + lot_kit_data["heading"] = ( + core.core_config.HEADING_FOR_LOT_USER_COMMERCIAL_KIT_BASIC_DATA + ) return lot_kit_data @@ -403,9 +403,9 @@ def get_molecule_lot_kit_in_sample(sample_id): if core.models.MoleculePreparation.objects.filter( sample__pk__exact=sample_id ).exists(): - extraction_kits[ - "molecule_heading_lot_kits" - ] = core.core_config.HEADING_FOR_DISPLAY_IN_SAMPLE_INFO_USER_KIT_DATA + extraction_kits["molecule_heading_lot_kits"] = ( + core.core_config.HEADING_FOR_DISPLAY_IN_SAMPLE_INFO_USER_KIT_DATA + ) molecule_objs = core.models.MoleculePreparation.objects.filter( sample__pk__exact=sample_id ).order_by("protocol_used") @@ -433,18 +433,17 @@ def get_user_lot_kit_data_to_display(user_lot_kit_obj): """ display_data = {} display_data["lot_kit_data"] = user_lot_kit_obj.get_basic_data() - display_data[ - "lot_kit_heading" - ] = core.core_config.HEADING_FOR_LOT_USER_COMMERCIAL_KIT_BASIC_DATA + display_data["lot_kit_heading"] = ( + core.core_config.HEADING_FOR_LOT_USER_COMMERCIAL_KIT_BASIC_DATA + ) commercial_obj = user_lot_kit_obj.get_commercial_obj() commercial_data = commercial_obj.get_commercial_protocol_basic_data() commercial_data.append(commercial_obj.get_platform_name()) commercial_data.append(commercial_obj.get_cat_number()) display_data["commercial_data"] = [commercial_data] - display_data[ - "commercial_heading" - ] = core.core_config.HEADING_FOR_COMMERCIAL_KIT_BASIC_DATA - + display_data["commercial_heading"] = ( + core.core_config.HEADING_FOR_COMMERCIAL_KIT_BASIC_DATA + ) return display_data diff --git a/core/utils/common.py b/core/utils/common.py index 6873eb1d5..589fbe681 100644 --- a/core/utils/common.py +++ b/core/utils/common.py @@ -250,26 +250,26 @@ def save_inital_sample_setting_value(apps_name, data): if "lab_request" in data: lab_request_data = {} lab_request_data["apps_name"] = apps_name - lab_request_data["labName"] = data["lab_request"]["labRequestName"] - lab_request_data["labNameCoding"] = data["lab_request"]["labRequesCoding"] - lab_request_data["labUnit"] = data["lab_request"]["department"] - lab_request_data["labContactName"] = data["lab_request"]["contact"] - lab_request_data["labPhone"] = data["lab_request"]["phone"] - lab_request_data["labEmail"] = data["lab_request"]["email"] + lab_request_data["lab_name"] = data["lab_request"]["labRequestName"] + lab_request_data["lab_name_coding"] = data["lab_request"]["labRequesCoding"] + lab_request_data["lab_unit"] = data["lab_request"]["department"] + lab_request_data["lab_contact_name"] = data["lab_request"]["contact"] + lab_request_data["lab_phone"] = data["lab_request"]["phone"] + lab_request_data["lab_email"] = data["lab_request"]["email"] lab_request_data["address"] = data["lab_request"]["address"] lab_request_data["city"] = data["lab_request"]["city"] if core.models.LabRequest.objects.filter( - lab_name_coding__iexact=lab_request_data["labNameCoding"], + lab_name_coding__iexact=lab_request_data["lab_name_coding"], apps_name__exact=lab_request_data["apps_name"], ).exists(): setting_defined["ERROR"] = [ core.core_config.ERROR_LABORATORY_REQUEST_ALREADY_DEFINED, - lab_request_data["labNameCoding"], + lab_request_data["lab_name_coding"], ] return setting_defined core.models.LabRequest.objects.create_lab_request(lab_request_data) setting_defined["settings"] = "Lab Request" - setting_defined["value"] = lab_request_data["labName"] + setting_defined["value"] = lab_request_data["lab_name"] if "molecule_type" in data: molecule_type_data = {} molecule_type_data["apps_name"] = apps_name diff --git a/core/utils/graphics.py b/core/utils/graphics.py index 788644fb6..44dd03bae 100644 --- a/core/utils/graphics.py +++ b/core/utils/graphics.py @@ -1,25 +1,18 @@ -def preparation_3D_pie(heading, sub_title, theme, source_data): - """_summary_ +def preparation_3D_pie( + heading: str, sub_title: str, theme: str, source_data: dict +) -> dict: + """Join the parameters to create a dictionary with two keys: chart and data. + The chart key contains the heading, sub_title, theme, numberPrefix, showlegend, placevaluesInside, showpercentvalues, rotatevalues, showCanvasBg, showCanvasBase, canvasBaseDepth, canvasBgDepth, canvasBaseColor, canvasBgColor, exportEnabled. + The data key contains a list of dictionaries with the keys label and value. - Parameters - ---------- - heading : str - text to insert on top of graphic - sub_title : str - additional text to include in graphic - axis_x_description : str - description of x axis - axis_y_description : str - description - theme : str - name of the available themes to be used for the graphic - source_data : dict - Dictionary containing value for the graphic + Args: + heading (str): title of the chart + sub_title (str): additional information + theme (str): theme of the chart + source_data (dict): dictionary with the data to be displayed - Returns - ------- - _type_ - _description_ + Returns: + dict: dictionary with the keys chart and data """ data_source = {} data_source["chart"] = { @@ -58,15 +51,30 @@ def preparation_3D_pie(heading, sub_title, theme, source_data): def preparation_graphic_data( - heading, - sub_caption, - x_axis_name, - y_axis_name, - theme, - input_data, - label_key=None, - label_value=None, -): + heading: str, + sub_caption: str, + x_axis_name: str, + y_axis_name: str, + theme: str, + input_data: dict, + label_key: str = None, + label_value: str = None, +) -> dict: + """Join the parameters to create a dictionary with two keys: chart and data. + + Args: + heading (str): heading of the chart + sub_caption (str): sub_caption of the chart + x_axis_name (str): name of the x axis + y_axis_name (str): name of the y axis + theme (str): theme of the chart + input_data (dict): data to be displayed + label_key (str, optional): key name from the input dict. Defaults to None. + label_value (str, optional): value field in the innput dict. Defaults to None. + + Returns: + dict: dictionary with the keys chart and data + """ data_source = {} data_source["chart"] = { "caption": heading, diff --git a/core/utils/patient_projects.py b/core/utils/patient_projects.py index afee7ee48..61ad821db 100644 --- a/core/utils/patient_projects.py +++ b/core/utils/patient_projects.py @@ -76,9 +76,9 @@ def get_all_project_info(proyect_id): if core.models.PatientProjectsFields.objects.filter( patient_projects_id=project_obj ).exists(): - project_data[ - "field_heading" - ] = core.core_config.HEADING_FOR_DEFINING_PROJECT_FIELDS + project_data["field_heading"] = ( + core.core_config.HEADING_FOR_DEFINING_PROJECT_FIELDS + ) project_data["project_name"] = project_obj.get_project_name() project_fields = core.models.PatientProjectsFields.objects.filter( patient_projects_id=project_obj diff --git a/core/utils/protocols.py b/core/utils/protocols.py index 8346499ca..8d9a0c72c 100644 --- a/core/utils/protocols.py +++ b/core/utils/protocols.py @@ -66,9 +66,9 @@ def define_table_for_prot_parameters(protocol_id): prot_parameters["protocol_name"] = protocol_obj.get_name() prot_parameters["protocol_id"] = protocol_id - prot_parameters[ - "heading" - ] = core.core_config.HEADING_FOR_DEFINING_PROTOCOL_PARAMETERS + prot_parameters["heading"] = ( + core.core_config.HEADING_FOR_DEFINING_PROTOCOL_PARAMETERS + ) return prot_parameters @@ -176,17 +176,17 @@ def get_all_protocol_info(protocol_id): protocol_data. """ protocol_data = {} - protocol_data["parameters"] = [] protocol_obj = core.models.Protocols.objects.get(pk__exact=protocol_id) if core.models.ProtocolParameters.objects.filter(protocol_id=protocol_obj).exists(): - protocol_data[ - "parameter_heading" - ] = core.core_config.HEADING_FOR_DEFINING_PROTOCOL_PARAMETERS + protocol_data["parameter_heading"] = ( + core.core_config.HEADING_FOR_DEFINING_PROTOCOL_PARAMETERS + ) protocol_data["protocol_name"] = protocol_obj.get_name() protocol_parameters = core.models.ProtocolParameters.objects.filter( protocol_id=protocol_obj ).order_by("parameter_order") + protocol_data["parameters"] = [] for parameter in protocol_parameters: protocol_data["parameters"].append(parameter.get_all_parameter_info()) protocol_data["protocol_id"] = protocol_id @@ -231,13 +231,12 @@ def get_protocol_fields(protocol_id): parameter_data.append(protocol_parameter_obj.get_parameter_protocol_id()) parameter_list.append(parameter_data) - parameters_protocol[ - "heading" - ] = core.core_config.HEADING_FOR_MODIFY_PROTOCOL_FIELDS + parameters_protocol["heading"] = ( + core.core_config.HEADING_FOR_MODIFY_PROTOCOL_FIELDS + ) parameters_protocol["protocol_id"] = protocol_id parameters_protocol["protocol_name"] = protocol_obj.get_name() parameters_protocol["fields"] = parameter_list - return parameters_protocol @@ -365,13 +364,12 @@ def modify_fields_in_protocol(form_data): def set_protocol_parameters(request): protocol_id = request.POST["protocol_id"] - json_data = json.loads(request.POST["table_data1"]) parameters = core.core_config.HEADING_FOR_DEFINING_PROTOCOL_PARAMETERS protocol_id_obj = core.models.Protocols.objects.get(pk__exact=protocol_id) saved_parameters = [] stored_parameters = {} - for row_data in json_data: + for row_data in json.loads(request.POST["table_data1"]): if row_data[0] == "": continue prot_parameters = {} diff --git a/core/utils/samples.py b/core/utils/samples.py index 0c9a28c8e..3b141250c 100644 --- a/core/utils/samples.py +++ b/core/utils/samples.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.db.models import CharField, Count, F, Func, Prefetch, Value +from django.db.models.functions import Lower # Local imports import core.core_config @@ -172,13 +173,18 @@ def save_recorded_samples(samples_data, req_user, app_name): sample["sample_project"] = core.models.SampleProjects.objects.get( sample_project_name__exact=sample["sample_project"] ) - # If only recorded set sample to completed state if sample["only_recorded"] and sample["sample_project"] is None: sample["sample_state"] = "Completed" sample["completed_date"] = datetime.datetime.now() - # If no sample project data needed set to defined - elif sample["sample_project"] is None: + # If no sample project data needed or sample projects has no parameters + # then set the sample state to defined + elif ( + sample["sample_project"] is None + or core.models.SampleProjects.objects.filter( + sample_project_name__exact=sample["sample_project"] + ).exists() + ): sample["sample_state"] = "Defined" else: sample["sample_state"] = "Pre-Defined" @@ -193,20 +199,24 @@ def save_recorded_samples(samples_data, req_user, app_name): return samples_data -def validate_sample_data(sample_data, req_user, app_name): - """Sample data validation +def validate_sample_data( + sample_data: json, + req_user: str, + app_name: str, + repeat_allowed: bool = False, + allow_user_repeat: bool = False, +) -> tuple: + """sample data validation - Parameters - ---------- - sample_data - sample data formatted in json obtained from jspreadsheet - req_user - requesting user - app_name - application name (wetlab, drylab, core, etc.) + Args: + sample_data (json): sample data formatted in json obtained from jspreadsheet + req_user (str): requested user name + app_name (str): application name (wetlab, drylab, core, etc.) + repeat_allowed (bool, optional): allow or not that sample can be repeated. Defaults to False. + allow_user_repeat (bool, optional): if allowed to be repeated check if same request user is allowed have repeated sample names. Defaults to False. - Returns - ------- + Returns: + tuple: returns result as boolean and a dictionary with the following keys validation list with validation info for each sample with format: [{"Sample Name": "test 01", @@ -222,6 +232,27 @@ def validate_sample_data(sample_data, req_user, app_name): sample_name_list = [] line = 0 result = True + not_allowed_sample_names = {} + # collect the sample names already in the database in case that the user + # is not allowed to repeat + if not repeat_allowed: + if not allow_user_repeat: + if core.models.Samples.objects.filter( + sample_user__username__exact=req_user + ).exists(): + existing_sample_name_list = list( + core.models.Samples.objects.filter( + sample_user__username__iexact=req_user + ).values_list("sample_name", flat=True) + ) + else: + existing_sample_name_list = list( + core.models.Samples.objects.all().values_list( + Lower("sample_name", flat=True) + ) + ) + # convert to dict to speed up the search + not_allowed_sample_names = dict.fromkeys(existing_sample_name_list, 0) for sample in sample_data: line += 1 sample_dict = {} @@ -237,7 +268,10 @@ def validate_sample_data(sample_data, req_user, app_name): validation.append(sample_dict) continue - if sample["sample_name"] not in sample_name_list: + if repeat_allowed or ( + sample["sample_name"].lower() not in sample_name_list + and sample["sample_name"].lower() not in not_allowed_sample_names + ): sample_name_list.append(sample["sample_name"]) else: error_cause = core.core_config.ERROR_REPEATED_SAMPLE_NAME.copy() @@ -384,6 +418,10 @@ def validate_project_data(project_data, project_name, sample_validation=False): sample_dict["Validation error"].append(" ".join(error_cause)) """ if field_type == "Date" and sample[field_name] != "": + # if field contains also time, then removed it + sample[field_name] = re.sub( + r"\s\d{2}:\d{2}:\d{2}", "", sample[field_name] + ) try: datetime.datetime.strptime(sample[field_name], "%Y-%m-%d") except Exception: @@ -436,13 +474,13 @@ def save_project_data(excel_data, project_info): for field in project_info["sample_project_fields"]: field_value = {} field_value["sample_id"] = sample_id - field_value[ - "sample_project_field_id" - ] = core.models.SampleProjectsFields.objects.get( - sample_projects_id__exact=core.models.SampleProjects.objects.get( - sample_project_name__exact=project_info["sample_project_name"] - ), - sample_project_field_name__exact=field["sample_project_field_name"], + field_value["sample_project_field_id"] = ( + core.models.SampleProjectsFields.objects.get( + sample_projects_id__exact=core.models.SampleProjects.objects.get( + sample_project_name__exact=project_info["sample_project_name"] + ), + sample_project_field_name__exact=field["sample_project_field_name"], + ) ) field_value["sample_project_field_value"] = sample[ field["sample_project_field_name"] @@ -521,18 +559,18 @@ def add_molecule_protocol_parameters(data, parameters): ) for param in parameters: molecule_parameter_value = {} - molecule_parameter_value[ - "molecule_parameter_id" - ] = core.models.ProtocolParameters.objects.filter( - protocol_id=prot_obj, parameter_name__iexact=param - ).last() + molecule_parameter_value["molecule_parameter_id"] = ( + core.models.ProtocolParameters.objects.filter( + protocol_id=prot_obj, parameter_name__iexact=param + ).last() + ) molecule_parameter_value["molecule_id"] = molecule_obj molecule_parameter_value["parameter_value"] = row[param] _ = core.models.MoleculeParameterValue.objects.create_molecule_parameter_value( molecule_parameter_value ) - molecule_obj.set_state("Completed") + molecule_obj.set_state("assigned_parameters") # Update sample state sample_obj = molecule_obj.get_sample_obj() sample_obj.set_state("Pending for use") @@ -668,7 +706,9 @@ def create_table_molecule_pending_use(sample_list, app_name): use_type = {} use_type["data"] = list( core.models.MoleculePreparation.objects.filter( - molecule_used_for=None, sample__in=sample_list + sample_continues_on=None, + sample__in=sample_list, + state__molecule_state_name="assigned_parameters", ).values_list("sample__sample_name", "molecule_code_id", "pk") ) if len(use_type["data"]) > 0: @@ -740,9 +780,9 @@ def create_table_pending_molecules(molecule_list): .annotate(pk=F("pk")) ) if len(molecule_data["data"]) > 0: - molecule_data[ - "molecule_heading" - ] = core.core_config.HEADING_FOR_PENDING_MOLECULES + molecule_data["molecule_heading"] = ( + core.core_config.HEADING_FOR_PENDING_MOLECULES + ) return molecule_data @@ -762,9 +802,9 @@ def define_table_for_sample_project_fields(sample_project_id): pk__exact=sample_project_id ) - sample_project_data[ - "sample_project_name" - ] = sample_project_obj.get_sample_project_name() + sample_project_data["sample_project_name"] = ( + sample_project_obj.get_sample_project_name() + ) sample_project_data["sample_project_id"] = sample_project_id sample_project_data["heading"] = core.core_config.HEADING_FOR_SAMPLE_PROJECT_FIELDS return sample_project_data @@ -838,9 +878,9 @@ def get_all_sample_information(sample_id, join_values=False): sample_information["sample_name"] = sample_obj.get_sample_name() sample_information["sample_definition"] = sample_obj.get_info_for_display() - sample_information[ - "sample_definition_heading" - ] = core.core_config.HEADING_FOR_SAMPLE_DEFINITION + sample_information["sample_definition_heading"] = ( + core.core_config.HEADING_FOR_SAMPLE_DEFINITION + ) if join_values: sample_information["sample_definition_join_value"] = list( zip( @@ -860,9 +900,9 @@ def get_all_sample_information(sample_id, join_values=False): # check if molecule information exists for the sample if core.models.MoleculePreparation.objects.filter(sample=sample_obj).exists(): molecules = core.models.MoleculePreparation.objects.filter(sample=sample_obj) - sample_information[ - "molecule_definition_heading" - ] = core.core_config.HEADING_FOR_MOLECULE_DEFINITION + sample_information["molecule_definition_heading"] = ( + core.core_config.HEADING_FOR_MOLECULE_DEFINITION + ) sample_information["molecule_definition"] = [] sample_information["molecule_parameter_values"] = [] sample_information["molecule_definition_data"] = [] @@ -877,7 +917,7 @@ def get_all_sample_information(sample_id, join_values=False): parameter_names = core.models.ProtocolParameters.objects.filter( protocol_id=protocol_used_obj ).order_by("parameter_order") - molecule_param_heading = ["Molecule CodeID"] + molecule_param_heading = ["Extraction Code ID"] mol_param_value = [molecule.get_molecule_code_id()] for p_name in parameter_names: molecule_param_heading.append(p_name.get_parameter_name()) @@ -885,10 +925,14 @@ def get_all_sample_information(sample_id, join_values=False): molecule_id=molecule ).exists(): try: + # use the last value to avoid errors for incorrect + # used. mol_param_value.append( - core.models.MoleculeParameterValue.objects.get( + core.models.MoleculeParameterValue.objects.filter( molecule_id=molecule, molecule_parameter_id=p_name - ).get_param_value() + ) + .last() + .get_param_value() ) except core.models.MoleculeParameterValue.DoesNotExist: # if the parameter was not set at the time the molecule was handeled @@ -950,9 +994,9 @@ def get_info_to_display_sample_project(sample_project_id): ) # collect data from project info_s_project["sample_project_id"] = sample_project_id - info_s_project[ - "sample_project_name" - ] = sample_project_obj.get_sample_project_name() + info_s_project["sample_project_name"] = ( + sample_project_obj.get_sample_project_name() + ) info_s_project["main_data"] = list( zip( core.core_config.SAMPLE_PROJECT_MAIN_DATA, @@ -1058,13 +1102,13 @@ def get_parameters_sample_project(sample_project_id): parameter_data.insert(1, "") s_project_fields_list.append(parameter_data) parameters_s_project["fields"] = s_project_fields_list - parameters_s_project[ - "heading" - ] = core.core_config.HEADING_FOR_MODIFY_SAMPLE_PROJECT_FIELDS + parameters_s_project["heading"] = ( + core.core_config.HEADING_FOR_MODIFY_SAMPLE_PROJECT_FIELDS + ) parameters_s_project["sample_project_id"] = sample_project_id - parameters_s_project[ - "sample_project_name" - ] = sample_project_obj.get_sample_project_name() + parameters_s_project["sample_project_name"] = ( + sample_project_obj.get_sample_project_name() + ) # parameters_s_project["parameter_names"] = ",".join(parameter_names) # parameters_s_project["parameter_ids"] = ",".join(parameter_ids) else: @@ -1258,32 +1302,37 @@ def get_molecule_protocols(apps_name): def get_molecule_data_and_protocol_parameters(protocol_objs): mol_data_parm = {} for protocol_obj, mol_ids in protocol_objs.items(): - prot_name = protocol_obj.get_name() - mol_data_parm[prot_name] = {} - mol_data_parm[prot_name][ - "params_type" - ] = core.utils.protocols.get_protocol_parameters_and_type(protocol_obj) - mol_data_parm[prot_name][ - "fix_heading" - ] = core.core_config.HEADING_FOR_MOLECULE_ADDING_PARAMETERS - mol_data_parm[prot_name][ - "lot_kit" - ] = core.utils.commercial_kits.get_lot_commercial_kits(protocol_obj) - mol_data_parm[prot_name]["param_heading"] = [] - prot_params = core.models.ProtocolParameters.objects.filter( + # check if the protocol has parameters + if core.models.ProtocolParameters.objects.filter( protocol_id=protocol_obj, parameter_used=True - ).order_by("parameter_order") - for param in prot_params: - mol_data_parm[prot_name]["param_heading"].append(param.get_parameter_name()) - mol_data_parm[prot_name]["param_heading_in_string"] = ";".join( - mol_data_parm[prot_name]["param_heading"] - ) - mol_data_parm[prot_name]["m_data"] = list( - core.models.MoleculePreparation.objects.filter(pk__in=mol_ids).values_list( - "pk", "sample__sample_name", "molecule_code_id" + ).exists(): + prot_name = protocol_obj.get_name() + mol_data_parm[prot_name] = {} + mol_data_parm[prot_name]["params_type"] = ( + core.utils.protocols.get_protocol_parameters_and_type(protocol_obj) + ) + mol_data_parm[prot_name][ + "fix_heading" + ] = core.core_config.HEADING_FOR_MOLECULE_ADDING_PARAMETERS + mol_data_parm[prot_name]["lot_kit"] = ( + core.utils.commercial_kits.get_lot_commercial_kits(protocol_obj) + ) + mol_data_parm[prot_name]["param_heading"] = [] + prot_params = core.models.ProtocolParameters.objects.filter( + protocol_id=protocol_obj, parameter_used=True + ).order_by("parameter_order") + for param in prot_params: + mol_data_parm[prot_name]["param_heading"].append( + param.get_parameter_name() + ) + mol_data_parm[prot_name]["param_heading_in_string"] = ";".join( + mol_data_parm[prot_name]["param_heading"] + ) + mol_data_parm[prot_name]["m_data"] = list( + core.models.MoleculePreparation.objects.filter( + pk__in=mol_ids + ).values_list("pk", "sample__sample_name", "molecule_code_id") ) - ) - return mol_data_parm @@ -1483,9 +1532,10 @@ def get_selection_from_excel_data(data, heading, check_field, field_id): excel_data = json.loads(data) # Convert excel list-list to dictionary with field_names excel_json_data = core.utils.common.jspreadsheet_to_dict(heading, excel_data) + for row in excel_json_data: if check_field is not None: - if row[check_field] is True or row[check_field] != "": + if row[check_field] is not False and row[check_field] != "": selected.append(row[field_id]) selected_row.append(row) else: @@ -1511,9 +1561,9 @@ def get_table_record_molecule(samples, apps_name): """ molecule_information = {} - molecule_information[ - "headings" - ] = core.core_config.HEADING_FOR_MOLECULE_PROTOCOL_DEFINITION + molecule_information["headings"] = ( + core.core_config.HEADING_FOR_MOLECULE_PROTOCOL_DEFINITION + ) sample_code_ids = [] valid_samples = [] for sample in samples: @@ -1605,9 +1655,9 @@ def get_type_of_sample_information(sample_type_id): ) break else: - sample_type_data[ - "ERROR" - ] = core.core_config.ERROR_TYPE_OF_SAMPLE_ID_DOES_NOT_EXISTS + sample_type_data["ERROR"] = ( + core.core_config.ERROR_TYPE_OF_SAMPLE_ID_DOES_NOT_EXISTS + ) return sample_type_data @@ -1783,9 +1833,9 @@ def record_molecule_use(from_data, app_name): if core.models.MoleculeUsedFor.objects.filter( used_for__exact=from_data["moleculeUseName"] ).exists(): - molecule_use_information[ - "ERROR" - ] = core.core_config.ERROR_MOLECULE_USE_FOR_EXISTS + molecule_use_information["ERROR"] = ( + core.core_config.ERROR_MOLECULE_USE_FOR_EXISTS + ) return molecule_use_information molecule_use_data = {} molecule_use_data["usedFor"] = from_data["moleculeUseName"] @@ -1799,7 +1849,7 @@ def record_molecule_use(from_data, app_name): return molecule_use_information -def record_molecules(samples, excel_data, heading, user, app_name): +def record_extract_protocol(samples, excel_data, heading, user, app_name): """Recored the molecues defined in excel_data. If information is missing returns the data to display again for correcting. @@ -1828,7 +1878,7 @@ def record_molecules(samples, excel_data, heading, user, app_name): return_data.append(r_data) # collect data for dropdown selection protocol_filter_selection = [] - (protocols_dict, protocol_list) = get_molecule_protocols(app_name) + protocols_dict, protocol_list = get_molecule_protocols(app_name) for key, value in protocols_dict.items(): protocol_filter_selection.append([key, value]) return { @@ -1863,7 +1913,6 @@ def record_molecules(samples, excel_data, heading, user, app_name): molecule_data["molecule_code_id"] = code_split.group(1) + str(number_code) else: molecule_data["molecule_code_id"] = sample_obj.get_sample_code() + "_E1" - molecule_obj = core.models.MoleculePreparation.objects.create_molecule( molecule_data ) @@ -2037,7 +2086,7 @@ def set_molecule_use(molecule_use_data, app_name): } for molecule in molecule_use_data: molecule_obj = get_molecule_obj_from_id(molecule["m_id"]) - molecule_obj.set_molecule_use(molecule["Molecule use for"], app_name) + molecule_obj.set_molecule_use(molecule["Sample continues on"], app_name) sample_obj = molecule_obj.get_sample_obj() if molecule_obj.get_used_for_massive(): sample_obj.set_state("Library preparation") @@ -2046,8 +2095,8 @@ def set_molecule_use(molecule_use_data, app_name): molecule_update["data"].append( [ molecule["Sample Name"], - molecule["Molecule CodeID"], - molecule["Molecule use for"], + molecule["Extraction Code ID"], + molecule["Sample continues on"], ] ) return molecule_update @@ -2064,6 +2113,16 @@ def set_sample_project_fields(data_form): saved_fields = [] stored_fields = {} + valid_data = False + # check if there is at least one field to be used + for row_line in excel_json_data: + if row_line["Used"] is True: + valid_data = True + break + if not valid_data: + stored_fields["ERROR"] = core.core_config.ERROR_NO_USED_FIELD_ARE_ARE_SET + return stored_fields + for row_line in excel_json_data: if row_line["Field name"] == "": continue diff --git a/django_utils/.gitignore b/django_utils/.gitignore deleted file mode 100644 index ed8eee433..000000000 --- a/django_utils/.gitignore +++ /dev/null @@ -1,103 +0,0 @@ -# Byte-compiled / optimized / DLL files -__migrations__/ -migrations/ -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ diff --git a/django_utils/forms.py b/django_utils/forms.py index c8f3c3032..187fb2cde 100644 --- a/django_utils/forms.py +++ b/django_utils/forms.py @@ -104,3 +104,17 @@ def __init__(self, *args, **kwargs): ), ), ) + + def clean_username(self): + username = self.cleaned_data.get("username") + if self.instance and self.instance.pk and username: + if ( + User.objects.exclude(pk=self.instance.pk) + .filter(username__exact=username) + .exists() + ): + raise forms.ValidationError( + self.error_messages["duplicate_username"], code="duplicate_username" + ) + return username + return super().clean_username() diff --git a/django_utils/migrations/0001_initial.py b/django_utils/migrations/0001_initial.py new file mode 100644 index 000000000..421f9be63 --- /dev/null +++ b/django_utils/migrations/0001_initial.py @@ -0,0 +1,115 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Center", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("center_name", models.CharField(max_length=50, verbose_name="Center")), + ( + "center_abbr", + models.CharField(max_length=25, verbose_name="Acronym"), + ), + ], + options={ + "db_table": "utils_center", + }, + ), + migrations.CreateModel( + name="ClassificationArea", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("classification_area_name", models.CharField(max_length=80)), + ( + "classification_area_description", + models.CharField(blank=True, max_length=255, null=True), + ), + ], + options={ + "db_table": "utils_classification_area", + }, + ), + migrations.CreateModel( + name="Profile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "profile_position", + models.CharField(max_length=50, verbose_name="Position"), + ), + ( + "profile_area", + models.CharField(max_length=50, verbose_name="Area / Unit"), + ), + ( + "profile_extension", + models.CharField(max_length=5, verbose_name="Phone extension"), + ), + ( + "profile_center", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="django_utils.center", + verbose_name="Center", + ), + ), + ( + "profile_classification_area", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="django_utils.classificationarea", + ), + ), + ( + "profile_user_id", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "utils_profile", + }, + ), + ] diff --git a/django_utils/migrations/__init__.py b/django_utils/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/django_utils/templatetags/replace_spaces.py b/django_utils/templatetags/replace_spaces.py new file mode 100644 index 000000000..d3bf60fba --- /dev/null +++ b/django_utils/templatetags/replace_spaces.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.filter +def replace_spaces_with_underscores(value): + return value.replace(" ", "_") diff --git a/django_utils/templatetags/user_text.py b/django_utils/templatetags/user_text.py index 5e1299451..a8d751d10 100644 --- a/django_utils/templatetags/user_text.py +++ b/django_utils/templatetags/user_text.py @@ -1,6 +1,5 @@ from django import template - register = template.Library() diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 000000000..7e9776d08 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + apache: + container_name: iskylims_apache + restart: unless-stopped + image: registry.access.redhat.com/ubi9/httpd-24 + depends_on: + - app + ports: + - "8080:8080" + volumes: + - /etc/localtime:/etc/localtime:ro + - /usr/share/zoneinfo:/usr/share/zoneinfo:ro + - ${APACHE_CONF_PATH:-/opt/iskylims/conf}/iskylims_apache_reverse_proxy.conf:/etc/httpd/conf.d/iskylims.conf:ro + - ${APACHE_CONF_PATH:-/opt/iskylims/conf}/iskylims_apache_logs.conf:/etc/httpd/conf.d/logformat.conf:ro + - /var/log/local/iskylims/apache:/var/log/httpd:U + - iskylims_static:${APP_INSTALL_PATH:-/opt/iskylims}/static:ro + networks: + - iskylims_net + + app: + container_name: iskylims_app + restart: unless-stopped + build: + context: . + args: + INSTALL_TYPE: ${INSTALL_TYPE:-dep} + GIT_REVISION: ${GIT_REVISION:-main} + INSTALL_CONF: ${INSTALL_CONF:-conf/docker_production_settings.txt} + APP_UID: ${APP_UID:-1212} + APP_GID: ${APP_GID:-1212} + APP_SHELL: ${APP_SHELL:-/sbin/nologin} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + expose: + - "${APP_PORT:-8001}" + environment: + DJANGO_SETTINGS_MODULE: iskylims.settings + DJANGO_DEBUG: ${DJANGO_DEBUG:-false} + APP_MODE: prod + APP_PORT: ${APP_PORT:-8001} + DB_CONN_MAX_AGE: ${DB_CONN_MAX_AGE:-60} + WEB_CONCURRENCY: ${WEB_CONCURRENCY:-2} + GUNICORN_THREADS: ${GUNICORN_THREADS:-2} + GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-300} + GUNICORN_KEEPALIVE: ${GUNICORN_KEEPALIVE:-5} + APP_INSTALL_PATH: ${APP_INSTALL_PATH:-/opt/iskylims} + user: "${APP_UID:-1005}:${APP_GID:-1005}" + volumes: + - /etc/localtime:/etc/localtime:ro + - /usr/share/zoneinfo:/usr/share/zoneinfo:ro + - /var/log/local/iskylims/apps/:${APP_INSTALL_PATH:-/opt/iskylims}/logs:U + - ${DJANGO_SETTINGS_PATH:-/opt/iskylims/iskylims/settings.py}:${APP_INSTALL_PATH:-/opt/iskylims}/iskylims/settings.py:U + - iskylims_documents:${APP_INSTALL_PATH:-/opt/iskylims}/documents + - iskylims_static:${APP_INSTALL_PATH:-/opt/iskylims}/static + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - iskylims_net + +networks: + iskylims_net: + +volumes: + iskylims_documents: + iskylims_static: diff --git a/docker-compose.yml b/docker-compose.test.yml similarity index 72% rename from docker-compose.yml rename to docker-compose.test.yml index 293fa54f3..bf49e7c66 100644 --- a/docker-compose.yml +++ b/docker-compose.test.yml @@ -2,7 +2,7 @@ version: '3.4' services: db: - image: mysql:8.0 + image: docker.io/library/mysql:8.0 container_name: db command: --default-authentication-plugin=mysql_native_password restart: always @@ -13,8 +13,8 @@ services: start_period: 30s environment: MYSQL_DATABASE: iskylims_docker - MYSQL_USER : django - MYSQL_PASSWORD : djangopass + MYSQL_USER: django + MYSQL_PASSWORD: djangopass MYSQL_ROOT_PASSWORD: root ports: @@ -27,7 +27,7 @@ services: - /usr/share/zoneinfo:/usr/share/zoneinfo samba: - image: dperson/samba + image: docker.io/dperson/samba container_name: samba networks: - develop_net @@ -38,15 +38,23 @@ services: command: '-S -s "ngs_data;/mnt/test_ngs_data;yes;yes;no;samba_user;none;none;ngs data samba share" -u "samba_user;sambapasswd" -p' app: - build: . + build: + context: . + args: + INSTALL_TYPE: ${INSTALL_TYPE:-dep} + GIT_REVISION: ${GIT_REVISION:-main} + INSTALL_CONF: ${INSTALL_CONF:-conf/docker_test_settings.txt} + APP_SHELL: ${APP_SHELL:-/bin/bash} container_name: iskylims_app ports: - "8001:8001" + environment: + APP_MODE: dev networks: - develop_net depends_on: db: - condition: service_healthy + condition: service_healthy volumes: - /etc/localtime:/etc/localtime:ro - /usr/share/zoneinfo:/usr/share/zoneinfo @@ -56,4 +64,4 @@ networks: volumes: db_data_vol: - ngs_data_vol: \ No newline at end of file + ngs_data_vol: diff --git a/docker_install.sh b/docker_install.sh deleted file mode 100644 index e92bcf83a..000000000 --- a/docker_install.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/bash - -ISKYLIMS_VERSION="3.0.0" - -usage() { -cat << EOF -This script install and upgrade the iskylims app. - -usage : $0 --demo_data - Optional input data: - --demo_data | provide already dowloaded demo data from zenodo - - -Examples: - Install demo docker system - bash $0 - - Provide already downloaded data from zenodo (compressed) - bash $0 --demo_data /path/to/iskylims_demo_data.tar.gz -EOF -} - -# translate long options to short -reset=true - -for arg in "$@" -do - if [ -n "$reset" ]; then - unset reset - set -- # this resets the "$@" array so we can rebuild it - fi - case "$arg" in - # OPTIONAL - --demo_data) set -- "$@" -d ;; - - # ADITIONAL - --help) set -- "$@" -h ;; - --version) set -- "$@" -v ;; - # PASSING VALUE IN PARAMETER - *) set -- "$@" "$arg" ;; - esac -done - -# SETTING DEFAULT VALUES -demo_data=false - -# PARSE VARIABLE ARGUMENTS WITH getops -options=":d:vh" -while getopts $options opt; do - case $opt in - d) - demo_data=$OPTARG - ;; - h ) - usage - exit 1 - ;; - v ) - echo $ISKYLIMS_VERSION - exit 1 - ;; - \?) - echo "Invalid Option: -$OPTARG" 1>&2 - usage - exit 1 - ;; - : ) - echo "Option -$OPTARG requires an argument." >&2 - exit 1 - ;; - * ) - echo "Unimplemented option: -$OPTARG" >&2; - exit 1 - ;; - esac -done -shift $((OPTIND-1)) - -echo "Deploying test containers..." -docker compose build --no-cache -docker compose up -d - -echo "Waiting 20 seconds for starting database and web services..." -sleep 20 - -echo "Creating the database structure for iSkyLIMS" -docker exec -it iskylims_app python3 manage.py migrate -docker exec -it iskylims_app python3 manage.py makemigrations django_utils core wetlab drylab -docker exec -it iskylims_app python3 manage.py migrate - -echo "Creating super user " -docker exec -it iskylims_app python3 manage.py createsuperuser - -echo "Loading in database initial data" -docker exec -it iskylims_app python3 manage.py loaddata conf/first_install_tables.json -docker exec -it iskylims_app python3 manage.py loaddata test/test_data.json - -echo "Download testing files and copy it to samba container" -if [ "$demo_data" == "false" ];then - wget https://zenodo.org/record/8091169/files/iskylims_demo_data.tar.gz - demo_data="./iskylims_demo_data.tar.gz" -fi -docker cp $demo_data samba:/mnt -docker exec -it samba tar -xf /mnt/iskylims_demo_data.tar.gz -C /mnt - -echo "deleting compress testing file" -docker exec -it samba rm /mnt/iskylims_demo_data.tar.gz - -if [ "$demo_data" == "false" ];then - rm -f $demo_data -fi - -echo "Running crontab" -docker exec -it iskylims_app python3 manage.py crontab add -docker exec -it iskylims_app service cron start - -echo "You can now access iskylims via: http://localhost:8001" \ No newline at end of file diff --git a/drylab/.gitignore b/drylab/.gitignore deleted file mode 100644 index 781e59dfd..000000000 --- a/drylab/.gitignore +++ /dev/null @@ -1,67 +0,0 @@ -# Custom -*.save* -*.swp -drylab_samba_conf.py -drylab_email_conf.py - -# Django migrations -__migrations__/ -migrations/ - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ diff --git a/drylab/admin.py b/drylab/admin.py index a062a2e56..abdf90ed0 100644 --- a/drylab/admin.py +++ b/drylab/admin.py @@ -19,7 +19,6 @@ class ServiceAdmin(admin.ModelAdmin): "service_center", "service_user_id", "service_state", - "service_status", "service_created_date", "service_delivered_date", ) diff --git a/drylab/migrations/0001_initial.py b/drylab/migrations/0001_initial.py new file mode 100644 index 000000000..5b271eca7 --- /dev/null +++ b/drylab/migrations/0001_initial.py @@ -0,0 +1,579 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("core", "0001_initial"), + ("wetlab", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="AvailableService", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "avail_service_description", + models.CharField(max_length=200, verbose_name="Available services"), + ), + ("service_in_use", models.BooleanField(default=True)), + ("service_id", models.CharField(blank=True, max_length=40, null=True)), + ( + "description", + models.CharField(blank=True, max_length=200, null=True), + ), + ("lft", models.PositiveIntegerField(editable=False)), + ("rght", models.PositiveIntegerField(editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(editable=False)), + ( + "parent", + mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="drylab.availableservice", + ), + ), + ], + options={ + "verbose_name": "AvailableService", + "verbose_name_plural": "AvailableServices", + "db_table": "drylab_available_service", + "ordering": ["tree_id", "lft"], + }, + ), + migrations.CreateModel( + name="ConfigSetting", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("configuration_name", models.CharField(max_length=80)), + ( + "configuration_value", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "drylab_config_setting", + }, + ), + migrations.CreateModel( + name="Pipelines", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("pipeline_name", models.CharField(max_length=50)), + ("pipeline_version", models.CharField(max_length=10)), + ("pipeline_in_use", models.BooleanField(default=True)), + ( + "pipeline_file", + models.FileField( + blank=True, null=True, upload_to="drylab/pipelinesFiles" + ), + ), + ( + "pipeline_url", + models.CharField(blank=True, max_length=200, null=True), + ), + ( + "pipeline_description", + models.CharField(blank=True, max_length=500, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "user_name", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "drylab_pipelines", + }, + ), + migrations.CreateModel( + name="Resolution", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "resolution_number", + models.CharField( + max_length=255, null=True, verbose_name="Resolution name" + ), + ), + ( + "resolution_estimated_date", + models.DateField( + null=True, verbose_name=" Estimated resolution date" + ), + ), + ( + "resolution_date", + models.DateField(auto_now_add=True, verbose_name="Resolution date"), + ), + ("resolution_queued_date", models.DateField(blank=True, null=True)), + ( + "resolution_in_progress_date", + models.DateField(blank=True, null=True), + ), + ("resolution_delivery_date", models.DateField(blank=True, null=True)), + ( + "resolution_notes", + models.TextField( + blank=True, + max_length=1000, + null=True, + verbose_name="Resolution notes", + ), + ), + ( + "resolution_full_number", + models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Acronym Name", + ), + ), + ( + "resolution_pdf_file", + models.FileField( + blank=True, null=True, upload_to="documents/drylab/resolutions" + ), + ), + ( + "available_services", + models.ManyToManyField(blank=True, to="drylab.availableservice"), + ), + ( + "resolution_assigned_user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="groups+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "resolution_pipelines", + models.ManyToManyField(blank=True, to="drylab.pipelines"), + ), + ], + options={ + "db_table": "drylab_resolution", + }, + ), + migrations.CreateModel( + name="ResolutionStates", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_value", models.CharField(max_length=50)), + ( + "state_display", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "description", + models.CharField(blank=True, max_length=255, null=True), + ), + ], + options={ + "db_table": "drylab_resolution_states", + }, + ), + migrations.CreateModel( + name="Service", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "service_center", + models.CharField( + max_length=50, null=True, verbose_name="Sequencing center" + ), + ), + ( + "service_request_number", + models.CharField( + max_length=80, null=True, verbose_name="Service ID" + ), + ), + ("service_request_int", models.CharField(max_length=80, null=True)), + ( + "service_run_specs", + models.CharField( + blank=True, + max_length=10, + null=True, + verbose_name="Run specifications", + ), + ), + ( + "service_status", + models.CharField( + choices=[ + ("recorded", "Recorded"), + ("approved", "Approved"), + ("rejected", "Rejected"), + ("queued", "Queued"), + ("in_progress", "In Progress"), + ("delivered", "Delivered"), + ("archived", "Archived"), + ], + max_length=15, + verbose_name="Service status", + ), + ), + ( + "service_notes", + models.TextField( + blank=True, + max_length=2048, + null=True, + verbose_name="Service Notes", + ), + ), + ( + "service_created_date", + models.DateField(auto_now_add=True, null=True), + ), + ("service_approved_date", models.DateField(blank=True, null=True)), + ("service_rejected_date", models.DateField(blank=True, null=True)), + ("service_delivered_date", models.DateField(blank=True, null=True)), + ( + "service_available_service", + mptt.fields.TreeManyToManyField( + to="drylab.availableservice", verbose_name="AvailableServices" + ), + ), + ( + "service_project_names", + models.ManyToManyField( + blank=True, to="wetlab.projects", verbose_name="User's projects" + ), + ), + ( + "service_sequencing_platform", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencingplatform", + ), + ), + ], + options={ + "db_table": "drylab_service", + }, + ), + migrations.CreateModel( + name="ServiceState", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("state_value", models.CharField(max_length=50)), + ( + "state_display", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "description", + models.CharField(blank=True, max_length=255, null=True), + ), + ("show_in_stats", models.BooleanField(default=False)), + ], + options={ + "db_table": "drylab_service_state", + }, + ), + migrations.CreateModel( + name="UploadServiceFile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("upload_file", models.FileField(upload_to="drylab/service_files")), + ( + "upload_file_name", + models.CharField(blank=True, max_length=255, null=True), + ), + ("uploaded_at", models.DateTimeField(auto_now_add=True)), + ( + "upload_service", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="drylab.service", + ), + ), + ], + options={ + "db_table": "drylab_upload_service_file", + }, + ), + migrations.AddField( + model_name="service", + name="service_state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="drylab.servicestate", + verbose_name="Service State", + ), + ), + migrations.AddField( + model_name="service", + name="service_user_id", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="ResolutionParameters", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("resolution_parameter", models.CharField(max_length=50)), + ("resolution_param_value", models.CharField(max_length=80)), + ( + "resolution_param_notes", + models.CharField(blank=True, max_length=200, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "resolution", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="drylab.resolution", + ), + ), + ], + options={ + "db_table": "drylab_resolution_parameters", + }, + ), + migrations.AddField( + model_name="resolution", + name="resolution_service_id", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="resolutions", + to="drylab.service", + ), + ), + migrations.AddField( + model_name="resolution", + name="resolution_state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="drylab.resolutionstates", + ), + ), + migrations.CreateModel( + name="RequestedSamplesInServices", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sample_key", models.CharField(blank=True, max_length=15, null=True)), + ("sample_name", models.CharField(blank=True, max_length=50, null=True)), + ( + "sample_path", + models.CharField(blank=True, max_length=250, null=True), + ), + ( + "run_name_key", + models.CharField(blank=True, max_length=15, null=True), + ), + ("run_name", models.CharField(blank=True, max_length=50, null=True)), + ("project_key", models.CharField(blank=True, max_length=15, null=True)), + ( + "project_name", + models.CharField(blank=True, max_length=50, null=True), + ), + ("only_recorded_sample", models.BooleanField(default=False)), + ("generated_at", models.DateField(auto_now_add=True)), + ( + "samples_in_service", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="samples", + to="drylab.service", + ), + ), + ], + options={ + "db_table": "drylab_request_samples_in_services", + }, + ), + migrations.CreateModel( + name="ParameterPipeline", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("parameter_name", models.CharField(max_length=80)), + ( + "parameter_value", + models.CharField(blank=True, max_length=200, null=True), + ), + ( + "parameter_type", + models.CharField(blank=True, max_length=20, null=True), + ), + ( + "parameter_pipeline", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="drylab.pipelines", + ), + ), + ], + options={ + "db_table": "drylab_parameter_pipeline", + }, + ), + migrations.CreateModel( + name="Delivery", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "delivery_notes", + models.TextField(blank=True, max_length=1000, null=True), + ), + ("execution_start_date", models.DateField(blank=True, null=True)), + ("execution_end_date", models.DateField(blank=True, null=True)), + ( + "execution_time", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "permanent_used_space", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "temporary_used_space", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "delivery_resolution_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="delivery", + to="drylab.resolution", + ), + ), + ( + "pipelines_in_delivery", + models.ManyToManyField(blank=True, to="drylab.pipelines"), + ), + ], + options={ + "db_table": "drylab_delivery", + }, + ), + ] diff --git a/drylab/migrations/0002_remove_service_service_status.py b/drylab/migrations/0002_remove_service_service_status.py new file mode 100644 index 000000000..e36b7be5d --- /dev/null +++ b/drylab/migrations/0002_remove_service_service_status.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("drylab", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="service", + name="service_status", + ), + ] diff --git a/drylab/migrations/__init__.py b/drylab/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/drylab/models.py b/drylab/models.py index 678c93e2f..ab1a12b66 100644 --- a/drylab/models.py +++ b/drylab/models.py @@ -11,17 +11,6 @@ import core.models import drylab.config -# STATUS_CHOICES will be deprecated from release 2.4.0 or higher -STATUS_CHOICES = ( - ("recorded", _("Recorded")), - ("approved", _("Approved")), - ("rejected", _("Rejected")), - ("queued", _("Queued")), - ("in_progress", _("In Progress")), - ("delivered", _("Delivered")), - ("archived", _("Archived")), -) - class ServiceState(models.Model): state_value = models.CharField(max_length=50) @@ -224,7 +213,6 @@ def create_service(self, data): service_request_number=data["service_request_number"], service_request_int=data["service_request_int"], service_state=service_state_obj, - service_status="Recorded", service_notes=data["service_notes"], ) return new_service @@ -265,10 +253,6 @@ class Service(models.Model): service_run_specs = models.CharField( _("Run specifications"), max_length=10, blank=True, null=True ) - # Not changed in refactorization because it will be deprecated from next release - service_status = models.CharField( - _("Service status"), max_length=15, choices=STATUS_CHOICES - ) service_notes = models.TextField( _("Service Notes"), max_length=2048, null=True, blank=True ) @@ -323,7 +307,7 @@ def get_creation_date(self, format=True): else: return self.service_created_date - def get_delivery_date(self, format=True): + def get_delivered_date(self, format=True): if self.service_delivered_date: if format: return self.service_delivered_date.strftime("%Y-%m-%d") diff --git a/drylab/scripts/drylab_service_state_migration.py b/drylab/scripts/drylab_service_state_migration.py index 9a2222424..17b7336f4 100644 --- a/drylab/scripts/drylab_service_state_migration.py +++ b/drylab/scripts/drylab_service_state_migration.py @@ -1,7 +1,7 @@ from drylab.models import Service, ServiceState - """ + Upgrade: 2.3.0 -> 2.3.1 The script is applicable for the upgrade from 2.3.0 to 2.3.1. Service state that was defined as option choice in models,is replaced in version 2.3.1 as a primary key in SampleState table, given in this way diff --git a/drylab/templates/drylab/add_delivery.html b/drylab/templates/drylab/add_delivery.html index 7c83bcc3d..ccce5ef60 100644 --- a/drylab/templates/drylab/add_delivery.html +++ b/drylab/templates/drylab/add_delivery.html @@ -9,16 +9,14 @@
{% include 'registration/login_inline.html' %} - {% if ERROR %} + {% if error_message %}
-
-
-

Result of your request

-
+
+
ERROR
- {% for message in ERROR %} -

{{message}}

+ {% for values in error_message %} +

{{values}}

{% endfor %}
diff --git a/drylab/templates/drylab/add_in_progress.html b/drylab/templates/drylab/add_in_progress.html index e18bfd0df..5dcc4e9cb 100644 --- a/drylab/templates/drylab/add_in_progress.html +++ b/drylab/templates/drylab/add_in_progress.html @@ -8,16 +8,14 @@
{% include 'registration/login_inline.html' %} - {% if ERROR %} + {% if error_message %}
-
-
-

Result of your request

-
+
+
ERROR
- {% for message in ERROR %} -

{{message}}

+ {% for values in error_message %} +

{{values}}

{% endfor %}
diff --git a/drylab/templates/drylab/add_on_hold.html b/drylab/templates/drylab/add_on_hold.html index 24a4a86c0..e2840eba2 100644 --- a/drylab/templates/drylab/add_on_hold.html +++ b/drylab/templates/drylab/add_on_hold.html @@ -8,16 +8,14 @@
{% include 'registration/login_inline.html' %} - {% if ERROR %} + {% if error_message %}
-
-
-

Result of your request

-
+
+
ERROR
- {% for message in ERROR %} -

{{message}}

+ {% for values in error_message %} +

{{values}}

{% endfor %}
diff --git a/drylab/templates/drylab/add_resolution.html b/drylab/templates/drylab/add_resolution.html index 6fa79d5f0..9ac2a26f1 100644 --- a/drylab/templates/drylab/add_resolution.html +++ b/drylab/templates/drylab/add_resolution.html @@ -6,19 +6,19 @@
{% include 'registration/login_inline.html' %} - {% if ERROR %} -
-
-
-
-

Result of your request

-
-
- {% for message in ERROR %}

{{ message }}

{% endfor %} -
+ {% if error_message %} +
+
+
+
ERROR
+
+ {% for values in error_message %} +

{{values}}

+ {% endfor %}
+
{% endif %}
diff --git a/drylab/templates/drylab/search_service.html b/drylab/templates/drylab/search_service.html index 61e30823f..3af875d0f 100644 --- a/drylab/templates/drylab/search_service.html +++ b/drylab/templates/drylab/search_service.html @@ -1,226 +1,258 @@ {% extends 'core/base.html' %} {% load static %} - {% block content %} - -{% include "core/cdn_table_functionality.html"%} - -{% include "drylab/menu.html" %} - -
-
- {% include 'registration/login_inline.html' %} - - {% if ERROR %} -
-
-
-
-

Result of your request

-
-
- {% for message in ERROR %} -

{{message}}

- {% endfor %} + {% include "core/cdn_table_functionality.html" %} + {% include "drylab/menu.html" %} +
+
+ {% include 'registration/login_inline.html' %} + {% if ERROR %} +
+
+
+
+

Result of your request

+
+
+ {% for message in ERROR %}

{{ message }}

{% endfor %} +
+
-
-
- {% endif %} - {% if display_multiple_services %} -
-
-
-
- Services search results -
-
- - - - - - - - - - - - - - - {% for key, values in display_multiple_services.s_list.items %} - - {% for serviceID, status, dates ,center, projects in values %} - - - {% for date in dates %} - - {% endfor %} - - - {%endfor%} - - {%endfor%} - - - - - - - - - - - - - -
Service ID StatusRecorded DateApproved DateRejected DateDelivered DateCenterProject names
{{ serviceID }} {{ status }} {{date}}{{ center }} {% for project in projects %}{{ project }}
{% endfor %}
Service Request ID StatusRecorded DateApproved DateRejected DateDelivered DateCenterProject names
+ {% endif %} + {% if display_multiple_services %} +
+
+
+
Services search results
+
+ + + + + + + + + + + + + + + {% for key, values in display_multiple_services.s_list.items %} + + {% for serviceID, status, dates ,center, projects in values %} + + + {% for date in dates %}{% endfor %} + + + {% endfor %} + + {% endfor %} + + + + + + + + + + + + + +
Service IDStatusRecorded DateApproved DateRejected DateDelivered DateCenterProject names
+ {{ serviceID }} + {{ status }}{{ date }}{{ center }} + {% for project in projects %} + {{ project }} +
+ {% endfor %} +
Service Request IDStatusRecorded DateApproved DateRejected DateDelivered DateCenterProject names
+
+
-
-
- {% else %} - -
-
-
-
Service search
-
-
-
-
-
- {% csrf_token %} - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- {% if services_search_list.username %} - - {% else %} - - {% endif %} - -
-
- {% if services_search_list.username %} - - {% else %} - - {% endif %} - -
-
-
-
-
- {% if services_search_list.wetlab_app %} -
-
Search using wetlab information
-
+ {% else %} + +
+
+
+
+ Service search +
+
+ +
+
+
+ {% csrf_token %} + +
+ + +
+
+ + +
+
+ + +
- - + +
- - + +
- - + +
+ {% if services_search_list.username %} + + + {% else %} +
+ + +
+
+ + +
+ {% endif %} +
+
+
+
+ {% if services_search_list.wetlab_app %} +
+
Search using wetlab information
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ {% endif %} +
+
+
+
+ +
+
+
- {% endif %} -
-
-
-
- -
-
-
-
+
- -
+
+ {% endif %}
- {% endif %} -
-
-
- -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/drylab/templates/drylab/stats_services_time.html b/drylab/templates/drylab/stats_services_time.html index 8b9de7ce8..876e7738d 100644 --- a/drylab/templates/drylab/stats_services_time.html +++ b/drylab/templates/drylab/stats_services_time.html @@ -1,6 +1,7 @@ {% extends 'core/base.html' %} {% load static %} {% block content %} + {% include "core/cdn_table_functionality.html" %} {% include "core/graphic_chart_functionality.html" %} {% include "drylab/menu.html" %}
@@ -21,122 +22,382 @@
{{ error_message }}
{% endif %} {% if services_stats_info %} -
-
-
-
-

Services per user

-
-
- {% if services_stats_info.graphic_requested_services_per_user %} -
- {{ services_stats_info.graphic_requested_services_per_user |safe }} - {% endif %} -
+
+
-
-
-
-

Services status

+ + -
-
-
-
-

Services per classification area

-
-
- {% if services_stats_info.graphic_area_services %} -
- {{ services_stats_info.graphic_area_services |safe }} - {% endif %} + -
-
-
-
-

Services per center

+
+
+
+
+

Service Requests by center

+
+
+ {% if services_stats_info.graphic_center_services %} +
+ + {{ services_stats_info.graphic_center_services |safe }} + {% endif %} +
+
+
+
+
+
+

Service Requests by Classification Area

+
+
+ {% if services_stats_info.graphic_area_services %} +
+ {{ services_stats_info.graphic_area_services |safe }} + {% endif %} +
+
+
-
- {% if services_stats_info.graphic_center_services %} -
- - {{ services_stats_info.graphic_center_services |safe }} - {% endif %} +
+
+
+
+

Service Request Statistics by Center

+
+
+

{{ services_stats_info.period_time }}.

+ {% if services_stats_info.graphic_center_services_per_time %} +
+ {{ services_stats_info.graphic_center_services_per_time |safe }} + {% endif %} +
+
+
+
+
+
+

Service Request Statistics by Classification Area

+
+
+

{{ services_stats_info.period_time }}.

+ {% if services_stats_info.graphic_area_services_per_time %} +
+ + {{ services_stats_info.graphic_area_services_per_time |safe }} + {% endif %} +
+
+
-
-
-
-
-
-
-

Service statistics per center

+ -
-
-
-

Services Statistics per classification area

+ -
-
-
-
-
-

Level 2 available services

-
-
-

{{ services_stats_info.period_time }}.

- {% if services_stats_info.graphic_req_l2_services %} -
- {{ services_stats_info.graphic_req_l2_services |safe }} - {% endif %} + -
-
-
-
-
-
-

Level 3 Available Services

+ @@ -189,4 +450,28 @@

Services statistics

{% endif %} + {% endblock %} diff --git a/drylab/templatetags/upload_tags.py b/drylab/templatetags/upload_tags.py index 144bfe95f..b56046e73 100644 --- a/drylab/templatetags/upload_tags.py +++ b/drylab/templatetags/upload_tags.py @@ -6,8 +6,7 @@ @register.simple_tag def upload_js(): - return mark_safe( - """ + return mark_safe(""" - """ - ) + """) diff --git a/drylab/utils/deliveries.py b/drylab/utils/deliveries.py index ee5432df0..03cb4bc00 100644 --- a/drylab/utils/deliveries.py +++ b/drylab/utils/deliveries.py @@ -1,5 +1,6 @@ # Generic imports import datetime +from smtplib import SMTPException import django.core.mail @@ -29,15 +30,15 @@ def prepare_delivery_form(resolution_id): resolution_id, input="id" ) if resolution_obj is not None: - delivery_data_form[ - "available_services" - ] = resolution_obj.get_available_services_ids() + delivery_data_form["available_services"] = ( + resolution_obj.get_available_services_ids() + ) delivery_data_form["resolution_id"] = resolution_id delivery_data_form["resolution_number"] = resolution_obj.get_resolution_number() - delivery_data_form[ - "pipelines_data" - ] = drylab.utils.pipelines.get_pipelines_for_resolution(resolution_obj) + delivery_data_form["pipelines_data"] = ( + drylab.utils.pipelines.get_pipelines_for_resolution(resolution_obj) + ) return delivery_data_form @@ -133,6 +134,6 @@ def send_delivery_service_email(email_data): to_users = [email_data["user_email"], email_data["user_email"], notification_user] try: django.core.mail.send_mail(subject, body_message, from_user, to_users) - except Exception: - pass + except (SMTPException, ConnectionRefusedError): + raise return diff --git a/drylab/utils/pipelines.py b/drylab/utils/pipelines.py index 1ef2f04d6..bd757d8c8 100644 --- a/drylab/utils/pipelines.py +++ b/drylab/utils/pipelines.py @@ -101,9 +101,9 @@ def get_detail_pipeline_data(pipeline_id): pipeline_obj = drylab.models.Pipelines.objects.get(pk__exact=pipeline_id) detail_pipelines_data["pipeline_name"] = pipeline_obj.get_pipeline_name() detail_pipelines_data["pipeline_basic"] = pipeline_obj.get_pipeline_basic() - detail_pipelines_data[ - "pipeline_basic_heading" - ] = drylab.config.DISPLAY_DETAIL_PIPELINE_BASIC_INFO + detail_pipelines_data["pipeline_basic_heading"] = ( + drylab.config.DISPLAY_DETAIL_PIPELINE_BASIC_INFO + ) detail_pipelines_data["pipeline_additional_data"] = zip( drylab.config.DISPLAY_DETAIL_PIPELINE_ADDITIONAL_INFO, pipeline_obj.get_pipeline_additional(), @@ -116,9 +116,9 @@ def get_detail_pipeline_data(pipeline_id): parameter_objs = drylab.models.ParameterPipeline.objects.filter( parameter_pipeline=pipeline_obj ) - detail_pipelines_data[ - "parameter_heading" - ] = drylab.config.HEADING_PARAMETER_PIPELINE + detail_pipelines_data["parameter_heading"] = ( + drylab.config.HEADING_PARAMETER_PIPELINE + ) detail_pipelines_data["parameters"] = [] for parameter_obj in parameter_objs: detail_pipelines_data["parameters"].append( @@ -187,9 +187,9 @@ def get_pipelines_for_resolution(resolution_obj): for pipeline_obj in pipeline_objs: pipeline_data["pipelines"].append(pipeline_obj.get_pipeline_info()) if len(pipeline_data["pipelines"]) > 0: - pipeline_data[ - "heading_pipelines" - ] = drylab.config.DISPLAY_PIPELINES_USED_IN_RESOLUTION + pipeline_data["heading_pipelines"] = ( + drylab.config.DISPLAY_PIPELINES_USED_IN_RESOLUTION + ) return pipeline_data diff --git a/drylab/utils/req_services.py b/drylab/utils/req_services.py index e635bee8c..8398a55f3 100644 --- a/drylab/utils/req_services.py +++ b/drylab/utils/req_services.py @@ -183,15 +183,25 @@ def delete_samples_in_service(sample_list): return deleted_sample_names -def get_children_services(all_tree_services): - """ - Description: - The function get the children available services from a query of service - Input: - all_tree_services # queryset of available service - Return: - children_service +def get_children_services( + all_tree_services: list[drylab.models.AvailableService] = None, +) -> list[list]: + """The function get a list with the children available services from the + requested query of available service. If no query is provided, it will + return all the children services from the database + + Args: + all_tree_services (list[drylab.models.AvailableService]): Queryset of + available service. Defaults to None. + + Returns: + list[list]: List with 2 values: the primary key and the description for + each children service """ + if all_tree_services is None: + all_tree_services = drylab.models.AvailableService.objects.all().order_by( + "description" + ) children_services = [] for t_services in all_tree_services: if t_services.get_children(): @@ -357,9 +367,9 @@ def get_pending_services_info(): graphic_unit_pending_services = core.fusioncharts.fusioncharts.FusionCharts( "multilevelpie", "ex2", "535", "435", "chart-2", "json", data_source ) - pending_services_graphics[ - "graphic_pending_unit_services" - ] = graphic_unit_pending_services.render() + pending_services_graphics["graphic_pending_unit_services"] = ( + graphic_unit_pending_services.render() + ) pending_services_details["graphics"] = pending_services_graphics return pending_services_details @@ -391,9 +401,9 @@ def get_user_pending_services_info(user_name): del resolution_data[4] res_in_queued.append(resolution_data) user_pending_services_details["queued"] = res_in_queued - user_pending_services_details[ - "heading_in_queued" - ] = drylab.config.HEADING_USER_PENDING_SERVICE_QUEUED + user_pending_services_details["heading_in_queued"] = ( + drylab.config.HEADING_USER_PENDING_SERVICE_QUEUED + ) if drylab.models.Resolution.objects.filter( resolution_assigned_user__username__exact=user_name, resolution_state__state_value__exact="in_progress", @@ -407,9 +417,9 @@ def get_user_pending_services_info(user_name): del resolution_data[4] res_in_progress.append(resolution_data) user_pending_services_details["in_progress"] = res_in_progress - user_pending_services_details[ - "heading_in_progress" - ] = drylab.config.HEADING_USER_PENDING_SERVICE_QUEUED + user_pending_services_details["heading_in_progress"] = ( + drylab.config.HEADING_USER_PENDING_SERVICE_QUEUED + ) return user_pending_services_details @@ -492,26 +502,26 @@ def get_service_data(request): # get samples which have sequencing data in iSkyLIMS user_sharing_list = drylab.utils.common.get_user_sharing_list(request.user) - service_data[ - "samples_data" - ] = wetlab.utils.api.wetlab_api.get_runs_projects_samples_and_dates( - user_sharing_list + service_data["samples_data"] = ( + wetlab.utils.api.wetlab_api.get_runs_projects_samples_and_dates( + user_sharing_list + ) ) if len(service_data["samples_data"]) > 0: - service_data[ - "samples_heading" - ] = drylab.config.HEADING_SELECT_SAMPLE_IN_SERVICE + service_data["samples_heading"] = ( + drylab.config.HEADING_SELECT_SAMPLE_IN_SERVICE + ) # get the samples that are only defined without sequencing data available from iSkyLIMS - service_data[ - "sample_only_recorded" - ] = core.utils.samples.get_only_recorded_samples_and_dates() + service_data["sample_only_recorded"] = ( + core.utils.samples.get_only_recorded_samples_and_dates() + ) if len(service_data["sample_only_recorded"]) > 0: - service_data[ - "sample_only_recorded_heading" - ] = drylab.config.HEADING_SELECT_ONLY_RECORDED_SAMPLE_IN_SERVICE + service_data["sample_only_recorded_heading"] = ( + drylab.config.HEADING_SELECT_ONLY_RECORDED_SAMPLE_IN_SERVICE + ) return service_data diff --git a/drylab/utils/resolutions.py b/drylab/utils/resolutions.py index a914e069f..472007ab9 100644 --- a/drylab/utils/resolutions.py +++ b/drylab/utils/resolutions.py @@ -216,10 +216,10 @@ def create_new_resolution(resolution_data_form): .get_resolution_full_number() ) else: - resolution_data_form[ - "resolution_full_number" - ] = get_assign_resolution_full_number( - resolution_data_form["service_id"], resolution_data_form["acronym"] + resolution_data_form["resolution_full_number"] = ( + get_assign_resolution_full_number( + resolution_data_form["service_id"], resolution_data_form["acronym"] + ) ) resolution_data_form["resolution_number"] = create_resolution_number( resolution_data_form["service_id"] @@ -355,9 +355,9 @@ def prepare_form_data_add_resolution(form_data): existing_resolution = drylab.models.Resolution.objects.filter( resolution_service_id=service_obj ).last() - resolution_form_data[ - "resolution_full_number" - ] = existing_resolution.get_resolution_full_number() + resolution_form_data["resolution_full_number"] = ( + existing_resolution.get_resolution_full_number() + ) users = django.contrib.auth.models.User.objects.filter( groups__name=drylab.config.SERVICE_MANAGER ) @@ -376,12 +376,12 @@ def prepare_form_data_add_resolution(form_data): for req_service in req_available_services_with_desc: req_available_services_id.append(req_service[0]) - resolution_form_data[ - "pipelines_data" - ] = drylab.utils.pipelines.get_all_defined_pipelines(True) - resolution_form_data[ - "pipelines_heading" - ] = drylab.config.HEADING_PIPELINES_SELECTION_IN_RESOLUTION + resolution_form_data["pipelines_data"] = ( + drylab.utils.pipelines.get_all_defined_pipelines(True) + ) + resolution_form_data["pipelines_heading"] = ( + drylab.config.HEADING_PIPELINES_SELECTION_IN_RESOLUTION + ) return resolution_form_data @@ -404,7 +404,7 @@ def send_resolution_creation_email(email_data): subject_tmp = drylab.config.SUBJECT_RESOLUTION_QUEUED.copy() subject_tmp.insert(1, email_data["service_number"]) subject = " ".join(subject_tmp) - if email_data["status"] == "Accepted": + if email_data["status"] == "accepted": date = email_data["date"].strftime("%d %B, %Y") body_preparation = list( map( @@ -466,8 +466,8 @@ def send_resolution_creation_email(email_data): ] try: django.core.mail.send_mail(subject, body_message, from_user, to_users) - except SMTPException: - pass + except (SMTPException, ConnectionRefusedError): + raise return @@ -514,8 +514,8 @@ def send_resolution_in_progress_email(email_data): to_users = [email_data["user_email"], notification_user] try: django.core.mail.send_mail(subject, body_message, from_user, to_users) - except SMTPException: - pass + except (SMTPException, ConnectionRefusedError): + raise return @@ -562,8 +562,8 @@ def send_resolution_on_hold_email(email_data): to_users = [email_data["user_email"], notification_user] try: django.core.mail.send_mail(subject, body_message, from_user, to_users) - except SMTPException: - pass + except (SMTPException, ConnectionRefusedError): + raise return diff --git a/drylab/utils/stats.py b/drylab/utils/stats.py index 1df1d8768..344f6afaa 100644 --- a/drylab/utils/stats.py +++ b/drylab/utils/stats.py @@ -39,9 +39,9 @@ def create_statistics_by_user(user_id, start_date=None, end_date=None): service_objs = service_objs.filter(service_user_id__pk__exact=user_id) if len(service_objs) == 0: - stats_info[ - "ERROR" - ] = drylab.config.ERROR_NO_MATCHES_FOUND_FOR_YOUR_SERVICE_SEARCH + stats_info["ERROR"] = ( + drylab.config.ERROR_NO_MATCHES_FOUND_FOR_YOUR_SERVICE_SEARCH + ) return stats_info # create table of services @@ -103,17 +103,17 @@ def create_statistics_by_user(user_id, start_date=None, end_date=None): "Percentage of services", "Research vs all", "ocean", service_user ) - stats_info[ - "research_vs_other_graphic" - ] = core.fusioncharts.fusioncharts.FusionCharts( - "pie3d", - "research_vs_other_graph", - "580", - "300", - "research_vs_other_chart", - "json", - g_data, - ).render() + stats_info["research_vs_other_graphic"] = ( + core.fusioncharts.fusioncharts.FusionCharts( + "pie3d", + "research_vs_other_graph", + "580", + "300", + "research_vs_other_chart", + "json", + g_data, + ).render() + ) # getting statistics of the created services per week service_per_week = ( @@ -133,17 +133,17 @@ def create_statistics_by_user(user_id, start_date=None, end_date=None): "ocean", format_service_per_week, ) - stats_info[ - "research_service_weeks_graphic" - ] = core.fusioncharts.fusioncharts.FusionCharts( - "column3d", - "research_service_weeks_graph", - "580", - "400", - "research_service_weeks_chart", - "json", - g_data, - ).render() + stats_info["research_service_weeks_graphic"] = ( + core.fusioncharts.fusioncharts.FusionCharts( + "column3d", + "research_service_weeks_graph", + "580", + "400", + "research_service_weeks_chart", + "json", + g_data, + ).render() + ) # create graphic for requested service on level 2 avail_serv_level_2 = drylab.models.AvailableService.objects.filter(level=2) @@ -164,16 +164,16 @@ def create_statistics_by_user(user_id, start_date=None, end_date=None): "value", ) - stats_info[ - "research_avail_services_graphic" - ] = core.fusioncharts.fusioncharts.FusionCharts( - "column3d", - "research_avail_services_graph", - "580", - "400", - "research_avail_services_chart", - "json", - g_data, - ).render() + stats_info["research_avail_services_graphic"] = ( + core.fusioncharts.fusioncharts.FusionCharts( + "column3d", + "research_avail_services_graph", + "580", + "400", + "research_avail_services_chart", + "json", + g_data, + ).render() + ) return stats_info diff --git a/drylab/views.py b/drylab/views.py index 8f5db9411..617b3aea6 100644 --- a/drylab/views.py +++ b/drylab/views.py @@ -3,18 +3,20 @@ import json import os from datetime import date, datetime +from smtplib import SMTPException import django.contrib.auth.models from django.conf import settings from django.contrib.auth.decorators import login_required from django.core.files.storage import FileSystemStorage -from django.db.models import Prefetch +from django.db.models import Count, Prefetch from django.http import HttpResponse from django.shortcuts import redirect, render # Local imports import core.fusioncharts.fusioncharts import core.utils.common +import core.utils.graphics import django_utils.models import drylab.config import drylab.models @@ -46,25 +48,33 @@ def index(request): s_info.append(r_service_obj.get_user_name()) service_list["recorded"].append(s_info) - if ( - drylab.models.Service.objects.all() - .exclude(service_state__state_value__exact="delivered") - .exclude(service_approved_date=None) - .exists() - ): - ongoing_services_objs = ( - drylab.models.Service.objects.all() - .exclude(service_state__state_value__exact="delivered") - .exclude(service_approved_date=None) - .order_by("service_approved_date") + # Fetch the excluded states + excluded_states = ["delivered", "rejected", "archived"] + + # Check if there are ongoing resolutions + if drylab.models.Resolution.objects.exclude( + resolution_state__state_value__in=excluded_states + ).exists(): + # Get resolutions excluding delivered, rejected, and archived + ongoing_resolutions = ( + drylab.models.Resolution.objects.exclude( + resolution_state__state_value__in=excluded_states + ) + .select_related( + "resolution_state", "resolution_service_id" + ) # Optimize DB joins + .order_by("resolution_estimated_date") # Order by estimated delivery date ) + service_list["ongoing"] = [] - for ongoing_services_obj in ongoing_services_objs: - s_info = [] - s_info.append(ongoing_services_obj.get_identifier()) - s_info.append(ongoing_services_obj.get_delivery_date()) + for resolution in ongoing_resolutions: + s_info = [ + resolution.get_identifier(), # Keep service identifier from resolution + resolution.get_resolution_estimated_date(), # Use estimated delivery date + ] service_list["ongoing"].append(s_info) + org_name = drylab.utils.common.get_configuration_from_database("ORGANIZATION_NAME") return render( @@ -79,9 +89,9 @@ def configuration_email(request): if request.user.username != "admin": return redirect("/wetlab") email_conf_data = core.utils.common.get_email_data() - email_conf_data[ - "EMAIL_ISKYLIMS" - ] = drylab.utils.common.get_configuration_from_database("EMAIL_FOR_NOTIFICATIONS") + email_conf_data["EMAIL_ISKYLIMS"] = ( + drylab.utils.common.get_configuration_from_database("EMAIL_FOR_NOTIFICATIONS") + ) if request.method == "POST" and (request.POST["action"] == "emailconfiguration"): result_email = core.utils.common.send_test_email(request.POST) if result_email != "OK": @@ -488,10 +498,12 @@ def search_service(request): for center in center_availables: center_list_abbr.append(center.center_abbr) services_search_list["centers"] = center_list_abbr - services_search_list[ - "states" - ] = drylab.utils.req_services.get_available_service_states(True) - + services_search_list["states"] = ( + drylab.utils.req_services.get_available_service_states(True) + ) + services_search_list["available_services"] = ( + drylab.utils.req_services.get_children_services() + ) if "wetlab" in settings.INSTALLED_APPS: services_search_list["wetlab_app"] = True @@ -504,6 +516,7 @@ def search_service(request): start_date = request.POST["start_date"] end_date = request.POST["end_date"] center = request.POST["service_center"] + available_services = request.POST["available_services"] service_user = request.POST["service_user"] assigned_user = request.POST["bioinfo_user"] @@ -519,6 +532,7 @@ def search_service(request): if ( service_id == "" and service_state == "" + and available_services == "" and start_date == "" and end_date == "" and center == "" @@ -575,9 +589,14 @@ def search_service(request): }, ) else: - services_found = drylab.models.Service.objects.prefetch_related( - "service_user_id", "service_state" - ).all() + if available_services != "": + services_found = drylab.models.Service.objects.prefetch_related( + "service_user_id", "service_state" + ).filter(service_available_service__id__exact=available_services) + else: + services_found = drylab.models.Service.objects.prefetch_related( + "service_user_id", "service_state" + ).all() if service_state != "": services_found = services_found.filter( @@ -774,10 +793,22 @@ def add_on_hold(request): email_data["user_email"] = service_obj.get_user_email() email_data["user_name"] = service_obj.get_user_name() email_data["resolution_number"] = resolution_number - drylab.utils.resolutions.send_resolution_in_progress_email(email_data) + on_hold_resolution = {} on_hold_resolution["resolution_number"] = resolution_number + try: + drylab.utils.resolutions.send_resolution_on_hold_email(email_data) + except (SMTPException, ConnectionRefusedError): + return render( + request, + "drylab/add_on_hold.html", + { + "on_hold_resolution": on_hold_resolution, + "error_message": ["Unable to send confirmation email."], + }, + ) + return render( request, "drylab/add_on_hold.html", @@ -832,12 +863,23 @@ def add_resolution(request): email_data["date"] = resolution_data_form["resolution_estimated_date"] # include the email for the user who requested the service email_data["service_owner_email"] = new_resolution.get_service_owner_email() - drylab.utils.resolutions.send_resolution_creation_email(email_data) + created_resolution = {} created_resolution["resolution_number"] = resolution_data_form[ "resolution_number" ] - # Display pipeline parameters + + try: + drylab.utils.resolutions.send_resolution_creation_email(email_data) + except (SMTPException, ConnectionRefusedError): + return render( + request, + "drylab/add_resolution.html", + { + "created_resolution": created_resolution, + "error_message": ["Unable to send confirmation email."], + }, + ) return render( request, @@ -915,16 +957,29 @@ def add_in_progress(request): if drylab.utils.resolutions.check_allow_service_update( resolution_obj, "in_progress" ): - # update the service status and in_porgress date + # update the service status and in_progress date service_obj = service_obj.update_state("in_progress") email_data = {} email_data["user_email"] = service_obj.get_user_email() email_data["user_name"] = service_obj.get_user_name() email_data["resolution_number"] = resolution_number - drylab.utils.resolutions.send_resolution_in_progress_email(email_data) + in_progress_resolution = {} in_progress_resolution["resolution_number"] = resolution_number + + try: + drylab.utils.resolutions.send_resolution_in_progress_email(email_data) + except (SMTPException, ConnectionRefusedError): + return render( + request, + "drylab/add_in_progress.html", + { + "in_progress_resolution": in_progress_resolution, + "error_message": ["Unable to send confirmation email."], + }, + ) + return render( request, "drylab/add_in_progress.html", @@ -1008,13 +1063,25 @@ def add_delivery(request): email_data["user_name"] = request.user.username email_data["resolution_number"] = delivery_recorded["resolution_number"] email_data["service_owner_email"] = resolution_obj.get_service_owner_email() - drylab.utils.deliveries.send_delivery_service_email(email_data) + if drylab.utils.resolutions.check_allow_service_update( resolution_obj, "delivered" ): service_obj = resolution_obj.get_service_obj() service_obj = service_obj.update_state("delivered") service_obj.update_delivered_date(date.today()) + + try: + drylab.utils.deliveries.send_delivery_service_email(email_data) + except (SMTPException, ConnectionRefusedError): + return render( + request, + "drylab/add_delivery.html", + { + "delivery_recorded": delivery_recorded, + "error_message": ["Unable to send confirmation email."], + }, + ) return render( request, "drylab/add_delivery.html", @@ -1130,6 +1197,8 @@ def stats_by_services_request(request): if request.method == "POST" and request.POST["action"] == "service_statistics": start_date = request.POST["start_date"] end_date = request.POST["end_date"] + if start_date == "" and end_date == "": + return render(request, "drylab/stats_services_time.html") if start_date != "" and not drylab.utils.common.check_valid_date_format( start_date ): @@ -1159,12 +1228,10 @@ def stats_by_services_request(request): else: user_services[user] = 1 - period_of_time_selected = str( - " For the period between " + start_date + " and " + end_date - ) + period_of_time_selected = str(" From " + start_date + " to " + end_date) # creating the graphic for requested services data_source = drylab.utils.graphics.column_graphic_dict( - "Requested Services by:", + "Service request by user", period_of_time_selected, "User names", "Number of Services", @@ -1172,11 +1239,11 @@ def stats_by_services_request(request): user_services, ) graphic_requested_services = core.fusioncharts.fusioncharts.FusionCharts( - "column3d", "ex1", "525", "350", "chart-1", "json", data_source + "column3d", "ex1", "900", "350", "chart-1", "json", data_source + ) + services_stats_info["graphic_requested_services_per_user"] = ( + graphic_requested_services.render() ) - services_stats_info[ - "graphic_requested_services_per_user" - ] = graphic_requested_services.render() # preparing stats for status of the services status_services = {} @@ -1189,7 +1256,7 @@ def stats_by_services_request(request): # creating the graphic for status services data_source = drylab.utils.graphics.graphic_3D_pie( - "Status of Requested Services", + "Service request status", period_of_time_selected, "", "", @@ -1198,12 +1265,12 @@ def stats_by_services_request(request): ) graphic_status_requested_services = ( core.fusioncharts.fusioncharts.FusionCharts( - "pie3d", "ex2", "525", "350", "chart-2", "json", data_source + "pie3d", "ex2", "500", "400", "chart-2", "json", data_source ) ) - services_stats_info[ - "graphic_status_requested_services" - ] = graphic_status_requested_services.render() + services_stats_info["graphic_status_requested_services"] = ( + graphic_status_requested_services.render() + ) # preparing stats for request by Area user_area_dict = {} @@ -1225,7 +1292,7 @@ def stats_by_services_request(request): # creating the graphic for areas data_source = drylab.utils.graphics.column_graphic_dict( - "Services requested per Area", + "Service requests by area", period_of_time_selected, "Area ", "Number of Services", @@ -1235,9 +1302,9 @@ def stats_by_services_request(request): graphic_area_services = core.fusioncharts.fusioncharts.FusionCharts( "column3d", "ex3", "600", "350", "chart-3", "json", data_source ) - services_stats_info[ - "graphic_area_services" - ] = graphic_area_services.render() + services_stats_info["graphic_area_services"] = ( + graphic_area_services.render() + ) # preparing stats for services request by Center user_center_dict = {} @@ -1257,7 +1324,7 @@ def stats_by_services_request(request): user_center_dict[user_center] = 1 # creating the graphic for areas data_source = drylab.utils.graphics.column_graphic_dict( - "Services requested per Center", + "Services requests by center", period_of_time_selected, "Center ", "Number of Services", @@ -1267,9 +1334,9 @@ def stats_by_services_request(request): graphic_center_services = core.fusioncharts.fusioncharts.FusionCharts( "column3d", "ex4", "600", "350", "chart-4", "json", data_source ) - services_stats_info[ - "graphic_center_services" - ] = graphic_center_services.render() + services_stats_info["graphic_center_services"] = ( + graphic_center_services.render() + ) ################################################ # Preparing the statistics per period of time @@ -1314,7 +1381,7 @@ def stats_by_services_request(request): if d_period not in user_services_period[center]: user_services_period[center][d_period] = 0 data_source = drylab.utils.graphics.column_graphic_per_time( - "Services requested by center ", + "Service requests by center and period of time", period_of_time_selected, "date", "number of services", @@ -1326,9 +1393,9 @@ def stats_by_services_request(request): "mscolumn3d", "ex5", "525", "350", "chart-5", "json", data_source ) ) - services_stats_info[ - "graphic_center_services_per_time" - ] = graphic_center_services_per_time.render() + services_stats_info["graphic_center_services_per_time"] = ( + graphic_center_services_per_time.render() + ) # Preparing the statistics for Area on period of time user_area_services_period = {} @@ -1341,9 +1408,9 @@ def stats_by_services_request(request): ).exists(): user_area = django_utils.models.Profile.objects.get( profile_user_id=user_id - ).profile_area + ).get_clasification_area() else: - user_center = "Not defined" + user_area = "Not defined" if date_service not in time_values_dict: time_values_dict[date_service] = 1 if user_area in user_area_services_period: @@ -1364,7 +1431,7 @@ def stats_by_services_request(request): user_area_services_period[area][d_period] = 0 data_source = drylab.utils.graphics.column_graphic_per_time( - "Services requested by Area ", + "Service requests by Area ", period_of_time_selected, "date", "number of services", @@ -1376,38 +1443,101 @@ def stats_by_services_request(request): "mscolumn3d", "ex6", "525", "350", "chart-6", "json", data_source ) ) - services_stats_info[ - "graphic_area_services_per_time" - ] = graphic_area_services_per_time.render() + services_stats_info["graphic_area_services_per_time"] = ( + graphic_area_services_per_time.render() + ) services_stats_info["period_time"] = period_of_time_selected + # collecting services, samples and re-analysis data for creating # statistics on Requested Level 2 Services - + # service_dict = {} + sample_in_l2 = {} + re_analysis_l2 = {} + sample_re_analysis_l2 = {} for service in services_found: service_request_list = service.service_available_service.filter(level=2) for service_requested in service_request_list: service_name = service_requested.avail_service_description - if service_name in service_dict: - service_dict[service_name] += 1 - else: - service_dict[service_name] = 1 + service_dict[service_name] = service_dict.get(service_name, 0) + 1 + + # count the number of samples handled on level 2 services + s_count = drylab.models.RequestedSamplesInServices.objects.filter( + samples_in_service=service + ).count() + sample_in_l2[service_name] = ( + sample_in_l2.get(service_name, 0) + s_count + ) + # check if there are more than one resolution for the same + # service, to be included as re-analysis + resolution_count = drylab.models.Resolution.objects.filter( + resolution_service_id=service + ).count() + if resolution_count > 1: + # reduce the number of services requested because the + # first resolution is the requested service + resolution_count -= 1 + # increase the number of re-analysis services + re_analysis_l2[service_name] = ( + re_analysis_l2.get(service_name, 0) + resolution_count + ) + # count the number of samples handled on re-analysis services + # these number is multiplied by the number of resolutions + sample_re_analysis_l2[service_name] = ( + sample_re_analysis_l2.get(service_name, 0) + + s_count * resolution_count + ) # creating the graphic for requested services data_source = drylab.utils.graphics.column_graphic_dict( "Requested Services:", "level 2 ", "", "", "fint", service_dict ) graphic_req_l2_services = core.fusioncharts.fusioncharts.FusionCharts( - "column3d", "ex7", "800", "375", "chart-7", "json", data_source + "column3d", "ex7", "600", "375", "chart-7", "json", data_source + ) + services_stats_info["graphic_req_l2_services"] = ( + graphic_req_l2_services.render() + ) + # creating graphic for samples handled on level 2 services + data_source = drylab.utils.graphics.column_graphic_dict( + "Sample per Services:", "level 2 ", "", "", "ocean", sample_in_l2 + ) + graphic_sample_service_l2 = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex13", "1100", "375", "chart-13", "json", data_source + ) + services_stats_info["graphic_sample_per_service_l2"] = ( + graphic_sample_service_l2.render() + ) + # creating the graphic for requested re-analysis services + data_source = drylab.utils.graphics.column_graphic_dict( + "Reanalysis Services:", "level 2 ", "", "", "fint", re_analysis_l2 + ) + graphic_re_analysis_l2_services = ( + core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex15", "550", "375", "chart-15", "json", data_source + ) + ) + services_stats_info["graphic_re_analysis_l2_services"] = ( + graphic_re_analysis_l2_services.render() + ) + # creating the graphic for re-analysis sample services + data_source = drylab.utils.graphics.column_graphic_dict( + "Reanalysis Samples", "level 2 ", "", "", "fint", sample_re_analysis_l2 + ) + graphic_sample_re_analysis_l2 = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex16", "550", "375", "chart-16", "json", data_source + ) + services_stats_info["graphic_sample_re_analysis_service_l2"] = ( + graphic_sample_re_analysis_l2.render() ) - services_stats_info[ - "graphic_req_l2_services" - ] = graphic_req_l2_services.render() # statistics on Requested Level 3 Services - + # getting also the number of samples handled on level 3 services service_dict = {} + sample_in_l3 = {} + re_analysis_l3 = {} + sample_re_analysis_l3 = {} for service in services_found: service_request_list = service.service_available_service.filter(level=3) for service_requested in service_request_list: @@ -1416,18 +1546,177 @@ def stats_by_services_request(request): service_dict[service_name] += 1 else: service_dict[service_name] = 1 - - # creating the graphic for requested services + # count the number of samples handled on level 3 services + s_count = drylab.models.RequestedSamplesInServices.objects.filter( + samples_in_service=service + ).count() + sample_in_l3[service_name] = ( + sample_in_l3.get(service_name, 0) + s_count + ) + # check if there are more than one resolution for the same + # service, to be included as re-analysis + resolution_count = drylab.models.Resolution.objects.filter( + resolution_service_id=service + ).count() + if resolution_count > 1: + # reduce the number of services requested because the + # first resolution is the requested service + resolution_count -= 1 + # increase the number of re-analysis services + re_analysis_l3[service_name] = ( + re_analysis_l3.get(service_name, 0) + resolution_count + ) + # count the number of samples handled on re-analysis services + # these number is multiplied by the number of resolutions + sample_re_analysis_l3[service_name] = ( + sample_re_analysis_l3.get(service_name, 0) + + s_count * resolution_count + ) + + # creating the graphic for requested services on level 3 data_source = drylab.utils.graphics.column_graphic_dict( "Requested Services:", "level 3 ", "", "", "fint", service_dict ) graphic_req_l3_services = core.fusioncharts.fusioncharts.FusionCharts( - "column3d", "ex8", "800", "375", "chart-8", "json", data_source + "column3d", "ex8", "1200", "375", "chart-8", "json", data_source + ) + services_stats_info["graphic_req_l3_services"] = ( + graphic_req_l3_services.render() + ) + # creating graphic for samples handled on level 3 services + data_source = drylab.utils.graphics.column_graphic_dict( + "Sample per Services:", "level 3 ", "", "", "fint", sample_in_l3 + ) + graphic_sample_service_l3 = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex14", "1100", "375", "chart-14", "json", data_source + ) + services_stats_info["graphic_sample_per_service_l3"] = ( + graphic_sample_service_l3.render() + ) + # creating the graphic for requested re-analysis l3 services + data_source = drylab.utils.graphics.column_graphic_dict( + "Reanalysis Services:", "level 3 ", "", "", "ocean", re_analysis_l3 + ) + graphic_re_analysis_l3_services = ( + core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex17", "550", "375", "chart-17", "json", data_source + ) + ) + services_stats_info["graphic_re_analysis_l3_services"] = ( + graphic_re_analysis_l3_services.render() + ) + # creating the graphic for re-analysis sample l3 services + data_source = drylab.utils.graphics.column_graphic_dict( + "Reanalysis Samples", + "level 3 ", + "", + "", + "ocean", + sample_re_analysis_l3, + ) + graphic_sample_re_analysis_l3 = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex18", "550", "375", "chart-18", "json", data_source + ) + services_stats_info["graphic_sample_re_analysis_service_l3"] = ( + graphic_sample_re_analysis_l3.render() + ) + + # Samples handled by requested services + sample_in_services_objs = ( + drylab.models.RequestedSamplesInServices.objects.filter( + samples_in_service__in=services_found + ) + ) + ana_sample_in_runs = ( + sample_in_services_objs.exclude(run_name=None) + .values("run_name") + .annotate(sample_count=Count("sample_name")) + ) + g_data = core.utils.graphics.preparation_graphic_data( + "Analyzed Samples from sequencing runs", + "", + "", + "", + "ocean", + ana_sample_in_runs, + "run_name", + "sample_count", + ) + graphic_req_samples_run = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex9", "600", "375", "chart-9", "json", g_data + ) + services_stats_info["graphic_samples_per_run"] = ( + graphic_req_samples_run.render() + ) + ana_sample_in_proj = ( + sample_in_services_objs.exclude(project_name=None) + .values("project_name") + .annotate(sample_count=Count("sample_name")) + ) + g_data = core.utils.graphics.preparation_graphic_data( + "Analyzed samples by project", + "", + "", + "", + "ocean", + ana_sample_in_proj, + "project_name", + "sample_count", + ) + graphic_req_samples_proj = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex10", "600", "375", "chart-10", "json", g_data + ) + services_stats_info["graphic_samples_per_project"] = ( + graphic_req_samples_proj.render() + ) + ana_sample_in_user = sample_in_services_objs.values( + "samples_in_service__service_user_id__username" + ).annotate(sample_count=Count("sample_name")) + g_data = core.utils.graphics.preparation_graphic_data( + "Analyzed samples by user", + "", + "", + "", + "ocean", + ana_sample_in_user, + "samples_in_service__service_user_id__username", + "sample_count", ) - services_stats_info[ - "graphic_req_l3_services" - ] = graphic_req_l3_services.render() + graphic_req_samples_user = core.fusioncharts.fusioncharts.FusionCharts( + "column3d", "ex11", "600", "375", "chart-11", "json", g_data + ) + services_stats_info["graphic_samples_per_user"] = ( + graphic_req_samples_user.render() + ) + analyzed_samples = {} + analyzed_samples["Sequenced samples"] = sample_in_services_objs.exclude( + only_recorded_sample=True + ).count() + analyzed_samples["Only recorded samples"] = sample_in_services_objs.exclude( + only_recorded_sample=False + ).count() + data_source = drylab.utils.graphics.graphic_3D_pie( + "Sample Analysis", + period_of_time_selected, + "", + "", + "fint", + analyzed_samples, + ) + graphic_analyzed_samples = core.fusioncharts.fusioncharts.FusionCharts( + "pie3d", "ex12", "600", "400", "chart-12", "json", data_source + ) + services_stats_info["graphic_analyzed_samples"] = ( + graphic_analyzed_samples.render() + ) + services_stats_info["table_samples"] = sample_in_services_objs.values_list( + "sample_name", + "samples_in_service__service_request_number", + "samples_in_service__pk", + "samples_in_service__service_user_id__username", + "samples_in_service__service_state__state_display", + ) return render( request, "drylab/stats_services_time.html", @@ -1489,9 +1778,9 @@ def configuration_test(request): test_results["services"] = ("Available services", "NOK") else: test_results["services"] = ("Available services", "OK") - test_results[ - "iSkyLIMS_settings" - ] = drylab.utils.test_conf.get_iSkyLIMS_settings() + test_results["iSkyLIMS_settings"] = ( + drylab.utils.test_conf.get_iSkyLIMS_settings() + ) test_results["config_file"] = drylab.utils.test_conf.get_config_file( config_file ) @@ -1528,10 +1817,10 @@ def configuration_test(request): else: resolution_results["create_service_ok"] = "OK" resolution_number = "SRVTEST-IIER001.1" - resolution_results[ - "resolution_test" - ] = drylab.utils.test_conf.create_resolution_test( - resolution_number, service_requested + resolution_results["resolution_test"] = ( + drylab.utils.test_conf.create_resolution_test( + resolution_number, service_requested + ) ) resolution_results["create_resolution_ok"] = "OK" diff --git a/install.sh b/install.sh index 8e07fb181..14835a067 100644 --- a/install.sh +++ b/install.sh @@ -1,21 +1,27 @@ #!/bin/bash -ISKYLIMS_VERSION="3.x.x" +APP_VERSION="3.1.0" +# usage: prints the command line help and usage examples. usage() { cat << EOF This script install and upgrade the iskylims app. -usage : $0 --upgrade --dev --conf +usage : $0 --upgrade --git_revision --conf Optional input data: - --install | Install iskylims full/dep/app - --upgrade | Upgrade iskylims full/dep/app - --dev | Use the develop version instead of main release - --conf | Select custom configuration file. Default: ./install_settings.txt - --tables | Load the first inital tables for upgrades in conf folder - --script | Run a migration script. - --ren_app | Rename apps required for the upgrade migration to 3.0.0 - --docker | Specific installation for docker compose configuration. + --install | Install iskylims full/dep/app + --upgrade | Upgrade iskylims full/dep/app + --stage | Stage app files only (install/upgrade) without DB work. Internal/container use. + --bootstrap | Run DB/bootstrap steps only (install/upgrade) against an existing staged app. Internal/container use. + --git_revision | Git revision name to run (branch, tag, commit SHA, or 'current' to use copied local sources as-is) + --conf | Select custom configuration file. Default: ./install_settings.txt + --tables | Load the first inital tables (from conf folder) + --skip_tables | Skip loading initial tables (even during install) + --script | Run a migration script after migrations. + --script_before | Run a migration script before migrations. + --script_after | Run a migration script after migrations (same as --script). + --ren_app | Rename apps required for the upgrade migration to 3.0.0 + --docker | Deprecated. Use --skip_apache_restart to avoid Apache checks/restart. Examples: @@ -26,127 +32,224 @@ Examples: $0 --install app Upgrade using develop code - $0 --upgrade full --dev + $0 --upgrade full --git_revision develop Upgrade running migration script and update initial tables $0 --upgrade full --script --tables + Stage application files during a container image build + $0 --stage install --git_revision main --conf conf/docker_production_settings.txt + + Bootstrap database/static using an already staged container image + $0 --bootstrap upgrade --git_revision main --conf conf/docker_production_settings.txt + Make adjustments for apps renaming in upgrade 2.3.0 to 2.3.1 $0 --upgrade full --ren_app --script --tables + + Upgrade running pre/post migration scripts: + $0 --upgrade app --script_before --script_after EOF } +# log: write timestamped log entries to stdout. +_log_compose_entry() { + local level="$1"; shift + local message="$*" + local timestamp + timestamp="$(date '+%Y-%m-%d %H:%M:%S')" + printf "%s [%s] %s" "$timestamp" "$level" "$message" +} + +log() { + local level="$1"; shift + local message="$*" + local entry + entry="$(_log_compose_entry "$level" "$message")" + printf "%s\n" "$entry" +} + +# db_check: verifies connectivity to the configured MySQL instance using mysqladmin/mysqlshow. db_check(){ - # user should have mysql permission on remote server. - mysqladmin -h $DB_SERVER_IP -u$DB_USER -p$DB_PASS -P$DB_PORT processlist > /dev/null + log "INFO" "Checking database connectivity against $DB_SERVER_IP:$DB_PORT" + local mysqladmin_bin + local mysqlshow_bin + mysqladmin_bin="$(command -v mysqladmin || command -v mariadb-admin || true)" + mysqlshow_bin="$(command -v mysqlshow || command -v mariadb-show || true)" + + if [ -z "$mysqladmin_bin" ] || [ -z "$mysqlshow_bin" ]; then + log "ERROR" "mysql client tools not found (mysqladmin/mysqlshow or mariadb-admin/mariadb-show)." + exit 1 + fi + + "$mysqladmin_bin" -h $DB_SERVER_IP -u$DB_USER -p$DB_PASS -P$DB_PORT processlist > /dev/null if ! [ $? -eq 0 ]; then - echo -e "${RED}ERROR : Unable to connect to database. Check if your database is running and accessible${NC}" + log "ERROR" "Unable to connect to database. Check if your database is running and accessible" exit 1 fi - RESULT=`mysqlshow --user=$DB_USER --password=$DB_PASS --host=$DB_SERVER_IP --port=$DB_PORT | grep -o $DB_NAME` + RESULT=`"$mysqlshow_bin" --user=$DB_USER --password=$DB_PASS --host=$DB_SERVER_IP --port=$DB_PORT | grep -o $DB_NAME` if ! [ "$RESULT" == "$DB_NAME" ] ; then - echo -e "${RED}ERROR : iskylims database is not defined yet ${NC}" - echo -e "${RED}ERROR : Create iskylims database on your mysql server and run again the installation script ${NC}" + log "ERROR" "iskylims database is not defined yet" + log "ERROR" "Create iskylims database on your mysql server and run again the installation script" exit 1 fi } +# apache_check: ensures apache/httpd service is running depending on distribution. apache_check(){ if [[ $linux_distribution == "Ubuntu" ]]; then if ! pidof apache2 > /dev/null ; then - # web server down, restart the server - echo "Apache Server is down... Trying to restart Apache" + log "WARN" "Apache Server is down... Trying to restart Apache" systemctl restart apache2.service sleep 10 if pidof apache2 > /dev/null ; then - echo "Apache Server is up" + log "INFO" "Apache Server is up" else - echo -e "${RED}ERROR : Unable to start Apache ${NC}" - echo -e "${RED}ERROR : Solve the issue with Apache server and run again the installation script ${NC}" + log "ERROR" "Unable to start Apache" + log "ERROR" "Solve the issue with Apache server and run again the installation script" exit 1 fi fi elif [[ $linux_distribution == "CentOs" || $linux_distribution == "RedHatEnterprise" ]]; then if ! pidof httpd > /dev/null ; then - # web server down, restart the server - echo "Apache Server is down... Trying to restart Apache" + log "WARN" "Apache Server is down... Trying to restart Apache" systemctl restart httpd sleep 10 if pidof httpd > /dev/null ; then - echo "Apache Server is up" + log "INFO" "Apache Server is up" else - echo -e "${RED}ERROR : Unable to start Apache ${NC}" - echo -e "${RED}ERROR : Solve the issue with Apache server and run again the installation script ${NC}" + log "ERROR" "Unable to start Apache" + log "ERROR" "Solve the issue with Apache server and run again the installation script" exit 1 fi fi fi } +# python_check: confirm required Python version is available in PYTHON_BIN_PATH. python_check(){ - - python_version=$(su -c $PYTHON_BIN_PATH --version $user) + python_version=$(su -c $PYTHON_BIN_PATH --version $user 2>&1) if [[ $python_version == "" ]]; then - echo -e "${RED}ERROR : Python3 is not found in your system ${NC}" - echo -e "${RED}ERROR : Solve the issue with Python and run again the installation script ${NC}" + log "ERROR" "Python3 is not found in your system" + log "ERROR" "Solve the issue with Python and run again the installation script" exit 1 fi p_version=$(echo $python_version | cut -d"." -f2) if (( $p_version < 7 )); then - echo -e "${RED}ERROR : Application requieres at least the version 3.7.x of Python3 ${NC}" - echo -e "${RED}ERROR : Solve the issue with python and run again the installation script ${NC}" + log "ERROR" "Application requires at least version 3.7.x of Python3" + log "ERROR" "Solve the issue with python and run again the installation script" exit 1 fi } +# root_check: enforce running privileged sections as root. root_check(){ if [[ $EUID -ne 0 ]]; then - printf "\n\n%s" - printf "${RED}------------------${NC}\n" - printf "%s" - printf "${RED}Exiting installation. This script must be run as root ${NC}\n" - printf "\n\n%s" - printf "${RED}------------------${NC}\n" - printf "%s" + log "ERROR" "Exiting installation. This script must be run as root" exit 1 fi } +generate_django_secret_key(){ + "$PYTHON_BIN_PATH" -c "import secrets; print(''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for _ in range(50)))" +} + +sed_replacement_escape(){ + printf '%s' "$1" | sed -e 's/[\&|]/\\&/g' +} + +# update_settings_and_urls: rewrite Django settings and urls with deployment values. update_settings_and_urls(){ - # save SECRET KEY at home user directory - grep ^SECRET $INSTALL_PATH/iskylims/settings.py > ~/.secret - - # Copying config files and script. TODO CHANGE iSkyLIMS to app name - cp conf/template_settings.txt $INSTALL_PATH/iskylims/settings.py - cp conf/urls.py $INSTALL_PATH/iskylims - - # replacing dummy variables with real values - sed -i "/^SECRET/c\\$(cat ~/.secret)" $INSTALL_PATH/iskylims/settings.py - sed -i "s/djangouser/${DB_USER}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/djangopass/${DB_PASS}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/djangohost/${DB_SERVER_IP}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/djangoport/${DB_PORT}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/djangodbname/${DB_NAME}/g" $INSTALL_PATH/iskylims/settings.py - - sed -i "s/emailhostserver/${EMAIL_HOST_SERVER}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/emailport/${EMAIL_PORT}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/emailhostuser/${EMAIL_HOST_USER}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/emailhostpassword/${EMAIL_HOST_PASSWORD}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/emailhosttls/${EMAIL_USE_TLS}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/localserverip/${LOCAL_SERVER_IP}/g" $INSTALL_PATH/iskylims/settings.py - sed -i "s/localhost/${DNS_URL}/g" $INSTALL_PATH/iskylims/settings.py -} - -upgrade_venv(){ - echo "activate the virtualenv" - source virtualenv/bin/activate - echo "Installing required python packages" - python -m pip install --upgrade pip - python -m pip install -r conf/requirements.txt + log "INFO" "Updating settings.py and urls.py with deployment values" + local project_dir="$INSTALL_PATH/$PROJECT_NAME" + local secret_line="" + local tmp_settings="" + + if [ -f "$project_dir/settings.py" ]; then + secret_line="$(grep -E "^SECRET_KEY[[:space:]]*=" "$project_dir/settings.py" | tail -n 1)" + fi + if [ -z "$secret_line" ] || [[ "$secret_line" =~ SECRET_KEY[[:space:]]*=[[:space:]]*SECRET ]]; then + secret_line="SECRET_KEY = '$(generate_django_secret_key)'" + fi + + tmp_settings="$(mktemp)" + cp conf/template_settings.txt "$tmp_settings" + cp conf/urls.py "$project_dir" + + sed -i \ + -e "s|^SECRET_KEY.*|$(sed_replacement_escape "$secret_line")|" \ + -e "s|djangouser|$(sed_replacement_escape "$DB_USER")|g" \ + -e "s|djangopass|$(sed_replacement_escape "$DB_PASS")|g" \ + -e "s|djangohost|$(sed_replacement_escape "$DB_SERVER_IP")|g" \ + -e "s|djangoport|$(sed_replacement_escape "$DB_PORT")|g" \ + -e "s|djangodbname|$(sed_replacement_escape "$DB_NAME")|g" \ + -e "s|emailhostserver|$(sed_replacement_escape "$EMAIL_HOST_SERVER")|g" \ + -e "s|emailport|$(sed_replacement_escape "$EMAIL_PORT")|g" \ + -e "s|emailhostuser|$(sed_replacement_escape "$EMAIL_HOST_USER")|g" \ + -e "s|emailhostpassword|$(sed_replacement_escape "$EMAIL_HOST_PASSWORD")|g" \ + -e "s|emailhosttls|$(sed_replacement_escape "$EMAIL_USE_TLS")|g" \ + -e "s|localserverip|$(sed_replacement_escape "$LOCAL_SERVER_IP")|g" \ + -e "s|localhost|$(sed_replacement_escape "$DNS_URL")|g" \ + "$tmp_settings" + + cp "$tmp_settings" "$project_dir/settings.py" + rm -f "$tmp_settings" } +# restore_git_ref: reset repository to branch/tag/commit active before script ran. +restore_git_ref() { + echo "Restoring to initial git reference: $initial_git_ref" + git checkout "$initial_git_ref" --quiet +} + +# load_tables: wrapper to call Django loaddata with optional verbosity. +load_tables() { + # Function parameters + local data_file="${1:-conf/first_install_tables.json}" + local verbose="${2:-false}" + + # Check if the file exists + if [[ ! -f "$data_file" ]]; then + echo "Error: The data file '$data_file' does not exist." + return 1 + fi + + # Conditional message based on verbose mode + if [[ "$verbose" == true ]]; then + echo "Loading pre-filled tables from file: $data_file" + fi + + # Load pre-filled tables + python manage.py loaddata "$data_file" + if [[ $? -eq 0 ]]; then + echo "Tables loaded successfully from '$data_file'." + else + echo "Error loading tables from '$data_file'." + return 1 + fi + + if [[ "$verbose" == true ]]; then + echo "Table loading process completed." + fi +} + +# ensure_git_safe_directory: avoid Git "dubious ownership" failures in containerized installs. +ensure_git_safe_directory() { + local repo_dir + repo_dir="$(pwd -P)" + + if [ -d "$repo_dir/.git" ] || [ -f "$repo_dir/.git" ]; then + git config --global --add safe.directory "$repo_dir" >/dev/null 2>&1 || true + git config --system --add safe.directory "$repo_dir" >/dev/null 2>&1 || true + fi +} + +# Ensure to recover current git branch/tag/SHA on script exit +ensure_git_safe_directory +initial_git_ref=$(git rev-parse --abbrev-ref HEAD || git rev-parse HEAD) +trap restore_git_ref EXIT + #================================================================ #SET TEMINAL COLORS #================================================================ @@ -157,6 +260,649 @@ BLUE='\033[0;34m' RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' +ORANGE='\033[0;33m' + +# log_section: print a visually separated header in both console and log. +log_section() { + local message="$1" + log "INFO" "$message" + printf "\n\n%s\n" "${YELLOW}------------------${NC}" + printf "%b\n" "${YELLOW}${message}${NC}" + printf "%s\n\n" "${YELLOW}------------------${NC}" +} + +# log_info: convenience helper for blue info messages (console only). +log_info() { + printf "%b\n" "${BLUE}$(_log_compose_entry "INFO" "$1")${NC}" +} + +# log_warn: emit warning text in cyan for terminal visibility. +log_warn() { + printf "%b\n" "${CYAN}$(_log_compose_entry "WARN" "$1")${NC}" +} + +# log_error: emit error text in red for terminal visibility. +log_error() { + printf "%b\n" "${RED}$(_log_compose_entry "ERROR" "$1")${NC}" +} + +# abort_install: log an error and exit with optional status. +abort_install() { + log_error "$1" + exit "${2:-1}" +} + +chown_if_root() { + if [ "$EUID" -eq 0 ]; then + chown "$@" + else + log_warn "Skipping chown (requires root): chown $*" + fi +} + +ensure_file_exists() { + local file_path="$1" + local friendly_name="${2:-$1}" + if [ ! -f "$file_path" ]; then + abort_install "Required file '$friendly_name' not found." + fi +} + +# load_install_config: source the selected install_settings file. +load_install_config() { + ensure_file_exists "$conf" "$conf" + # shellcheck disable=SC1090 + . "$conf" + INSTALL_PATH="${APP_INSTALL_PATH:-$INSTALL_PATH}" +} + +# checkout_git_revision: ensure desired git revision exists and check it out safely. +checkout_git_revision() { + if [[ "$git_branch" == "current" ]]; then + printf "${YELLOW}Using copied local working tree without git checkout.${NC}\n" + return 0 + fi + if git rev-parse --verify "$git_branch" >/dev/null 2>&1; then + if [[ $git_branch != $initial_git_ref ]]; then + local local_changes + local_changes=$(git status --porcelain) + if [[ -n $local_changes ]]; then + abort_install "Unable to switch to $git_branch. Commit or stash local changes first." + fi + printf "${YELLOW}Switching to revision %s.${NC}\n" "$git_branch" + git checkout "$git_branch" --quiet + else + printf "${YELLOW}Using current revision: '%s'.${NC}\n" "$git_branch" + fi + else + abort_install "Git reference $git_branch is not defined in ${PWD}." + fi +} + +# check_requirements: run Python/DB/Apache/root validations before install/upgrade. +check_requirements() { + log_section "Checking main requirements" + python_check + log_info "Valid version of Python" + if [[ "$operation_scope" == "full" || "$operation_scope" == "app" ]]; then + db_check + log_info "Successful check for database" + if [ "$restart_apache" = true ]; then + apache_check + log_info "Successful check for apache" + fi + fi + + if [ "$install_type" == "full" ] || [ "$install_type" == "dep" ] || [ "$upgrade_type" == "full" ] || [ "$upgrade_type" == "dep" ]; then + log_warn "Checking requirement of root user when installation is full or dep" + root_check + log_info "Successful checking of root user" + fi +} + +check_stage_requirements() { + log_section "Checking requirements for staged app preparation" + python_check + log_info "Valid version of Python" +} + +check_bootstrap_requirements() { + log_section "Checking requirements for application bootstrap" + python_check + log_info "Valid version of Python" + db_check + log_info "Successful check for database" +} + +# rename_apps_if_needed: handles legacy app renaming and DB/migration adjustments when --ren_app is provided. +rename_apps_if_needed() { + if [ $ren_app != true ]; then + return 0 + fi + + rm -rf $INSTALL_PATH/django_utils/migrations/* + rm -rf $INSTALL_PATH/iSkyLIMS_core/migrations/* + rm -rf $INSTALL_PATH/iSkyLIMS_wetlab/migrations/* + rm -rf $INSTALL_PATH/iSkyLIMS_drylab/migrations/* + + cd $INSTALL_PATH + sed -i "s/ugettext/gettext/g" iSkyLIMS_wetlab/models.py + sed -i "s/ugettext/gettext/g" iSkyLIMS_core/forms.py + sed -i "s/ugettext/gettext/g" django_utils/forms.py + echo "activate the virtualenv" + source virtualenv/bin/activate + + echo "Create a fake initial" + python manage.py makemigrations $FAKEINITIAL_MODULES + python manage.py migrate --fake-initial + + if [ -d "$INSTALL_PATH/iSkyLIMS_core" ]; then + echo "Changing app dir names in $INSTALL_PATH..." + rm -rf $INSTALL_PATH/.git $INSTALL_PATH/.github $INSTALL_PATH/.gitignore \ + $INSTALL_PATH/.Rhistory $INSTALL_PATH/docker-compose.test.yml $INSTALL_PATH/docker_iskylims_install.sh \ + $INSTALL_PATH/Dockerfile $INSTALL_PATH/install.sh $INSTALL_PATH/install_settings.txt + mv $INSTALL_PATH/iSkyLIMS_core $INSTALL_PATH/core + mv $INSTALL_PATH/iSkyLIMS_wetlab $INSTALL_PATH/wetlab + mv $INSTALL_PATH/iSkyLIMS_drylab $INSTALL_PATH/drylab + mv $INSTALL_PATH/iSkyLIMS_clinic $INSTALL_PATH/clinic + echo "Done changing app dir names in $INSTALL_PATH..." + fi + if [ -d "iSkyLIMS" ]; then + mv iSkyLIMS/ iskylims/ + sed -i "s/iSkyLIMS/iskylims/g" $INSTALL_PATH/iskylims/wsgi.py + sed -i "s/iSkyLIMS/iskylims/g" $INSTALL_PATH/manage.py + fi + + echo "Modifying database names and constraints..." + mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ + -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_core", "core") WHERE app_label like ("iSkyLIMS_%");' + mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ + -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_wetlab", "wetlab") WHERE app_label like ("iSkyLIMS_%");' + mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ + -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_drylab", "drylab") WHERE app_label like ("iSkyLIMS_%");' + + mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ + -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_core", "core") WHERE app like ("iSkyLIMS_%");' + mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ + -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_wetlab", "wetlab") WHERE app like ("iSkyLIMS_%");' + mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ + -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_drylab", "drylab") WHERE app like ("iSkyLIMS_%");' + + echo "Renaming tables" + query_rename_table="SELECT CONCAT('RENAME TABLE ', TABLE_SCHEMA, '.', TABLE_NAME, \ + ' TO ', TABLE_SCHEMA, '.', REPLACE(TABLE_NAME, 'iSkyLIMS_', ''), ';') \ + AS query FROM information_schema.tables WHERE TABLE_SCHEMA = \"$DB_NAME\" AND TABLE_NAME LIKE 'iSkyLIMS_%';" + mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_table" \ + | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash + + echo "Renaming index" + query_rename_unique_indexes="SELECT CONCAT('ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ + ' RENAME INDEX ', rcu.CONSTRAINT_NAME, \ + ' TO ', REPLACE(rcu.CONSTRAINT_NAME, 'iSkyLIMS_', ''), ';') \ + AS query FROM information_schema.key_column_usage rcu \ + JOIN information_schema.table_constraints tc \ + ON tc.CONSTRAINT_NAME = rcu.CONSTRAINT_NAME WHERE rcu.TABLE_SCHEMA = \"$DB_NAME\" \ + AND rcu.CONSTRAINT_NAME LIKE 'iSkyLIMS_%' AND tc.CONSTRAINT_TYPE = 'UNIQUE' \ + GROUP BY rcu.TABLE_SCHEMA, rcu.TABLE_NAME, rcu.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, \ + rcu.REFERENCED_TABLE_SCHEMA, rcu.REFERENCED_TABLE_NAME;" + mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_unique_indexes" \ + | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash + + echo "Renaming constraints" + query_rename_constraints="SELECT CONCAT('ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ + ' DROP FOREIGN KEY ' , rcu.CONSTRAINT_NAME, ';', \ + ' ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ + ' ADD CONSTRAINT ', REPLACE(rcu.CONSTRAINT_NAME, 'iSkyLIMS_', ''), ' ', \ + tc.CONSTRAINT_TYPE, ' (', GROUP_CONCAT(rcu.COLUMN_NAME ORDER BY rcu.ORDINAL_POSITION SEPARATOR ', '), ')', \ + IF(tc.CONSTRAINT_TYPE = 'FOREIGN KEY', \ + CONCAT(' REFERENCES ', rcu.REFERENCED_TABLE_SCHEMA, '.', REPLACE(rcu.REFERENCED_TABLE_NAME, 'iSkyLIMS_', ''), ' (', \ + GROUP_CONCAT(rcu.REFERENCED_COLUMN_NAME ORDER BY rcu.ORDINAL_POSITION SEPARATOR ', '), ') ON DELETE ', rc.DELETE_RULE), \ + ''), ';') AS query \ + FROM information_schema.key_column_usage rcu \ + LEFT JOIN information_schema.table_constraints tc ON rcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME \ + LEFT JOIN information_schema.referential_constraints rc ON rcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME \ + WHERE rcu.TABLE_SCHEMA = '$DB_NAME' AND rcu.CONSTRAINT_NAME LIKE 'iSkyLIMS_%' \ + GROUP BY rcu.TABLE_SCHEMA, rcu.TABLE_NAME, rcu.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, rcu.REFERENCED_TABLE_SCHEMA, rcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE;" + mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_constraints" | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash + + echo "Done modifying database names and constraints..." + + echo "Modifying names in migration files..." + sed -i 's/iSkyLIMS_core/core/g' */migrations/*.py + sed -i 's/iSkyLIMS_drylab/drylab/g' */migrations/*.py + sed -i 's/iSkyLIMS_wetlab/wetlab/g' */migrations/*.py + echo "Done modifying names in migration files..." + + echo "Copying custom migration files from conf." + cp $INSTALL_PATH/conf/0002_core_migration_v3.0.0.py $INSTALL_PATH/core/migrations/0002_migration_v3_0_0.py + cp $INSTALL_PATH/conf/0002_drylab_migration_v3.0.0.py $INSTALL_PATH/drylab/migrations/0002_migration_v3_0_0.py + cp $INSTALL_PATH/conf/0002_wetlab_migration_v3.0.0.py $INSTALL_PATH/wetlab/migrations/0002_migration_v3_0_0.py + cp $INSTALL_PATH/conf/0002_django_utils_migration_v3.0.0.py $INSTALL_PATH/django_utils/migrations/0002_migration_v3_0_0.py + + read -p "Do you want to proceed with the migrate command? (Y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]] ; then + log "WARN" "Exiting without running migrate command." + exit 1 + fi + + echo "activate the virtualenv" + source virtualenv/bin/activate + echo "Running migrate..." + python manage.py migrate + echo "Done migrate command." + + cd - +} + +# install_system_packages: install InterOp and distro-specific OS packages required by iSkyLIMS. +install_system_packages() { + if [ "${SKIP_SYSTEM_PACKAGES:-}" = "1" ]; then + echo "Skipping system package installation (SKIP_SYSTEM_PACKAGES=1)" + return + fi + + echo "Installing Interop" + if [ -d /opt/interop ]; then + echo "There is already an interop installation" + echo "Skipping Interop installation" + else + cd /opt + echo "Downloading interop software" + wget https://github.com/Illumina/interop/releases/download/v1.1.15/InterOp-1.1.15-Linux-GNU.tar.gz + tar -xf InterOp-1.1.15-Linux-GNU.tar.gz + ln -s InterOp-1.1.15-Linux-GNU interop + rm InterOp-1.1.15-Linux-GNU.tar.gz + echo "Interop is now installed" + cd - + fi + + if command -v lsb_release >/dev/null 2>&1; then + linux_distribution=$(lsb_release -i | cut -f 2-) + else + linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) + fi + + if [[ $linux_distribution == "Ubuntu" || $linux_distribution == "ubuntu" ]]; then + echo "Software installation for Ubuntu" + apt-get update && apt-get upgrade -y + apt-get install -y \ + apt-utils wget \ + libmysqlclient-dev \ + python3-venv \ + libpq-dev \ + python3-dev python3-pip python3-wheel \ + apache2-dev cifs-utils \ + gnuplot + + elif [[ $linux_distribution == "CentOS" || $linux_distribution == "RedHatEnterprise" || $linux_distribution == "centos" || $linux_distribution == "rhel" || $linux_distribution == "fedora" ]]; then + echo "Software installation for Centos/RedHat" + yum groupinstall "Development tools" + yum install zlib-devel bzip2-devel openssl-devel \ + wget httpd-devel mysql-libs sqlite sqlite-devel \ + mariadb-devel libffi-devel \ + gnuplot cifs-utils + fi +} + +# run_django_deploy: execute makemigrations/migrate and optional fixture/superuser steps. +run_django_deploy() { + local mode="${1:-install}" + if [ "$run_script_before" = true ]; then + for val in "${migration_script_before[@]}"; do + if [[ $val = *","* ]]; then + parameters=(${val//,/ }) + echo "Running pre-migration script: ${parameters[0]}" + ./manage.py runscript ${parameters[0]} --script-args ${parameters[1]} + echo "Done pre-migration script: ${parameters[0]}" + else + echo "Running pre-migration script: $val" + ./manage.py runscript $val + echo "Done pre-migration script: $val" + fi + done + fi + + if [ "$mode" = "upgrade" ]; then + echo "Applying migrations in fake-initial mode" + python manage.py migrate --noinput --fake-initial + # Second pass ensures non-initial migrations are applied after fake-initial. + echo "Applying migrations" + python manage.py migrate --noinput + else + echo "Applying migrations" + python manage.py migrate --noinput + fi + + if [ "$tables" = true ]; then + echo "Loading pre-filled tables..." + load_tables "$prefilled_tables" true + echo "Done loading pre-filled tables..." + fi + + if [ "$run_script" = true ]; then + for val in "${migration_script[@]}"; do + if [[ $val = *","* ]]; then + parameters=(${val//,/ }) + echo "Running post-migration script: ${parameters[0]}" + ./manage.py runscript ${parameters[0]} --script-args ${parameters[1]} + echo "Done post-migration script: ${parameters[0]}" + else + echo "Running post-migration script: $val" + ./manage.py runscript $val + echo "Done post-migration script: $val" + fi + done + fi + + if [ "$mode" = "install" ]; then + echo "Creating super user " + python manage.py createsuperuser --username admin + fi +} + +refresh_static_files() { + echo "Deleting static files..." + if [ -d "$INSTALL_PATH/static" ]; then + if command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$INSTALL_PATH/static"; then + echo "Static directory is a mount point. Skipping delete." + else + rm -rf "$INSTALL_PATH/static" || echo "Skipping static removal (busy)." + fi + fi + echo "Running collect statics..." + python manage.py collectstatic --noinput + echo "Done collect statics" +} + +# sync_requirements_file: copy repository requirements into the target installation path. +sync_requirements_file() { + mkdir -p $INSTALL_PATH/conf + rsync -rlv conf/requirements.txt $INSTALL_PATH/conf/requirements.txt +} + +# setup_virtualenv: create or refresh the Python virtualenv depending on mode. +setup_virtualenv() { + local mode="$1" + cd $INSTALL_PATH + if [ "$mode" = "install" ]; then + if [ -d virtualenv ]; then + echo "There already is a virtualenv for iskylims in $INSTALL_PATH." + read -p "Do you want to remove current virtualenv and reinstall? (Y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] ; then + rm -rf $INSTALL_PATH/virtualenv + bash -c "$PYTHON_BIN_PATH -m venv virtualenv" + else + echo "virtualenv already defined. Skipping." + fi + else + bash -c "$PYTHON_BIN_PATH -m venv virtualenv" + fi + else + if [ -d virtualenv ]; then + read -p "Do you want to remove current virtualenv and reinstall? (Y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] ; then + rm -rf $INSTALL_PATH/virtualenv + bash -c "$PYTHON_BIN_PATH -m venv virtualenv" + fi + else + read -p "There is no virtualenv. Do you want to create a new one? (Y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] ; then + bash -c "$PYTHON_BIN_PATH -m venv virtualenv" + else + echo "Exiting..." + exit 0 + fi + fi + fi + cd - +} + +# prepare_documents_structure: ensure document directories and templates exist with correct permissions. +prepare_documents_structure() { + echo "Created documents structure" + mkdir -p $INSTALL_PATH/documents/wetlab + mkdir -p $INSTALL_PATH/documents/wetlab/tmp + mkdir -p $INSTALL_PATH/documents/wetlab/sample_sheet + mkdir -p $INSTALL_PATH/documents/wetlab/images_plot + mkdir -p $INSTALL_PATH/documents/wetlab/templates + mkdir -p $INSTALL_PATH/documents/wetlab/sample_sheets_lib_prep + mkdir -p $INSTALL_PATH/documents/drylab + mkdir -p $INSTALL_PATH/documents/drylab/service_files + + chown_if_root -R "$user:$apache_group" "$INSTALL_PATH/documents" + chmod 775 $INSTALL_PATH/documents + + cp $INSTALL_PATH/conf/*_template.csv $INSTALL_PATH/documents/wetlab/templates/ + cp $INSTALL_PATH/conf/samples_template.xlsx $INSTALL_PATH/documents/wetlab/templates/ + + mkdir -p $INSTALL_PATH/documents/wetlab/collection_index_kits/ + cp $INSTALL_PATH/conf/collection_index_kits/*.txt $INSTALL_PATH/documents/wetlab/collection_index_kits/ + + cp $INSTALL_PATH/conf/template_logging_config.ini $INSTALL_PATH/wetlab/logging_config.ini + sed -i "s|INSTALL_PATH|${INSTALL_PATH}|g" $INSTALL_PATH/wetlab/logging_config.ini +} + +# install_python_requirements: activate the venv and install required Python packages. +install_python_requirements() { + cd $INSTALL_PATH + echo "activate the virtualenv" + source virtualenv/bin/activate + echo "Installing required python packages" + python -m pip install --upgrade pip + python -m pip install wheel + python -m pip install -r conf/requirements.txt + cd - +} + +ensure_virtualenv_ready() { + if [ ! -d "$INSTALL_PATH/virtualenv" ]; then + log_warn "Virtualenv missing. INSTALL_PATH=$INSTALL_PATH" + ls -la "$INSTALL_PATH" || true + abort_install "Virtualenv not found at $INSTALL_PATH/virtualenv. Run --install dep first." + fi +} + +# restart_apache_service: restart Apache/HTTPD unless running inside Docker or explicitly skipped. +restart_apache_service() { + if command -v lsb_release >/dev/null 2>&1; then + linux_distribution=$(lsb_release -i | cut -f 2-) + else + linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) + fi + if [[ $linux_distribution == "Ubuntu" ]]; then + apache_daemon="apache2" + else + apache_daemon="httpd" + fi + if ! systemctl restart $apache_daemon; then + echo -e "${ORANGE}Apache server restart failed. trying with sudo${NC}" + sudo systemctl restart $apache_daemon + fi +} + +# run_dependency_stage: execute the dependency portion (system packages + venv + pip) for install or upgrade. +run_dependency_stage() { + local mode="$1" + + if [ "$mode" = "install" ]; then + log_section "Preparing dependency environment for installation" + if [ -d $INSTALL_PATH ]; then + echo "There already is an installation of iskylims in $INSTALL_PATH." + read -p "Do you want to remove current installation and reinstall? (Y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]] ; then + echo "Exiting without running iSkyLIMS installation" + exit 1 + else + rm -rf $INSTALL_PATH + fi + fi + install_system_packages + mkdir -p $INSTALL_PATH + linux_distribution=$(lsb_release -i | cut -f 2-) + if [[ $linux_distribution == "Ubuntu" ]]; then + apache_group="www-data" + else + apache_group="apache" + fi + chown_if_root -R "$user:$apache_group" "$INSTALL_PATH" + chmod 775 $INSTALL_PATH + else + log_section "Preparing dependency environment for upgrade" + if [ ! -d $INSTALL_PATH ]; then + abort_install "Unable to start the upgrade. Folder $INSTALL_PATH does not exist." + fi + install_system_packages + fi + + sync_requirements_file + setup_virtualenv "$mode" + install_python_requirements +} + +# upgrade_application_files: sync code/config and run upgrade-specific tasks (renames, migrations). +upgrade_application_files() { + if [ ! -d $INSTALL_PATH ]; then + abort_install "Unable to start the upgrade. Folder $INSTALL_PATH does not exist." + fi + + log_section "Starting iSkyLIMS Upgrade version: ${APP_VERSION}" + + rename_apps_if_needed + + stage_upgrade_application_files + bootstrap_application_runtime "upgrade" + + log_section "Successfuly upgrade of iSKyLIMS version: ${APP_VERSION}" +} + +# stage_upgrade_application_files: sync code/config into INSTALL_PATH without DB work. +stage_upgrade_application_files() { + if [ ! -d $INSTALL_PATH ]; then + abort_install "Unable to start the upgrade. Folder $INSTALL_PATH does not exist." + fi + + echo "Copying files to installation folder" + rsync -rlv conf/ $INSTALL_PATH/conf/ + rsync -rlv --fuzzy --delay-updates --delete-delay \ + --exclude "logs" --exclude "documents" --exclude "__pycache__" \ + README.md LICENSE test conf $REQUIRED_MODULES $INSTALL_PATH + + cd $INSTALL_PATH + ensure_virtualenv_ready + echo "activate the virtualenv" + source virtualenv/bin/activate + + if [ ! -f "$INSTALL_PATH/manage.py" ]; then + echo "manage.py not found. Creating ${PROJECT_NAME} project" + "$INSTALL_PATH/virtualenv/bin/python" -m django startproject "$PROJECT_NAME" . + fi + + echo "Update settings and url file." + update_settings_and_urls + prepare_documents_structure + + cd - +} + +# install_application_files: deploy Django project files, update settings, and run initial migrations. +install_application_files() { + stage_install_application_files + bootstrap_application_runtime "install" + log_section "Successfuly iSkyLIMS Installation version: ${APP_VERSION}" + echo "Installation completed" +} + +# stage_install_application_files: copy app files into INSTALL_PATH without DB work. +stage_install_application_files() { + log_section "Starting iSkyLIMS install version: ${APP_VERSION}" + + user=${SUDO_USER:-$USER} + group=$(groups | cut -d" " -f1) + + if command -v lsb_release >/dev/null 2>&1; then + linux_distribution=$(lsb_release -i | cut -f 2-) + else + linux_distribution=$(awk -F= '/^ID=/{gsub(/"/,""); print $2}' /etc/os-release) + fi + + if [[ $linux_distribution == "Ubuntu" || $linux_distribution == "ubuntu" ]]; then + apache_group="www-data" + else + apache_group="apache" + fi + + if [ "$install_type" == "full" ] || [ "$install_type" == "app" ]; then + + if [ $LOG_TYPE == "symbolic_link" ]; then + if [ -d $LOG_PATH ]; then + if [ -e "$INSTALL_PATH/logs" ]; then + echo "Log target $INSTALL_PATH/logs already exists. Leaving it unchanged." + else + echo "Creating symbolic link to log folder" + ln -s "$LOG_PATH" "$INSTALL_PATH/logs" + chmod 775 "$LOG_PATH" + fi + else + echo "Log folder path: $LOG_PATH does not exist. Fix it in the install_settings.txt and run again." + exit 1 + fi + else + if [ ! -d $INSTALL_PATH/logs ]; then + mkdir -p $INSTALL_PATH/logs + chown_if_root "$user:$apache_group" "$INSTALL_PATH/logs" + chmod 775 $INSTALL_PATH/logs + else + echo "Log folder path: $INSTALL_PATH/logs already exist." + fi + fi + + rsync -rlv README.md LICENSE test conf $REQUIRED_MODULES $INSTALL_PATH + + cd $INSTALL_PATH + + prepare_documents_structure + + ensure_virtualenv_ready + echo "activate the virtualenv" + source virtualenv/bin/activate + + echo "Creating ${PROJECT_NAME} project" + "$INSTALL_PATH/virtualenv/bin/python" -m django startproject "$PROJECT_NAME" . + + update_settings_and_urls + + cd - + fi +} + +# bootstrap_application_runtime: run DB/bootstrap tasks against an already staged INSTALL_PATH. +bootstrap_application_runtime() { + local mode="$1" + + if [ ! -d "$INSTALL_PATH" ]; then + abort_install "Unable to bootstrap application. Folder $INSTALL_PATH does not exist." + fi + + cd $INSTALL_PATH + ensure_virtualenv_ready + echo "activate the virtualenv" + source virtualenv/bin/activate + + if [ ! -f "$INSTALL_PATH/manage.py" ]; then + abort_install "manage.py not found at $INSTALL_PATH/manage.py. Stage application files first." + fi + + update_settings_and_urls + run_django_deploy "$mode" + refresh_static_files + + cd - +} # translate long options to short reset=true @@ -168,14 +914,21 @@ do fi case "$arg" in # OPTIONAL - --install) set -- "$@" -i ;; - --upgrade) set -- "$@" -u ;; - --script) set -- "$@" -s ;; - --tables) set -- "$@" -t ;; - --dev) set -- "$@" -d ;; - --conf) set -- "$@" -c ;; - --ren_app) set -- "$@" -r ;; - --docker) set -- "$@" -k ;; + --install) set -- "$@" -i ;; + --upgrade) set -- "$@" -u ;; + --stage) set -- "$@" -j ;; + --bootstrap) set -- "$@" -l ;; + --script) set -- "$@" -s ;; + --script_before) set -- "$@" -p ;; + --script_after) set -- "$@" -o ;; + --script_prev) set -- "$@" -p ;; + --tables) set -- "$@" -t ;; + --skip_tables) set -- "$@" -b ;; + --git_revision) set -- "$@" -g ;; + --conf) set -- "$@" -c ;; + --ren_app) set -- "$@" -r ;; + --docker) set -- "$@" -k ;; + --skip_apache_restart) set -- "$@" -a ;; # ADITIONAL --help) set -- "$@" -h ;; @@ -188,33 +941,44 @@ done # SETTING DEFAULT VALUES ren_app=false tables=false -git_branch="main" +git_branch=$initial_git_ref conf="./install_settings.txt" install=true install_type="full" upgrade=false upgrade_type="full" +workflow="standard" +workflow_mode="" docker=false +prefilled_tables="conf/first_install_tables.json" +restart_apache=true +run_script=false +run_script_before=false +migration_script=() +migration_script_before=() +skip_tables=false # PARSE VARIABLE ARGUMENTS WITH getops -options=":c:s:i:u:drtkvh" +options=":c:s:i:u:j:l:r:g:tdbkvhao:p:" while getopts $options opt; do case $opt in i ) + workflow="standard" install=true upgrade=false - if [[ "$OPTARG" -eq "full" || "$OPTARG" -eq "dep" || "$OPTARG" -eq "app" ]]; then + if [[ "$OPTARG" == "full" || "$OPTARG" == "dep" || "$OPTARG" == "app" ]]; then install_type=$OPTARG upgrade_type=$OPTARG else - echo "Upgrade is not set to one valid option. Use: --upgrade full/app/dep" + echo "Install is not set to one valid option. Use: --install full/app/dep" exit 1 fi ;; u ) + workflow="standard" install=false upgrade=true - if [[ "$OPTARG" -eq "full" || "$OPTARG" -eq "dep" || "$OPTARG" -eq "app" ]]; then + if [[ "$OPTARG" == "full" || "$OPTARG" == "dep" || "$OPTARG" == "app" ]]; then upgrade_type=$OPTARG install_type=$OPTARG else @@ -222,31 +986,63 @@ while getopts $options opt; do exit 1 fi ;; + j ) + workflow="stage" + workflow_mode=$OPTARG + if [[ "$workflow_mode" != "install" && "$workflow_mode" != "upgrade" ]]; then + echo "Stage is not set to one valid option. Use: --stage install/upgrade" + exit 1 + fi + ;; + l ) + workflow="bootstrap" + workflow_mode=$OPTARG + if [[ "$workflow_mode" != "install" && "$workflow_mode" != "upgrade" ]]; then + echo "Bootstrap is not set to one valid option. Use: --bootstrap install/upgrade" + exit 1 + fi + ;; s ) run_script=true migration_script+=("$OPTARG") ;; + p ) + run_script_before=true + migration_script_before+=("$OPTARG") + ;; + o ) + run_script=true + migration_script+=("$OPTARG") + ;; t ) tables=true ;; + b ) + tables=false + skip_tables=true + ;; r ) ren_app=true ;; - d ) - git_branch="develop" + g ) + git_branch=$OPTARG ;; c ) conf=$OPTARG ;; k ) docker=true + restart_apache=false + ;; + a ) + restart_apache=false ;; h ) usage exit 1 ;; v ) - echo $ISKYLIMS_VERSION + echo $APP_VERSION exit 1 ;; \?) @@ -265,565 +1061,67 @@ while getopts $options opt; do esac done shift $((OPTIND-1)) -#============================================================================= -# SETTINGS CHECKINGS -#============================================================================= - -if [ ! -f "$conf" ]; then - printf "\n\n%s" - printf "${RED}------------------${NC}\n" - printf "${RED}Unable to start.${NC}\n" - printf "${RED}Configuration File $conf does not exist.${NC}\n" - printf "${RED}------------------${NC}\n" - exit 1 -fi - -# Read configuration file - -. $conf -# check if branch master/develop is defined and checkout -if [ "`git branch --list $git_branch`" ]; then - git checkout $git_branch -else - printf "\n\n%s" - printf "${RED}------------------${NC}\n" - printf "${RED}Unable to start.${NC}\n" - printf "${RED}Git branch $git_branch is not define in ${PWD}.${NC}\n" - printf "${RED}------------------${NC}\n" - exit 1 +operation="install" +operation_scope="$install_type" +if [ $upgrade == true ]; then + operation="upgrade" + operation_scope="$upgrade_type" fi - -#================================================================ -# CHECK REQUIREMENTS BEFORE STARTING INSTALLATION -#================================================================ - -echo "Checking main requirements" -python_check -printf "${BLUE}Valid version of Python${NC}\n" -if [ $docker == false ]; then - db_check - printf "${BLUE}Successful check for database${NC}\n" - apache_check - printf "${BLUE}Successful check for apache${NC}\n" +if [ "$workflow" != "standard" ]; then + operation="$workflow_mode" + operation_scope="app" fi -if [ "$install_type" == "full" ] || [ "$install_type" == "dep" ] || [ "$upgrade_type" == "full" ] || [ "$upgrade_type" == "dep" ]; then - printf "${YELLOW} Checking requirement of root user when installation is full or dep ${NC}\n" - root_check - printf "${BLUE}Successful checking of root user${NC}\n" +# Default to loading initial tables on installs unless explicitly skipped. +if [ "$operation" = "install" ] && [ "$skip_tables" = false ] && [ "$tables" = false ]; then + tables=true fi -#============================================================================= -# UPGRADE INSTALLATION -# Check if parameter is passing to script to upgrade the installation -# If "upgrade" parameter is set then the script only execute the upgrade part. -# If other parameter as upgrade is given return usage message and exit -#============================================================================= - -if [ $upgrade == true ]; then - # check if upgrade keyword is given - if [ ! -d $INSTALL_PATH ]; then - printf "\n\n%s" - printf "${RED}------------------${NC}\n" - printf "${RED}Unable to start the upgrade.${NC}\n" - printf "${RED}Folder $INSTALL_PATH does not exist.${NC}\n" - printf "${RED}------------------${NC}\n" - exit 1 +load_install_config +PROJECT_NAME="${PROJECT_NAME:-iskylims}" +checkout_git_revision +user=${SUDO_USER:-$USER} + +if [ "$workflow" = "stage" ]; then + check_stage_requirements + if [ "$workflow_mode" = "install" ]; then + stage_install_application_files + else + stage_upgrade_application_files fi - #================================================================ - # MAIN_BODY FOR UPGRADE - #================================================================ - printf "\n\n%s" - printf "${YELLOW}------------------${NC}\n" - printf "%s" - printf "${YELLOW}Starting iSkyLIMS Upgrade version: ${ISKYLIMS_VERSION}${NC}\n" - printf "%s" - printf "${YELLOW}------------------${NC}\n\n" - - if [ "$upgrade_type" = "full" ] || [ "$upgrade_type" = "dep" ]; then - if [ -d $INSTALL_PATH/virtualenv ]; then - read -p "Do you want to remove current virtualenv and reinstall? (Y/N) " -n 1 -r - echo # (optional) move to a new line - if [[ $REPLY =~ ^[Yy]$ ]] ; then - rm -rf $INSTALL_PATH/virtualenv - rsync -rlv conf/requirements.txt $INSTALL_PATH/conf/requirements.txt - cd $INSTALL_PATH - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - upgrade_venv - cd - - else - rsync -rlv conf/requirements.txt $INSTALL_PATH/conf/requirements.txt - cd $INSTALL_PATH - upgrade_venv - cd - - fi - else - echo "There is no virtualenv to upgrade in $INSTALL_PATH." - read -p "Do you want to create a new virtualenv and reinstall? (Y/N) " -n 1 -r - echo # (optional) move to a new line - if [[ $REPLY =~ ^[Yy]$ ]] ; then - rsync -rlv conf/requirements.txt $INSTALL_PATH/conf/requirements.txt - cd $INSTALL_PATH - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - upgrade_venv - cd - - else - echo "Exiting..." - exit 0 - fi - fi - fi - - if [ "$upgrade_type" = "full" ] || [ "$upgrade_type" = "app" ]; then - - # Delete git and no copy files stuff - if [ $ren_app == true ] ; then - # remove all previous migrations and make a fake initial - # delete existing migrations file - rm -rf $INSTALL_PATH/django_utils/migrations/* - rm -rf $INSTALL_PATH/iSkyLIMS_core/migrations/* - rm -rf $INSTALL_PATH/iSkyLIMS_wetlab/migrations/* - rm -rf $INSTALL_PATH/iSkyLIMS_drylab/migrations/* - - cd $INSTALL_PATH - sed -i "s/ugettext/gettext/g" iSkyLIMS_wetlab/models.py - sed -i "s/ugettext/gettext/g" iSkyLIMS_core/forms.py - sed -i "s/ugettext/gettext/g" django_utils/forms.py - echo "activate the virtualenv" - source virtualenv/bin/activate - - echo "Create a fake initial" - python manage.py makemigrations django_utils iSkyLIMS_core \ - iSkyLIMS_wetlab iSkyLIMS_drylab - python manage.py migrate --fake-initial - - if [ -d "$INSTALL_PATH/iSkyLIMS_core" ]; then - echo "Changing app dir names in $INSTALL_PATH..." - rm -rf $INSTALL_PATH/.git $INSTALL_PATH/.github $INSTALL_PATH/.gitignore \ - $INSTALL_PATH/.Rhistory $INSTALL_PATH/docker-compose.yml $INSTALL_PATH/docker_iskylims_install.sh \ - $INSTALL_PATH/Dockerfile $INSTALL_PATH/install.sh $INSTALL_PATH/install_settings.txt - mv $INSTALL_PATH/iSkyLIMS_core $INSTALL_PATH/core - mv $INSTALL_PATH/iSkyLIMS_wetlab $INSTALL_PATH/wetlab - mv $INSTALL_PATH/iSkyLIMS_drylab $INSTALL_PATH/drylab - mv $INSTALL_PATH/iSkyLIMS_clinic $INSTALL_PATH/clinic - echo "Done changing app dir names in $INSTALL_PATH..." - fi - if [ -d "iSkyLIMS" ]; then - mv iSkyLIMS/ iskylims/ - sed -i "s/iSkyLIMS/iskylims/g" $INSTALL_PATH/iskylims/wsgi.py - sed -i "s/iSkyLIMS/iskylims/g" $INSTALL_PATH/manage.py - fi - cd - - fi - - # update installation by sinchronize folders - echo "Copying files to installation folder" - rsync -rlv conf/ $INSTALL_PATH/conf/ - rsync -rlv --fuzzy --delay-updates --delete-delay \ - --exclude "logs" --exclude "documents" --exclude "migrations" --exclude "__pycache__" \ - README.md LICENSE test conf core drylab clinic wetlab django_utils $INSTALL_PATH - - # update the settings.py and the main urls - echo "Update settings and url file." - update_settings_and_urls - # update illumina template files.# Copy illumina sample sheet templates - mkdir -p $INSTALL_PATH/documents/wetlab/templates/ - cp $INSTALL_PATH/conf/*_template.csv $INSTALL_PATH/documents/wetlab/templates/ - cp $INSTALL_PATH/conf/samples_template.xlsx $INSTALL_PATH/documents/wetlab/templates/ - - # update logging configuration file - cp $INSTALL_PATH/conf/template_logging_config.ini $INSTALL_PATH/wetlab/logging_config.ini - sed -i "s@INSTALL_PATH@${INSTALL_PATH}@g" $INSTALL_PATH/wetlab/logging_config.ini - # update the sample sheet folder and name - if [ -d "$INSTALL_PATH/documents/wetlab/SampleSheets" ]; then - echo "Updating sample sheet folder name" - mv $INSTALL_PATH/documents/wetlab/SampleSheets $INSTALL_PATH/documents/wetlab/sample_sheet - fi - - if [ -d "$INSTALL_PATH/documents/wetlab/SampleSheets4LibPrep" ]; then - echo "Updating sample sheet for libary preparationfolder name" - mv $INSTALL_PATH/documents/wetlab/SampleSheets4LibPrep $INSTALL_PATH/documents/wetlab/sample_sheets_lib_prep - fi - - cd $INSTALL_PATH - echo "activate the virtualenv" - source virtualenv/bin/activate - ### RENAME APP in database and migration files #### - if [ $ren_app == true ] ; then - - echo "Modifying database names and constraints..." - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_core", "core") WHERE app_label like ("iSkyLIMS_%");' - # mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - # -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_clinic", "clinic") WHERE app_label like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_wetlab", "wetlab") WHERE app_label like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_content_type SET app_label = REPLACE(app_label , "iSkyLIMS_drylab", "drylab") WHERE app_label like ("iSkyLIMS_%");' - - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_core", "core") WHERE app like ("iSkyLIMS_%");' - # mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - # -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_clinic", "clinic") WHERE app like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_wetlab", "wetlab") WHERE app like ("iSkyLIMS_%");' - mysql -u $DB_USER -p$DB_PASS -D $DB_NAME -h $DB_SERVER_IP \ - -e 'UPDATE django_migrations SET app = REPLACE(app , "iSkyLIMS_drylab", "drylab") WHERE app like ("iSkyLIMS_%");' - echo "Renaming tables" - query_rename_table="SELECT CONCAT('RENAME TABLE ', TABLE_SCHEMA, '.', TABLE_NAME, \ - ' TO ', TABLE_SCHEMA, '.', REPLACE(TABLE_NAME, 'iSkyLIMS_', ''), ';') \ - AS query FROM information_schema.tables WHERE TABLE_SCHEMA = \"$DB_NAME\" AND TABLE_NAME LIKE 'iSkyLIMS_%';" - mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_table" \ - | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash - echo "Renaming index" - query_rename_unique_indexes="SELECT CONCAT('ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ - ' RENAME INDEX ', rcu.CONSTRAINT_NAME, \ - ' TO ', REPLACE(rcu.CONSTRAINT_NAME, 'iSkyLIMS_', ''), ';') \ - AS query FROM information_schema.key_column_usage rcu \ - JOIN information_schema.table_constraints tc \ - ON tc.CONSTRAINT_NAME = rcu.CONSTRAINT_NAME WHERE rcu.TABLE_SCHEMA = \"$DB_NAME\" \ - AND rcu.CONSTRAINT_NAME LIKE 'iSkyLIMS_%' AND tc.CONSTRAINT_TYPE = 'UNIQUE' \ - GROUP BY rcu.TABLE_SCHEMA, rcu.TABLE_NAME, rcu.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, \ - rcu.REFERENCED_TABLE_SCHEMA, rcu.REFERENCED_TABLE_NAME;" - mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_unique_indexes" \ - | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash - echo "Renaming constraints" - query_rename_constraints="SELECT CONCAT('ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ - ' DROP FOREIGN KEY ' , rcu.CONSTRAINT_NAME, ';', \ - ' ALTER TABLE ', rcu.TABLE_SCHEMA, '.', rcu.TABLE_NAME, \ - ' ADD CONSTRAINT ', REPLACE(rcu.CONSTRAINT_NAME, 'iSkyLIMS_', ''), ' ', \ - tc.CONSTRAINT_TYPE, ' (', GROUP_CONCAT(rcu.COLUMN_NAME ORDER BY rcu.ORDINAL_POSITION SEPARATOR ', '), ')', \ - IF(tc.CONSTRAINT_TYPE = 'FOREIGN KEY', \ - CONCAT(' REFERENCES ', rcu.REFERENCED_TABLE_SCHEMA, '.', REPLACE(rcu.REFERENCED_TABLE_NAME, 'iSkyLIMS_', ''), ' (', \ - GROUP_CONCAT(rcu.REFERENCED_COLUMN_NAME ORDER BY rcu.ORDINAL_POSITION SEPARATOR ', '), ') ON DELETE ', rc.DELETE_RULE), \ - ''), ';') AS query \ - FROM information_schema.key_column_usage rcu \ - LEFT JOIN information_schema.table_constraints tc ON rcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME \ - LEFT JOIN information_schema.referential_constraints rc ON rcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME \ - WHERE rcu.TABLE_SCHEMA = '$DB_NAME' AND rcu.CONSTRAINT_NAME LIKE 'iSkyLIMS_%' \ - GROUP BY rcu.TABLE_SCHEMA, rcu.TABLE_NAME, rcu.CONSTRAINT_NAME, tc.CONSTRAINT_TYPE, rcu.REFERENCED_TABLE_SCHEMA, rcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE;" - mysql -u $DB_USER -p$DB_PASS -h $DB_SERVER_IP -e "$query_rename_constraints" | xargs -I % echo "mysql -u$DB_USER -p'$DB_PASS' -D $DB_NAME -h $DB_SERVER_IP -e \"% \" " | bash - - echo "Done modifying database names and constraints..." - - echo "Modifying names in migration files..." - sed -i 's/iSkyLIMS_core/core/g' */migrations/*.py - # sed -i 's/iSkyLIMS_clinic/clinic/g' */migrations/*.py - sed -i 's/iSkyLIMS_drylab/drylab/g' */migrations/*.py - sed -i 's/iSkyLIMS_wetlab/wetlab/g' */migrations/*.py - echo "Done modifying names in migration files..." - - # copy modified migration files - echo "Copying custom migration files from conf." - cp $INSTALL_PATH/conf/0002_core_migration_v3.0.0.py $INSTALL_PATH/core/migrations/0002_migration_v3_0_0.py - cp $INSTALL_PATH/conf/0002_drylab_migration_v3.0.0.py $INSTALL_PATH/drylab/migrations/0002_migration_v3_0_0.py - cp $INSTALL_PATH/conf/0002_wetlab_migration_v3.0.0.py $INSTALL_PATH/wetlab/migrations/0002_migration_v3_0_0.py - # cp conf/0002_clinic_migration_v2.3.1.py clinic/migrations/0002_migration_v2_3_1.py - cp $INSTALL_PATH/conf/0002_django_utils_migration_v3.0.0.py $INSTALL_PATH/django_utils/migrations/0002_migration_v3_0_0.py - - read -p "Do you want to proceed with the migrate command? (Y/N) " -n 1 -r - echo # (optional) move to a new line - if [[ ! $REPLY =~ ^[Yy]$ ]] ; then - echo "Exiting without running migrate command." - exit 1 - fi - - echo "activate the virtualenv" - source virtualenv/bin/activate - echo "Running migrate..." - python manage.py migrate - echo "Done migrate command." - - else - echo "checking for database changes" - if python manage.py makemigrations | grep -q "No changes"; then - echo "No migration is required" - else - read -p "Do you want to proceed with the migrate command? (Y/N) " -n 1 -r - echo # (optional) move to a new line - if [[ ! $REPLY =~ ^[Yy]$ ]] ; then - echo "Exiting without running migrate command." - exit 1 - fi - echo "Running migrate..." - python manage.py migrate - echo "Done migrate command." - fi - fi - - echo "Running collect statics..." - python manage.py collectstatic - echo "Done collect statics" - - if [ $tables == true ] ; then - echo "Loading pre-filled tables..." - python manage.py loaddata conf/first_install_tables.json - echo "Done loading pre-filled tables..." - fi - - if [ $run_script ]; then - for val in "${migration_script[@]}"; do - echo "Running migration script: $val" - python manage.py runscript $val - echo "Done migration script: $val" - done - fi - - cd - - - # Linux distribution - linux_distribution=$(lsb_release -i | cut -f 2-) - - echo "" - echo "Restart apache server to update changes" - if [[ $linux_distribution == "Ubuntu" ]]; then - apache_daemon="apache2" - else - apache_daemon="httpd" - fi - - # systemctl restart $apache_user + exit 0 +fi - if ! [ $? -eq 0 ]; then - echo -e "${ORANGE}Apache server restart failed. trying with sudo{NC}" - sudo systemctl restart $apache_daemon - fi +if [ "$workflow" = "bootstrap" ]; then + check_bootstrap_requirements + if [ "$workflow_mode" = "upgrade" ]; then + rename_apps_if_needed fi - printf "\n\n%s" - printf "${BLUE}------------------${NC}\n" - printf "%s" - printf "${BLUE}Successfuly upgrade of iSKyLIMS version: ${ISKYLIMS_VERSION}${NC}\n" - printf "%s" - printf "${BLUE}------------------${NC}\n\n" - # exit once upgrade is finished + bootstrap_application_runtime "$workflow_mode" exit 0 - fi -#================================================================ -# INSTALL REPOSITORY REQUIRED SOFTWARE AND PYTHON VIRTUAL ENVIRONMENT -#================================================================ - -if [ $install == true ]; then - - if [ "$install_type" == "full" ] || [ "$install_type" == "dep" ]; then - - #================================================================ - # MAIN_BODY FOR INSTALL - #================================================================ - printf "\n\n%s" - printf "${YELLOW}------------------${NC}\n" - printf "%s" - printf "${YELLOW}Starting iSkyLIMS install version: ${ISKYLIMS_VERSION}${NC}\n" - printf "%s" - printf "${YELLOW}------------------${NC}\n\n" - - user=$SUDO_USER - group=$(groups | cut -d" " -f1) - - # Find out server Linux distribution - linux_distribution=$(lsb_release -i | cut -f 2-) - - if [[ $linux_distribution == "Ubuntu" ]]; then - apache_group="www-data" - else - apache_group="apache" - fi - - echo "Starting iSkyLIMS installation" - if [ -d $INSTALL_PATH ]; then - echo "There already is an installation of iskylims in $INSTALL_PATH." - read -p "Do you want to remove current installation and reinstall? (Y/N) " -n 1 -r - echo # (optional) move to a new line - if [[ ! $REPLY =~ ^[Yy]$ ]] ; then - echo "Exiting without running iSkyLIMS installation" - exit 1 - else - rm -rf $INSTALL_PATH - fi - fi - - echo "Installing Interop" - if [ -d /opt/interop ]; then - echo "There is already an interop installation" - echo "Skipping Interop installation" - else - cd /opt - echo "Downloading interop software" - wget https://github.com/Illumina/interop/releases/download/v1.1.15/InterOp-1.1.15-Linux-GNU.tar.gz - tar -xf InterOp-1.1.15-Linux-GNU.tar.gz - ln -s InterOp-1.1.15-Linux-GNU interop - rm InterOp-1.1.15-Linux-GNU.tar.gz - echo "Interop is now installed" - cd - - fi - - if [[ $linux_distribution == "Ubuntu" ]]; then - echo "Software installation for Ubuntu" - apt-get update && apt-get upgrade -y - apt-get install -y \ - apt-utils wget \ - libmysqlclient-dev \ - python3-venv \ - libpq-dev \ - python3-dev python3-pip python3-wheel \ - apache2-dev cifs-utils \ - gnuplot - fi - - if [[ $linux_distribution == "CentOS" || $linux_distribution == "RedHatEnterprise" ]]; then - echo "Software installation for Centos/RedHat" - yum groupinstall "Development tools" - yum install zlib-devel bzip2-devel openssl-devel \ - wget httpd-devel mysql-libs sqlite sqlite-devel \ - mariadb-devel libffi-devel \ - gnuplot cifs-utils - fi +check_requirements - ## Create the installation folder - mkdir -p $INSTALL_PATH/conf - chown -R $user:$apache_group $INSTALL_PATH - chmod 775 $INSTALL_PATH - - # Copy requirements before moving to install path - rsync -rlv conf/requirements.txt $INSTALL_PATH/conf/requirements.txt - - cd $INSTALL_PATH - # install virtual environment - echo "Creating virtual environment" - if [ -d $INSTALL_PATH/virtualenv ]; then - echo "There already is a virtualenv for iskylims in $INSTALL_PATH." - read -p "Do you want to remove current virtualenv and reinstall? (Y/N) " -n 1 -r - echo # (optional) move to a new line - if [[ ! $REPLY =~ ^[Yy]$ ]] ; then - rm -rf $INSTALL_PATH/virtualenv - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - else - echo "virtualenv alredy defined. Skipping." - fi - else - bash -c "$PYTHON_BIN_PATH -m venv virtualenv" - fi - - echo "activate the virtualenv" - source virtualenv/bin/activate - - # Install python packages required for iSkyLIMS - echo "Installing required python packages" - python -m pip install wheel - python -m pip install -r conf/requirements.txt - - cd - - - if [ "$install_type" == "full" ] || [ "$install_type" == "app" ]; then - printf "\n\n%s" - printf "${BLUE}------------------${NC}\n" - printf "%s" - printf "${BLUE}Software dep are successfuly installed${NC}\n" - printf "%s" - printf "${BLUE}------------------${NC}\n\n" - else - printf "\n\n%s" - printf "${BLUE}------------------${NC}\n" - printf "%s" - printf "${BLUE}Software dep are successfuly installed${NC}\n" - printf "%s" - printf "${BLUE}------------------${NC}\n\n" - printf "\n\n%s" - printf "${RED}------------------${NC}\n" - printf "%s" - printf "${RED}Exiting${NC}\n" - printf "%s" - printf "${RED}------------------${NC}\n\n" - exit 0 - fi +if [[ "$operation_scope" == "full" || "$operation_scope" == "dep" ]]; then + run_dependency_stage "$operation" + if [ "$operation_scope" = "dep" ]; then + log_info "Dependency stage completed." + exit 0 fi +fi - #================================================================ - # INSTALL iSkyLIMS PLATFORM APPLICATION - #================================================================ - - if [ "$install_type" == "full" ] || [ "$install_type" == "app" ]; then - - if [ $LOG_TYPE == "symbolic_link" ]; then - if [ -d $LOG_PATH ]; then - ln -s $LOG_PATH $INSTALL_PATH/logs - chmod 775 $LOG_PATH - else - echo "Log folder path: $LOG_PATH does not exist. Fix it in the install_settings.txt and run again." - exit 1 - fi - else - mkdir -p $INSTALL_PATH/logs - chown $user:$apache_group $INSTALL_PATH/logs - chmod 775 $INSTALL_PATH/logs - fi - - rsync -rlv README.md LICENSE test conf core drylab \ - wetlab clinic django_utils $INSTALL_PATH - - cd $INSTALL_PATH - - # Create necessary folders - echo "Created documents structure" - mkdir -p $INSTALL_PATH/documents/wetlab - mkdir -p $INSTALL_PATH/documents/wetlab/tmp - mkdir -p $INSTALL_PATH/documents/wetlab/sample_sheet - mkdir -p $INSTALL_PATH/documents/wetlab/images_plot - mkdir -p $INSTALL_PATH/documents/wetlab/templates - mkdir -p $INSTALL_PATH/documents/wetlab/sample_sheets_lib_prep - mkdir -p $INSTALL_PATH/documents/drylab - mkdir -p $INSTALL_PATH/documents/drylab/service_files - - chown -R $user:$apache_group $INSTALL_PATH/documents - chmod 775 $INSTALL_PATH/documents - - # Copy illumina sample sheet templates - cp $INSTALL_PATH/conf/*_template.csv $INSTALL_PATH/documents/wetlab/templates/ - cp $INSTALL_PATH/conf/samples_template.xlsx $INSTALL_PATH/documents/wetlab/templates/ - - # update logging configuration file - cp $INSTALL_PATH/conf/template_logging_config.ini $INSTALL_PATH/wetlab/logging_config.ini - sed -i "s|INSTALL_PATH|${INSTALL_PATH}|g" $INSTALL_PATH/wetlab/logging_config.ini - - # Starting iSkyLIMS - echo "activate the virtualenv" - source virtualenv/bin/activate - - echo "Creating iskylims project" - django-admin startproject iskylims . - - # update the settings.py and the main urls - update_settings_and_urls - - if [ $docker == false ]; then - echo "Creating the database structure for iSkyLIMS" - python manage.py migrate - python manage.py makemigrations django_utils core wetlab drylab - python manage.py migrate - echo "Loading in database initial data" - python manage.py loaddata conf/first_install_tables.json - echo "Creating super user " - python manage.py createsuperuser --username admin - fi - - # copy static files - echo "Run collectstatic" - python manage.py collectstatic - - cd - - - printf "\n\n%s" - printf "${BLUE}------------------${NC}\n" - printf "%s" - printf "${BLUE}Successfuly iSkyLIMS Installation version: ${ISKYLIMS_VERSION}${NC}\n" - printf "%s" - printf "${BLUE}------------------${NC}\n\n" - - echo "Installation completed" - exit 0 +if [[ "$operation_scope" == "full" || "$operation_scope" == "app" ]]; then + if [ "$operation" = "install" ]; then + install_application_files + else + upgrade_application_files + fi + if [ $restart_apache == true ]; then + restart_apache_service fi + exit 0 fi printf "\n\n%s" diff --git a/leame_actualizaciones_previas.md b/leame_actualizaciones_previas.md new file mode 100644 index 000000000..c68cf701a --- /dev/null +++ b/leame_actualizaciones_previas.md @@ -0,0 +1,288 @@ +# iSkyLIMS + +[![Django](https://img.shields.io/static/v1?label=Django&message=4.2&color=azul?style=plastic&logo=django)](https://github.com/django/django) +[![Python](https://img.shields.io/static/v1?label=Python&message=3.8.10&color=verde?style=plastic&logo=Python)](https://www.python.org/) +[![Bootstrap](https://img.shields.io/badge/Bootstrap-v5.0-azulvioleta?style=plastic&logo=Bootstrap)](https://getbootstrap.com) +[![versión](https://img.shields.io/badge/versión-3.0.0-naranja?style=plastic&logo=GitHub)](https://github.com/BU-ISCIII/iskylims.git) + +La introducción de la secuenciación masiva (MS) en las instalaciones de genómica ha significado un crecimiento exponencial en la generación de datos, lo que requiere un sistema de seguimiento preciso, desde la preparación de la biblioteca hasta la generación de archivos fastq, el análisis y la entrega al investigador. El software diseñado para manejar esas tareas se llama Sistemas de Gestión de Información de Laboratorio (LIMS), y su software debe adaptarse a las necesidades particulares de su laboratorio de genómica. iSkyLIMS nace con el objetivo de ayudar con las tareas de laboratorio húmedo e implementar un flujo de trabajo que guíe a los laboratorios de genómica en sus actividades, desde la preparación de la biblioteca hasta la producción de datos, reduciendo los posibles errores asociados a la tecnología de alto rendimiento y facilitando el control de calidad de la secuenciación. Además, iSkyLIMS conecta el laboratorio húmedo con el laboratorio seco, facilitando el análisis de datos por parte de bioinformáticos. + +![Imagen](img/iskylims_scheme.png) + +De acuerdo con la infraestructura existente, la secuenciación se realiza en un instrumento Illumina NextSeq. Los datos se almacenan en un dispositivo de almacenamiento masivo NetApp y los archivos fastq se generan (bcl2fastq) en un clúster de cómputo de alto rendimiento Sun Grid Engine (SGE-HPC). Los servidores de aplicaciones ejecutan aplicaciones web para el análisis bioinformático (GALAXY), la aplicación iSkyLIMS y alojan la capa de información de MySQL. El flujo de trabajo de iSkyLIMS WetLab se ocupa del seguimiento y las estadísticas de la ejecución de la secuenciación. El seguimiento de la ejecución pasa por cinco estados: "registrado", el usuario de genómica registra la nueva ejecución de la secuenciación en el sistema, el proceso esperará hasta que la ejecución se complete en la máquina y los datos se transfieran al dispositivo de almacenamiento masivo; "Envío de hoja de muestra", el archivo de hoja de muestra con la información de la ejecución de la secuenciación se copiará en la carpeta de ejecución para el proceso de bcl2fastq; "Procesamiento de datos", se procesan los archivos de parámetros de ejecución y los datos se almacenan en la base de datos; "Estadísticas en ejecución", los datos de desmultiplexación generados en el proceso de bcl2fastq se procesan y almacenan en la base de datos, "Completado", todos los datos se procesan y almacenan correctamente. Se proporcionan estadísticas por muestra, por proyecto, por ejecución y por investigación, así como informes anuales y mensuales. El flujo de trabajo de iSkyLIMS DryLab se encarga de la solicitud de servicios de bioinformática y estadísticas. El usuario solicita servicios que pueden estar asociados con una ejecución de secuenciación. Se proporciona seguimiento de estadísticas y servicios. + +- [iSkyLIMS](#iskylims) + - [Instalación](#instalación) + - [Requisitos previos](#requisitos-previos) + - [Instalación de iSkyLIMS en Docker](#instalación-de-iskylims-en-docker) + - [Instalación de iSkyLIMS en su servidor con Ubuntu/CentOS](#instalación-de-iskylims-en-su-servidor-con-ubuntucentos) + - [Clonar el repositorio de GitHub](#clonar-el-repositorio-de-github) + - [Crear la base de datos de iSkyLIMS y otorgar permisos](#crear-la-base-de-datos-de-iskylims-y-otorgar-permisos) + - [Configuración de ajustes](#configuración-de-ajustes) + - [Ejecutar el script de instalación](#ejecutar-el-script-de-instalación) + - [Actualización a la versión 3.0.0 de iSkyLIMS](#actualización-a-la-versión-300-de-iskylims) + - [Prerrequisitos](#prerrequisitos) + - [Clonar el repositorio de GitHub](#clonar-el-repositorio-de-github-1) + - [Configuración de opciones](#configuración-de-opciones) + - [Ejecución del script de actualización](#ejecución-del-script-de-actualización) + - [Pasos que necesitan permisos de adminsitración](#pasos-que-necesitan-permisos-de-adminsitración) + - [Pasos que no necesitan de permisos de administración](#pasos-que-no-necesitan-de-permisos-de-administración) + - [Qué hacer si algo falla](#qué-hacer-si-algo-falla) + - [Pasos finales de configuración](#pasos-finales-de-configuración) + - [Configuración de SAMBA](#configuración-de-samba) + - [Verificación de correo electrónico](#verificación-de-correo-electrónico) + - [Configurar el servidor Apache](#configurar-el-servidor-apache) + - [Verificación de la instalación](#verificación-de-la-instalación) + - [Documentación de iSkyLIMS](#documentación-de-iskylims) + +## Instalación + +Si tienes algún problema o deseas informar de algún error, por favor, publícalo en [issue](https://github.com/BU-ISCIII/iSkyLIMS/issues) + +### Requisitos previos + +Antes de comenzar la instalación, asegúrate de lo siguiente: + +- Tienes privilegios de **sudo** para instalar los paquetes de software adicionales que iSkyLIMS necesita. +- Dependencias: + - Librerías: +``` + yum groupinstall "Development tools" + yum install zlib-devel bzip2-devel openssl-devel \ + wget httpd-devel mysql-libs sqlite sqlite-devel \ + mariadb-devel mysql-client libffi-devel \ + gnuplot cifs-utils +``` + - lsb_relase: + - RedHat/CentOS: `yum install redhat-lsb-core` + - Ubuntu: `apt install lsb-core lsb-release` +- Base de datos MySQL > 8.0 o MariaDB > 10.4 +- Tienes configurado un servidor local para enviar correos electrónicos. +- git > 2.34 +- Tienes Apache servidor v2.4 +- Tienes Python > 3.8 (si lo compilas debes haber instalado previamente las dependecias de arriba) +- Tienes una conexión a la carpeta compartida de Samba donde se almacenan las carpetas de ejecución (por ejemplo, galera/NGS_Data). +- Dependencias: + + +### Instalación de iSkyLIMS en Docker + +Puedes probar iSkyLIMS creando un contenedor Docker en tu máquina local. + +Clona el repositorio de GitHub de iSkyLIMS y ejecuta el script de Docker para crear el contenedor Docker. + +```bash +git clone https://github.com/BU-ISCIII/iSkyLIMS.git iSkyLIMS +sudo bash docker_install.sh +``` + +El script crea un contenedor de Docker Compose con 3 servicios: + +- web1: contiene la aplicación web iSkyLIMS +- db1: contiene la base de datos MySQL +- samba: contiene el servidor Samba + +Después de crear Docker y tener los servicios en funcionamiento, la estructura de la base de datos y los datos iniciales se cargan en la base de datos. Cuando se complete este paso, se le pedirá que defina al superusuario que tendrá acceso a las páginas de administración de Django. Puede escribir cualquier nombre, pero recomendamos que utilice "admin", ya que más adelante se le pedirá un usuario administrador cuando defina la configuración inicial. + +Siga el mensaje de instrucciones para crear la cuenta del superusuario. + +Cuando el script finalice, abra su navegador escribiendo **localhost:8001** para acceder a iSkyLIMS + +### Instalación de iSkyLIMS en su servidor con Ubuntu/CentOS + +#### Clonar el repositorio de GitHub + +Abra una terminal de Linux y vaya a un directorio donde se descargará el código de iSkyLIMS + +```bash +cd +git clone https://github.com/BU-ISCIII/iskylims.git iskylims +cd iskylims +``` + +#### Crear la base de datos de iSkyLIMS y otorgar permisos + +1. Cree una nueva base de datos llamada "iskylims" (esto es obligatorio). +2. Cree un nuevo usuario con permisos para leer y modificar esa base de datos. +3. Anote el nombre de usuario, la contraseña y la información del servidor de la base de datos. + +#### Configuración de ajustes + +Copia la plantilla de ajustes iniciales en un archivo llamado `install_settings.txt` + +```bash +cp conf/template_install_settings.txt install_settings.txt +``` + +Abra el archivo de configuración con su editor favorito para establecer sus propios valores para la base de datos, la configuración de correo electrónico y la dirección IP local del servidor donde se ejecutará iSkyLIMS. + +```bash +sudo nano install_settings.txt +``` + +#### Ejecutar el script de instalación + +iSkyLIMS debe instalarse en el directorio "/opt". + +Necesitará privilegios de administrador para instalar las dependencias. Para manejar diferentes responsabilidades de instalación dentro de la organización, donde es posible que no sea la persona con privilegios de administrador, nuestro script de instalación tiene estas opciones en el parámetro `--install`: + +- `dep`: para instalar los paquetes de software, así como los paquetes de Python dentro del entorno virtual. Se necesita permisos de administrador. +- `app`: para instalar solo el software de la aplicación iSkyLIMS sin necesidad de tener permisos de administrador. +- `full`: si tiene directamente permisos de administrador, puede instalar tanto las dependencias como la aplicación con esta opción. + +Ejecute uno de los siguientes comandos en una terminal de Linux para la instalación, de acuerdo con la descripción anterior. + +```bash +# para instalar solo las dependencias +sudo bash install.sh --install dep + +# para instalar la aplicación iskylims +bash install.sh --install app + +# para instalar ambos al mismo tiempo +sudo bash install.sh --install full +``` + +### Actualización a la versión 3.0.0 de iSkyLIMS + +Si ya tienes iSkyLIMS en la versión 2.3.0, puedes actualizar a la última versión estable, la 3.0.0. + +La versión 3.0.0 es una versión importante con actualizaciones significativas en dependencias de terceros como Bootstrap. También hemos realizado un gran trabajo en la refactorización y el cambio de nombres de variables/funciones que afectan a la base de datos. Para obtener más detalles sobre los cambios, consulta las notas de la versión. + +#### Prerrequisitos + +Debido a que en esta actualización se modifican muchas tablas en la base de datos, es necesario que hagas una copia de seguridad de: + +- La base de datos de iSkyLIMS. +- La carpeta de iSkyLIMS (carpeta de instalación completa, por ejemplo, /opt/iSkyLIMS). + +Se recomienda encarecidamente que hagas estas copias de seguridad y las guardes de manera segura en caso de que la actualización falle, para poder recuperar tu sistema. Por ejemplo crea una carpeta en `/home/dadmin/backup_pro` que contenga la base de datos y la carpeta de /opt/iskylims para tenerla a mano y poder [restaurar el sistema](#qué-hacer-si-algo-falla). + +#### Clonar el repositorio de GitHub + +También hemos cambiado la forma en que se instala y actualiza iSkyLIMS. A partir de ahora, iSkyLIMS se descarga en una carpeta del usuario y se instala en otro lugar (por ejemplo, /opt/). + +Abre una terminal de Linux y dirígete a un directorio donde se descargará el código de iSkyLIMS. + +```bash +cd < directorio distinto al directorio de instalación > +git clone https://gitlab.isciii.es/BU-ISCIII/iskylims.git iskylims +cd iskylims +``` + +#### Configuración de opciones + +Copia la plantilla de configuración inicial en un archivo llamado install_settings.txt + +```bash +cp conf/template_install_settings.txt install_settings.txt +``` + +Abre el archivo de configuración con tu editor favorito para establecer tus propios valores para la base de datos, la configuración de correo electrónico y la dirección IP local del servidor donde se ejecutará iSkyLIMS. +> Si utilizas un sistema basado en Windows para modificar el archivo, asegúrate de que el archivo se guarde con una codificación amigable para Linux, como ASCII o UTF-8. + +```bash +nano install_settings.txt +``` + +#### Ejecución del script de actualización + +Si en tu organización se requiere que las dependencias u otros elementos que necesiten permisos de administrador sean instalados por una persona diferente a la que instala la aplicación, puedes utilizar el script de instalación en varios pasos de la siguiente manera. + +El script te irá solicitando confirmación en algun paso, si todo está yendo bien sin errores deberás pulsar `y` o `yes` según te lo solicite. + +> Nota: Los errores: "ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'query' at line 1" son normales ya que se trata del título de las sentencias sql que no deben ejecutarse. Se puede ignorar. + +##### Pasos que necesitan permisos de adminsitración + +En primer lugar, debes cambiar el nombre de la carpeta de la aplicación en la carpeta de instalación (`/opt/iSkyLIMS`): + +```bash +# Necesitas ser usuario root para realizar esta operación +sudo mv /opt/iSkyLIMS /opt/iskylims +``` + +Asegúrate de que la carpeta de instalación tenga los permisos correctos para que la persona que instala la aplicación pueda escribir en esa carpeta. + +```bash +# En el caso de que tengas un script para esta tarea. Necesitarás ajustar este script de acuerdo al cambio en el nombre de la ruta: /opt/iSkyLIMS a /opt/iskylims +sudo /scripts/hardening.sh +``` + +En la terminal de Linux, ejecuta uno de los siguientes comandos que mejor se adapte a ti: + +```bash +# para actualizar solo las dependencias del software. ES NECESARIO DISPONER DE PERMISOS DE ROOT. +sudo bash install.sh --upgrade dep + +# PARA INSTALAR AMBAS COSAS AL MISMO TIEMPO. REQUIERE DE ROOT. SI SE VA A INSTALAR POR OTRA PERSONA SIN ROOT NO HACER ESTO. +sudo bash install.sh --upgrade full --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables +``` + +##### Pasos que no necesitan de permisos de administración + +A continuación instalamos la aplicación de iskylims usando el siguiente comando: + +```bash +# para actualizar la aplicación de iskylims, incluyendo los cambios necesarios para la versión en base de datos. NO ES NECESARIO DISPONER DE PERMISOS ROOT. +bash install.sh --upgrade app --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables +``` + +Por último, asegúrate que los permisos de la carpeta son correctos. + +```bash +# En el caso de que tengas un script para esta tarea. En esta versión han cambiado algunas rutas a ficheros, es posible que tengas que ajustar el script en consecuencia. +sudo /scripts/hardening.sh +``` + +#### Qué hacer si algo falla + +Cuando actualizamos la aplicación usando el script estamos realizando varios cambios en la base de datos. Si algo falla tenemos que restaurar el estado anterior, antes de que hubiesemos realizado ninguna acción. + +Necesitamos copiar de vuelta nuestro backup de carpet ade aplicación a /opt/iSkyLIMS (o la carpeta de instalación de nuestra elección), y restaurar la base de datos realizando algo como lo siguiente: + +```bash +sudo rm -rf /opt/iskylims +sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ +sudo /scripts/hardening.sh +mysql -u iskylims -h dmysqlps.isciiides.es -p +# drop database iskylims; +# create database iskylims; +mysql -u iskylims -h dmysqlps.isciiides.es iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_202310160737.sql +``` + +### Pasos finales de configuración + +#### Configuración de SAMBA + +- Inicia sesión con la cuenta de administrador. +- Ve a Massive Sequencing +![Ir a WetLab](img/got_to_wetlab.png){width:50px} +- Ve a Configuración -> Configuración de SAMBA +- Completa el formulario con los parámetros apropiados para la carpeta compartida de SAMBA: +![Formulario SAMBA](img/samba_form.png) + +#### Verificación de correo electrónico + +- Ve a Massive Sequencing +- Ve a Configuración -> Configuración de correo electrónico +- Completa el formulario con los parámetros necesarios para la configuración de correo electrónico y trata de enviar un correo de prueba. + +#### Configurar el servidor Apache + +Copia el archivo de configuración de Apache que se encuentra en la carpeta `conf` según tu distribución dentro del directorio de configuración de Apache y cambia el nombre a iskylims.conf. Revisa cualquier requerimiento de tu sistema, se trata solo de un ejemplo. + +#### Verificación de la instalación + +Abre el navegador y escribe "localhost" o la "IP local del servidor" para comprobar que iSkyLIMS está en funcionamiento. + +También puedes verificar algunas funcionalidades mientras compruebas las conexiones de SAMBA y la base de datos usando: + +- Ve a [configurationTest](https://iskylims.isciii.es/wetlab/configurationTest/) +- Haz clic en Enviar +- Verifica todas las pestañas para asegurarte de que cada conexión sea exitosa. +- Ejecuta las 3 pruebas para cada máquina de secuenciación: MiSeq, NextSeq y NovaSeq. + +### Documentación de iSkyLIMS + +La documentación de iSkyLIMS está disponible en [https://iskylims.readthedocs.io/en/latest](https://iskylims.readthedocs.io/en/latest) diff --git a/readme_upgrade_docs_previous.md b/readme_upgrade_docs_previous.md new file mode 100644 index 000000000..66a597a94 --- /dev/null +++ b/readme_upgrade_docs_previous.md @@ -0,0 +1,290 @@ +# iSkyLIMS + +[![Django](https://img.shields.io/static/v1?label=Django&message=4.2&color=blue?style=plastic&logo=django)](https://github.com/django/django) +[![Python](https://img.shields.io/static/v1?label=Python&message=3.8.10&color=green?style=plastic&logo=Python)](https://www.python.org/) +[![Bootstrap](https://img.shields.io/badge/Bootstrap-v5.0-blueviolet?style=plastic&logo=Bootstrap)](https://getbootstrap.com) +[![version](https://img.shields.io/badge/version-3.0.0-orange?style=plastic&logo=GitHub)](https://github.com/BU-ISCIII/iskylims.git) + +The introduction of massive sequencing (MS) in genomics facilities has meant an exponential growth in data generation, requiring a precise tracking system, from library preparation to fastq file generation, analysis and delivery to the researcher. Software designed to handle those tasks are called Laboratory Information Management Systems (LIMS), and its software has to be adapted to their own genomics laboratory particular needs. iSkyLIMS is born with the aim of helping with the wet laboratory tasks, and implementing a workflow that guides genomics labs on their activities from library preparation to data production, reducing potential errors associated to high throughput technology, and facilitating the quality control of the sequencing. Also, iSkyLIMS connects the wet lab with dry lab facilitating data analysis by bioinformaticians. + +![Image](img/iskylims_scheme.png) + +According to existent infrastructure sequencing is performed on an Illumina NextSeq instrument. Data is stored in NetApp mass storage device and fastq files are generated (bcl2fastq) on a Sun Grid Engine High Performance Computing cluster (SGE-HPC). +Application servers run web applications for bioinformatics analysis (GALAXY), the iSkyLIMS app, and host the MySQL information tier. iSkyLIMS WetLab workflow deals with sequencing run tracking and statistics. Run tracking passes through five states: "recorded” genomics user record the new sequencing run into the system, the process will wait till run is completed by the machine and data is transferred to the mass storage device; “Sample sheet sent” sample sheet file with the sequencing run information will be copied to the run folder for bcl2fastq process; “Processing data” run parameters files are processed and data is stored in the database; “Running stats” demultiplexing data generated in bcl2fastq process is processed and stored into the database, “Completed” all data is processed and stored successfully. Statistics per sample, per project, per run and per investigation are provided, as well as annual and monthly reports. iSkyLIMS DryLab workflow deals with bioinformatics services request and statistics. User request services that can be associated with a sequencing run. Stats and services tracking is provided. + +- [iSkyLIMS](#iskylims) + - [Installation](#installation) + - [Pre-requisites](#pre-requisites) + - [iSkyLIMS docker installation](#iskylims-docker-installation) + - [Install iSkyLIMS in your server running ubuntu/CentOS](#install-iskylims-in-your-server-running-ubuntucentos) + - [Clone github repository](#clone-github-repository) + - [Create iskylims database and grant permissions](#create-iskylims-database-and-grant-permissions) + - [Configuration settings](#configuration-settings) + - [Run installation script](#run-installation-script) + - [Upgrade to iSkyLIMS version 3.0.0](#upgrade-to-iskylims-version-300) + - [Pre-requisites](#pre-requisites-1) + - [Clone github repository](#clone-github-repository-1) + - [Configuration settings](#configuration-settings-1) + - [Running upgrade script](#running-upgrade-script) + - [Steps requiring root](#steps-requiring-root) + - [Steps not requiring root](#steps-not-requiring-root) + - [What to do if something fails](#what-to-do-if-something-fails) + - [Final configuration steps](#final-configuration-steps) + - [SAMBA configurarion](#samba-configurarion) + - [Email verification](#email-verification) + - [Configure Apache server](#configure-apache-server) + - [Verification of the installation](#verification-of-the-installation) + - [iSkyLIMS documentation](#iskylims-documentation) + +## Installation + +For any problems or bug reporting please post us an [issue](https://github.com/BU-ISCIII/iSkyLIMS/issues) + +### Pre-requisites + +Before starting the installation make sure : + +- You have **sudo privileges** to install the additional software packets that iSkyLIMS needs. +- Database MySQL > 8.0 or MariaDB > 10.4 +- Local server configured for sending emails +- Apache server v2.4 +- git > 2.34 +- Python > 3.8 +- Connection to samba shared folder where run folders are stored (p.e galera/NGS_Data) +- Dependencies: + - lsb_release: + - RedHat/CentOS: ```yum install redhat-lsb-core``` + - Ubuntu: ```apt install lsb-core lsb-release``` + +### iSkyLIMS docker installation + +You can test iSkyLIMS by creating a docker container on your local machine. + +Clone the iSkyLIMS github repository and run the docker script to create the docker + +```bash +git clone https://github.com/BU-ISCIII/iSkyLIMS.git iSkyLIMS +sudo bash docker_install.sh +``` + +The script creates a docker compose container with 3 services: + +- web1: contains the iSkyLIMS web application +- db1: contains the mySQL database +- samba: contains samba server + +After Docker is created and services are up, database structure and initial data are loaded into database. When this step is completed, you will be asked to define the super user which will have access to django admin pages. You can type any name, but we recommend that you use "admin", because admin user is requested later on when defining the initial settings. + +Follow the prompt message to create the super user account. + +When script ends open your navigator typing **localhost:8001** to access to iSkyLIMS + +### Install iSkyLIMS in your server running ubuntu/CentOS + +#### Clone github repository + +Open a linux terminal and move to a directory where iSkyLIMS code will be +downloaded + +```bash +cd < your personal folder > +git clone https://github.com/BU-ISCIII/iskylims.git iskylims +cd iskylims +``` + +#### Create iskylims database and grant permissions + +1. Create a new database named "iskylims" (this is mandatory) +2. Create a new user with permission to read and modify that database. +3. Write down user, passwd and db server info. + +#### Configuration settings + +Copy the initial setting template into a file named install_settings.txt + +```bash +cp conf/template_install_settings.txt install_settings.txt +``` + +Open with your favourite editor the configuration file to set your own values for +database ,email settings and the local IP of the server where iSkyLIMS will run. + +```bash +nano install_settings.txt +``` + +#### Run installation script + +iSkyLIMS should be installed on the "/opt" directory. + +You will need sudo privileges for installing dependencies. In order to handle different installation responsibilities inside the organization, where you may not be the person with root privileges, our instalation script has these options in ```--install``` parameter: + +- dep: to install the software packages as well as python packages inside the virtual environment. Root is needed. +- app: to install only the iSkyLIMS application software without need of being root. +- full: if you directly have root permissions you can install both deps and app at the same time with this option. + +Execute one of the following commands in a linux terminal to install, according as +above description. + +```bash +# to install only software packages dependences +sudo bash install.sh --install dep + +# to install only iSkyLIMS application +bash install.sh --install app + +# to install both software +sudo bash install.sh --install full +``` + +### Upgrade to iSkyLIMS version 3.0.0 + +If you have already iSkyLIMS on version 2.3.0 you can upgrade to the latest stable version 3.0.0. + +Version 3.0.0 is a major release with important upgrades in third parties dependencies like bootstrap. Also, we 've done a huge work on refactoring and variables/function renaming that affects the database. For more details about the changes see the release notes. + +If you requires to upgrade from version 3.0.0 to the latest one 3.1.x and in your system you have already defined library pools, then you need to collect this data, before to run the upgrade script. For more information read [Pre-requisites for upgrade from 3.0.0 to 3.1.x](#pre-requisites-for-upgrade-from-30x-to-31x) + +#### Pre-requisites + +Because in this upgrade many tables in database are modified it is required that you backup: + +- iSkyLIMS database +- iSkyLIMS folder (complete installation folder, p.e /opt/iSkyLIMS) + +##### Pre-requisites for upgrade from 3.0.x to 3.1.x + +- Perform a backup of LibraryPool by running the folowing command + +```bash + mysql --user= --password= --host= --port= iskylims -e "SELECT* FROM wetlab_library_pool" > +``` + +It is highly recomended that you made these backups and keep them safely in case of upgrade failure, to recover your system. + +#### Clone github repository + +We've also change the way that iSkyLIMS is installed and upgraded. From now on iskylims is downloaded in a user folder and installed elsewhere (p.e /opt/). + +Open a linux terminal and move to a directory where iSkyLIMS code will be +downloaded + +```bash +cd < your personal folder > +git clone https://github.com/BU-ISCIII/iSkyLIMS.git iskylims +cd iskylims +``` + +#### Configuration settings + +Copy the initial setting template into a file named install_settings.txt + +```bash +cp conf/template_install_settings.txt install_settings.txt +``` + +Open with your favourite editor the configuration file to set your own values for +database ,email settings and the local IP of the server where iSkyLIMS will run. +> If you use a windows-based system for modifying the file, make sure the file is saved using a linux-friendly encoding like ASCII or UTF-8 + +```bash +sudo nano install_settings.txt +``` + +#### Running upgrade script + +If your organization requires that dependencies / stuff that needs root are installed by a different person that install the application the you can use the install script in several steps as follows. + +First you need to rename the folder app name in the installation folder (`/opt/iSkyLIMS`): + +##### Steps requiring root + +```bash +# You need root for this operation +sudo mv /opt/iSkyLIMS /opt/iskylims +``` + +Make sure that the installation folder has the correct permissions so the person installing the app can write in that folder. + +```bash +# In case you have a script for this task. You'll need to adjust this script according to the name changing: /opt/iSkyLIMS to /opt/iskylims +/scripts/hardening.sh +``` + +In the linux terminal execute one of the following command that fit better to you: + +```bash +# to upgrade only software packages dependences. NEEDS ROOT. +sudo bash install.sh --upgrade dep + +# to install both software. NEEDS ROOT. +sudo bash install.sh --upgrade full --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables +``` + +##### Steps not requiring root + +Next you need to upgrade iskylims app. Please use the command below: + +```bash +# to upgrade only iSkyLIMS application including changes required in this release. DOES NOT NEED ROOT. +bash install.sh --upgrade app --ren_app --script drylab_service_state_migration --script rename_app_name --script rename_sample_sheet_folder --script migrate_sample_type --script migrate_optional_values --tables +``` + +Make sure that the installation folder has the correct permissions. + +```bash +# In case you have a script for this task. Some paths have changed in this version, so you may need to adjust your hardening script. +/scripts/hardening.sh +``` + +#### What to do if something fails + +When we upgrade using the installation script we are performing several changes in the database. If something fails we need to restore the app situation before anything happened and start all over. + +We need to copy back the full `/opt/iSkyLIMS` folder back to `/opt` (or your installation path preference), and restore the database doing something like this: + +```bash +sudo rm -rf /opt/iskylims +sudo cp -r /home/dadmin/backup_prod/iSkyLIMS/ /opt/ +sudo /scripts/hardening.sh +mysql -u iskylims -h dmysqlps.isciiides.es +# drop database iskylims; +# create database iskylims; +mysql -u iskylims -h dmysqlps.isciiides.es iskylims < /home/dadmin/backup_prod/bk_iSkyLIMS_202310160737.sql +``` + +### Final configuration steps + +#### SAMBA configurarion + +- Login with admin account. +- Go to Massive sequencing +![go_to_wetlab](img/got_to_wetlab.png){width:50px} +- Go to Configuration -> Samba configuration +- Fill the form with the appropiate params for the samba shared folder: +![samba form](img/samba_form.png) + +#### Email verification + +- Go to Massive sequencing +- Go to Configuration -> Email configuration +- Fill the form with the needed params for your email configuration and try to send a test email. + +#### Configure Apache server + +Copy the apache configuration file according to your distribution inside the apache configutation directory and rename it to iskylims.conf + +#### Verification of the installation + +Open the navigator and type "localhost" or the "server local IP" and check that iSkyLIMs is running. + +You can also check some of the functionality, while also checking samba and database connections using: + +- Go to [configuration test](https://iskylims.isciii.es/wetlab/configurationTest/) +- Click submit +- Check all tabs so every connectin is successful. +- Run the 3 tests for each sequencing machine: MiSeq, NextSeq and NovaSeq. + +### iSkyLIMS documentation + +iSkyLIMS documentation is available at [https://iskylims.readthedocs.io/en/latest](https://iskylims.readthedocs.io/en/latest) diff --git a/scripts/container_start.sh b/scripts/container_start.sh new file mode 100644 index 000000000..c55adb46b --- /dev/null +++ b/scripts/container_start.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_DIR="${APP_INSTALL_PATH:-/opt/iskylims}" +CRON_DIR="${APP_DIR}/cron" +TMP_DIR="${APP_DIR}/tmp" +CRON_FILE="${CRON_DIR}/iskylims" +CRON_LOG="${TMP_DIR}/supercronic.log" +APP_MODE="${APP_MODE:-prod}" +APP_PORT="${APP_PORT:-8001}" +PROJECT_MODULE="${PROJECT_MODULE:-iskylims}" +GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}" +GUNICORN_KEEPALIVE="${GUNICORN_KEEPALIVE:-5}" +GUNICORN_THREADS="${GUNICORN_THREADS:-2}" + +if [ ! -f "${APP_DIR}/manage.py" ]; then + echo "Application entrypoint not found at ${APP_DIR}/manage.py" >&2 + ls -la "${APP_DIR}" >&2 || true + exit 1 +fi + +if [ ! -f "${APP_DIR}/virtualenv/bin/activate" ]; then + echo "Virtualenv activation script not found at ${APP_DIR}/virtualenv/bin/activate" >&2 + ls -la "${APP_DIR}/virtualenv" >&2 || true + exit 1 +fi + +source "${APP_DIR}/virtualenv/bin/activate" + +mkdir -p "${CRON_DIR}" "${TMP_DIR}" + +safe_chmod() { + local mode="$1" + shift + local path + for path in "$@"; do + if [ ! -e "${path}" ]; then + continue + fi + if [ -O "${path}" ]; then + chmod "${mode}" "${path}" + else + echo "Skipping chmod ${mode} on ${path}: not owned by $(id -un)." + fi + done +} + +safe_chmod 700 "${CRON_DIR}" "${TMP_DIR}" + +if [ "$APP_MODE" = "dev" ]; then + exec python "${APP_DIR}/manage.py" runserver "0.0.0.0:${APP_PORT}" +fi + +if command -v supercronic >/dev/null 2>&1; then + # Ensure django-crontab definitions are installed in user crontab first. + python "${APP_DIR}/manage.py" crontab add >/dev/null 2>&1 || true + crontab -l 2>/dev/null | sed '/^\s*#/d; /^\s*$/d' > "${CRON_FILE}" || true + if [ -s "${CRON_FILE}" ]; then + safe_chmod 600 "${CRON_FILE}" + : > "${CRON_LOG}" + supercronic "${CRON_FILE}" > "${CRON_LOG}" 2>&1 & + CRON_PID=$! + sleep 1 + if ! kill -0 "${CRON_PID}" 2>/dev/null; then + echo "supercronic failed to start. Check ${CRON_LOG} for details." + fi + else + echo "No cron entries found. Skipping crond start." + fi +else + echo "supercronic not found. Skipping cron." +fi + +if [ -n "${WEB_CONCURRENCY:-}" ]; then + GUNICORN_WORKERS="${WEB_CONCURRENCY}" +else + cpu_count="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 1)" + if [ "${cpu_count}" -le 2 ]; then + GUNICORN_WORKERS=2 + else + GUNICORN_WORKERS=4 + fi +fi + +exec gunicorn "${PROJECT_MODULE}.wsgi:application" \ + --bind "0.0.0.0:${APP_PORT}" \ + --workers "${GUNICORN_WORKERS}" \ + --threads "${GUNICORN_THREADS}" \ + --keep-alive "${GUNICORN_KEEPALIVE}" \ + --timeout "${GUNICORN_TIMEOUT}" \ + --worker-tmp-dir /dev/shm diff --git a/test/test_data.json b/test/test_data.json index 6b8826535..b87102667 100644 --- a/test/test_data.json +++ b/test/test_data.json @@ -204,7 +204,7 @@ "model": "core.userlotcommercialkits", "pk": 1, "fields": { - "user": 1, + "user": 2, "based_commercial": 1, "uses_number": 89, "chip_lot": "23123123123", @@ -218,7 +218,7 @@ "model": "core.userlotcommercialkits", "pk": 2, "fields": { - "user": 1, + "user": 2, "based_commercial": 2, "uses_number": 0, "chip_lot": "1341425345", @@ -252,7 +252,6 @@ "sample_project_field_used": true, "sample_project_field_type": "String", "sample_project_option_list": "", - "sample_project_searchable": true, "generated_at": "2023-07-25T16:19:01.096" } }, @@ -268,7 +267,6 @@ "sample_project_field_used": true, "sample_project_field_type": "String", "sample_project_option_list": "", - "sample_project_searchable": true, "generated_at": "2023-07-25T16:19:01.104" } }, @@ -284,7 +282,6 @@ "sample_project_field_used": true, "sample_project_field_type": "String", "sample_project_option_list": "", - "sample_project_searchable": true, "generated_at": "2023-07-25T16:19:01.108" } }, @@ -296,7 +293,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "22K0483", @@ -320,7 +317,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0173", @@ -344,7 +341,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0175", @@ -368,7 +365,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0176", @@ -392,7 +389,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0177", @@ -416,7 +413,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0178", @@ -440,7 +437,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0184", @@ -464,7 +461,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0185", @@ -488,7 +485,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0186", @@ -512,7 +509,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0187", @@ -536,7 +533,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0188", @@ -560,7 +557,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0189", @@ -584,7 +581,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0192", @@ -608,7 +605,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0193", @@ -632,7 +629,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0194", @@ -656,7 +653,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0195", @@ -680,7 +677,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Eco0196", @@ -704,7 +701,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Entb0011", @@ -728,7 +725,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Entb0088", @@ -752,7 +749,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Entb0089", @@ -776,7 +773,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Entb0090", @@ -800,7 +797,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0523", @@ -824,7 +821,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0524", @@ -848,7 +845,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0525", @@ -872,7 +869,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0526", @@ -896,7 +893,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0527", @@ -920,7 +917,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0528", @@ -944,7 +941,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0529", @@ -968,7 +965,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0530", @@ -992,7 +989,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0531", @@ -1016,7 +1013,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0532", @@ -1040,7 +1037,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0533", @@ -1064,7 +1061,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0534", @@ -1088,7 +1085,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0535", @@ -1112,7 +1109,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0536", @@ -1136,7 +1133,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0537", @@ -1160,7 +1157,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0538", @@ -1184,7 +1181,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0539", @@ -1208,7 +1205,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0540", @@ -1232,7 +1229,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0541", @@ -1256,7 +1253,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0542", @@ -1280,7 +1277,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0543", @@ -1304,7 +1301,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0544", @@ -1328,7 +1325,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0545", @@ -1352,7 +1349,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0546", @@ -1376,7 +1373,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0547", @@ -1400,7 +1397,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0548", @@ -1424,7 +1421,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0550", @@ -1448,7 +1445,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0551", @@ -1472,7 +1469,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0552", @@ -1496,7 +1493,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0553", @@ -1520,7 +1517,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0554", @@ -1544,7 +1541,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0572", @@ -1568,7 +1565,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0573", @@ -1592,7 +1589,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0574", @@ -1616,7 +1613,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0575", @@ -1640,7 +1637,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0576", @@ -1664,7 +1661,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0577", @@ -1688,7 +1685,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0578", @@ -1712,7 +1709,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0579", @@ -1736,7 +1733,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0580", @@ -1760,7 +1757,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0581", @@ -1784,7 +1781,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0582", @@ -1808,7 +1805,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0583", @@ -1832,7 +1829,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0584", @@ -1856,7 +1853,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0585", @@ -1880,7 +1877,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0586", @@ -1904,7 +1901,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0587", @@ -1928,7 +1925,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0588", @@ -1952,7 +1949,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0589", @@ -1976,7 +1973,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0590", @@ -2000,7 +1997,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0591", @@ -2024,7 +2021,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0592", @@ -2048,7 +2045,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0593", @@ -2072,7 +2069,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0594", @@ -2096,7 +2093,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0595", @@ -2120,7 +2117,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0597", @@ -2144,7 +2141,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0598", @@ -2168,7 +2165,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0599", @@ -2192,7 +2189,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0600", @@ -2216,7 +2213,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23K0601", @@ -2240,7 +2237,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Sm0003", @@ -2264,7 +2261,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Sm0005", @@ -2288,7 +2285,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Sm0006", @@ -2312,7 +2309,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Sm0007", @@ -2336,7 +2333,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Sm0008", @@ -2360,7 +2357,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Sm0004", @@ -2384,7 +2381,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Ent0085", @@ -2408,7 +2405,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 3, "sample_project": 1, "sample_name": "23Entb0091", @@ -2432,7 +2429,7 @@ "patient_core": null, "lab_request": 1, "sample_type": 1, - "sample_user": 1, + "sample_user": 2, "species": 1, "sample_project": 1, "sample_name": "ARBMET12", @@ -5225,9 +5222,9 @@ "sample": 147, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0575_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5244,9 +5241,9 @@ "sample": 158, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0586_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5263,9 +5260,9 @@ "sample": 157, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0585_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5282,9 +5279,9 @@ "sample": 156, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0584_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5301,9 +5298,9 @@ "sample": 155, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0583_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5320,9 +5317,9 @@ "sample": 154, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0582_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5339,9 +5336,9 @@ "sample": 153, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0581_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5358,9 +5355,9 @@ "sample": 152, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0580_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5377,9 +5374,9 @@ "sample": 151, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0579_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5396,9 +5393,9 @@ "sample": 150, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0578_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5415,9 +5412,9 @@ "sample": 149, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0577_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5434,9 +5431,9 @@ "sample": 148, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0576_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5453,9 +5450,9 @@ "sample": 159, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0587_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5472,9 +5469,9 @@ "sample": 146, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0574_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5491,9 +5488,9 @@ "sample": 145, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0573_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5510,9 +5507,9 @@ "sample": 144, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0572_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5529,9 +5526,9 @@ "sample": 143, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0554_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5548,9 +5545,9 @@ "sample": 142, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0553_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5567,9 +5564,9 @@ "sample": 141, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0552_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5586,9 +5583,9 @@ "sample": 140, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0551_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5605,9 +5602,9 @@ "sample": 139, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0550_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5624,9 +5621,9 @@ "sample": 138, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0548_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5643,9 +5640,9 @@ "sample": 137, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0547_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5662,9 +5659,9 @@ "sample": 170, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0599_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5681,9 +5678,9 @@ "sample": 180, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Entb0091_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5700,9 +5697,9 @@ "sample": 179, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Ent0085_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5719,9 +5716,9 @@ "sample": 178, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Sm0004_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5738,9 +5735,9 @@ "sample": 177, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Sm0008_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5757,9 +5754,9 @@ "sample": 176, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Sm0007_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5776,9 +5773,9 @@ "sample": 175, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Sm0006_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5795,9 +5792,9 @@ "sample": 174, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Sm0005_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5814,9 +5811,9 @@ "sample": 173, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Sm0003_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5833,9 +5830,9 @@ "sample": 172, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0601_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5852,9 +5849,9 @@ "sample": 171, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0600_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5871,9 +5868,9 @@ "sample": 136, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0546_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5890,9 +5887,9 @@ "sample": 169, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0598_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5909,9 +5906,9 @@ "sample": 168, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0597_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5928,9 +5925,9 @@ "sample": 167, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0595_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5947,9 +5944,9 @@ "sample": 166, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0594_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5966,9 +5963,9 @@ "sample": 165, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0593_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -5985,9 +5982,9 @@ "sample": 164, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0592_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6004,9 +6001,9 @@ "sample": 163, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0591_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6023,9 +6020,9 @@ "sample": 162, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0590_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6042,9 +6039,9 @@ "sample": 161, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0589_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6061,9 +6058,9 @@ "sample": 160, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0588_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6080,9 +6077,9 @@ "sample": 103, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0189_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6099,9 +6096,9 @@ "sample": 113, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0523_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6118,9 +6115,9 @@ "sample": 112, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Entb0090_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6137,9 +6134,9 @@ "sample": 111, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Entb0089_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6156,9 +6153,9 @@ "sample": 110, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Entb0088_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6175,9 +6172,9 @@ "sample": 109, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Entb0011_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6194,9 +6191,9 @@ "sample": 108, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0196_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6213,9 +6210,9 @@ "sample": 107, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0195_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6232,9 +6229,9 @@ "sample": 106, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0194_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6251,9 +6248,9 @@ "sample": 105, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0193_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6270,9 +6267,9 @@ "sample": 104, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0192_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6289,9 +6286,9 @@ "sample": 114, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0524_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6308,9 +6305,9 @@ "sample": 102, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0188_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6327,9 +6324,9 @@ "sample": 101, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0187_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6346,9 +6343,9 @@ "sample": 100, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0186_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6365,9 +6362,9 @@ "sample": 99, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0185_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6384,9 +6381,9 @@ "sample": 98, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0184_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6403,9 +6400,9 @@ "sample": 97, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0178_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6422,9 +6419,9 @@ "sample": 96, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0177_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6441,9 +6438,9 @@ "sample": 95, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0176_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6460,9 +6457,9 @@ "sample": 94, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0175_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6479,9 +6476,9 @@ "sample": 93, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23Eco0173_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6498,9 +6495,9 @@ "sample": 125, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0535_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6517,9 +6514,9 @@ "sample": 135, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0545_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6536,9 +6533,9 @@ "sample": 134, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0544_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6555,9 +6552,9 @@ "sample": 133, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0543_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6574,9 +6571,9 @@ "sample": 132, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0542_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6593,9 +6590,9 @@ "sample": 131, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0541_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6612,9 +6609,9 @@ "sample": 130, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0540_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6631,9 +6628,9 @@ "sample": 129, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0539_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6650,9 +6647,9 @@ "sample": 128, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0538_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6669,9 +6666,9 @@ "sample": 127, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0537_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6688,9 +6685,9 @@ "sample": 126, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0536_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6707,9 +6704,9 @@ "sample": 92, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_22K0483_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6726,9 +6723,9 @@ "sample": 124, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0534_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6745,9 +6742,9 @@ "sample": 123, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0533_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6764,9 +6761,9 @@ "sample": 122, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0532_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6783,9 +6780,9 @@ "sample": 121, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0531_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6802,9 +6799,9 @@ "sample": 120, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0530_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6821,9 +6818,9 @@ "sample": 119, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0529_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6840,9 +6837,9 @@ "sample": 118, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0528_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6859,9 +6856,9 @@ "sample": 117, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0527_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6878,9 +6875,9 @@ "sample": 116, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0526_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -6897,9 +6894,9 @@ "sample": 115, "molecule_type": 1, "state": 5, - "molecule_user": 1, - "user_lot_kit_id": 1, - "molecule_used_for": 9, + "molecule_user": 2, + "user_lot_kit_id": 2, + "sample_continues_on": 9, "molecule_code_id": "admin_23K0525_E1", "extraction_type": "Manual", "molecule_extraction_date": "2023-07-26T00:00:00", @@ -9158,84 +9155,120 @@ "model": "wetlab.runstates", "pk": 1, "fields": { - "run_state_name": "Pre-Recorded" + "run_state_name": "pre_recorded", + "state_display": "Pre Recorded", + "description": "Run name is defined but pending for input data.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 2, "fields": { - "run_state_name": "Recorded" + "run_state_name": "recorded", + "state_display": "Recorded", + "description": "Run name is defined.", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 3, "fields": { - "run_state_name": "Sample Sent" + "run_state_name": "sample_sent", + "state_display": "Sample Sent", + "description": "Run has copied sample sheet on remote server.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 4, "fields": { - "run_state_name": "Processing Run" + "run_state_name": "processing_run", + "state_display": "Processing Run", + "description": "Run is handled on the sequencer.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 5, "fields": { - "run_state_name": "Processed Run" + "run_state_name": "processed_run", + "state_display": "Processed Run", + "description": "Run is already processed on sequencer.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 6, "fields": { - "run_state_name": "Processing Bcl2fastq" + "run_state_name": "processing_bcl2fastq", + "state_display": "Processing Bcl2fastq", + "description": "Bcl2fastq is running", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 7, "fields": { - "run_state_name": "Processed Bcl2fastq" + "run_state_name": "processed_bcl2fastq", + "state_display": "Processed Bcl2fastq", + "description": "Bcl2fastq already completed", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 8, "fields": { - "run_state_name": "Completed" + "run_state_name": "completed", + "state_display": "Completed", + "description": "Run is completed and all processes are finished.", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 9, "fields": { - "run_state_name": "Cancelled" + "run_state_name": "cancelled", + "state_display": "Cancelled", + "description": "Run was manually cancelled while running on sequencer.", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 10, "fields": { - "run_state_name": "Error" + "run_state_name": "error", + "state_display": "Error", + "description": "There was an issue while processing the run", + "show_in_stats": true } }, { "model": "wetlab.runstates", "pk": 11, "fields": { - "run_state_name": "Processing Metrics" + "run_state_name": "spare_1", + "state_display": "", + "description": "", + "show_in_stats": false } }, { "model": "wetlab.runstates", "pk": 12, "fields": { - "run_state_name": "Processing Demultiplexing" + "run_state_name": "spare_2", + "state_display": "", + "description": "", + "show_in_stats": false } }, { @@ -9317,7 +9350,7 @@ "model": "wetlab.projects", "pk": 11, "fields": { - "user_id": 1, + "user_id": 2, "library_kit_id": null, "base_space_library": null, "project_name": "NextSeq_GEN_436_20230615", @@ -10067,7 +10100,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "1", - "count": "4,776,660", + "count": "4776660", "sequence": "GGGGGGGG+AGATCTCG", "generated_at": "2023-07-25T18:00:02.714" } @@ -10079,7 +10112,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "2", - "count": "315,920", + "count": "315920", "sequence": "GGGGGGGG+CGATCTCG", "generated_at": "2023-07-25T18:00:02.716" } @@ -10091,7 +10124,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "3", - "count": "240,860", + "count": "240860", "sequence": "CGCAACTA+GGGGGGGG", "generated_at": "2023-07-25T18:00:02.719" } @@ -10103,7 +10136,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "4", - "count": "232,220", + "count": "232220", "sequence": "GGGGGGGG+CAAGGTCT", "generated_at": "2023-07-25T18:00:02.721" } @@ -10115,7 +10148,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "5", - "count": "229,500", + "count": "229500", "sequence": "GGGGGGGG+GTACTCTC", "generated_at": "2023-07-25T18:00:02.723" } @@ -10127,7 +10160,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "6", - "count": "189,220", + "count": "189220", "sequence": "GGGGGGGG+CTTCGTTC", "generated_at": "2023-07-25T18:00:02.726" } @@ -10139,7 +10172,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "7", - "count": "180,020", + "count": "180020", "sequence": "GGGGGGGG+GCAATTCG", "generated_at": "2023-07-25T18:00:02.728" } @@ -10151,7 +10184,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "8", - "count": "162,120", + "count": "162120", "sequence": "GGGGGGGG+GAATCCGA", "generated_at": "2023-07-25T18:00:02.731" } @@ -10163,7 +10196,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "9", - "count": "161,920", + "count": "161920", "sequence": "GGGGGGGG+CTTCTGAG", "generated_at": "2023-07-25T18:00:02.733" } @@ -10175,7 +10208,7 @@ "runprocess_id": 21, "lane_number": "1", "top_number": "10", - "count": "122,580", + "count": "122580", "sequence": "GAATCACC+GGGGGGGG", "generated_at": "2023-07-25T18:00:02.737" } @@ -10187,7 +10220,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "1", - "count": "4,860,760", + "count": "4860760", "sequence": "GGGGGGGG+AGATCTCG", "generated_at": "2023-07-25T18:00:02.741" } @@ -10199,7 +10232,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "2", - "count": "257,000", + "count": "257000", "sequence": "GGGGGGGG+CGATCTCG", "generated_at": "2023-07-25T18:00:02.743" } @@ -10211,7 +10244,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "3", - "count": "241,140", + "count": "241140", "sequence": "CGCAACTA+GGGGGGGG", "generated_at": "2023-07-25T18:00:02.746" } @@ -10223,7 +10256,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "4", - "count": "232,080", + "count": "232080", "sequence": "GGGGGGGG+GTACTCTC", "generated_at": "2023-07-25T18:00:02.748" } @@ -10235,7 +10268,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "5", - "count": "230,140", + "count": "230140", "sequence": "GGGGGGGG+CAAGGTCT", "generated_at": "2023-07-25T18:00:02.750" } @@ -10247,7 +10280,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "6", - "count": "189,940", + "count": "189940", "sequence": "GGGGGGGG+CTTCGTTC", "generated_at": "2023-07-25T18:00:02.752" } @@ -10259,7 +10292,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "7", - "count": "179,300", + "count": "179300", "sequence": "GGGGGGGG+GCAATTCG", "generated_at": "2023-07-25T18:00:02.755" } @@ -10271,7 +10304,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "8", - "count": "163,740", + "count": "163740", "sequence": "GGGGGGGG+GAATCCGA", "generated_at": "2023-07-25T18:00:02.757" } @@ -10283,7 +10316,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "9", - "count": "160,260", + "count": "160260", "sequence": "GGGGGGGG+CTTCTGAG", "generated_at": "2023-07-25T18:00:02.759" } @@ -10295,7 +10328,7 @@ "runprocess_id": 21, "lane_number": "2", "top_number": "10", - "count": "120,120", + "count": "120120", "sequence": "GAATCACC+GGGGGGGG", "generated_at": "2023-07-25T18:00:02.761" } @@ -10307,7 +10340,7 @@ "runprocess_id": 22, "lane_number": "1", "top_number": "1", - "count": "2,020", + "count": "2020", "sequence": "GCTCATGA+TCTCTATT", "generated_at": "2023-07-25T18:00:03.700" } @@ -10319,7 +10352,7 @@ "runprocess_id": 22, "lane_number": "1", "top_number": "2", - "count": "1,380", + "count": "1380", "sequence": "GCTCATGA+CTCTTATT", "generated_at": "2023-07-25T18:00:03.702" } @@ -10331,7 +10364,7 @@ "runprocess_id": 22, "lane_number": "1", "top_number": "3", - "count": "1,080", + "count": "1080", "sequence": "GCTCAGAA+CTCTCTAT", "generated_at": "2023-07-25T18:00:03.704" } @@ -10343,7 +10376,7 @@ "runprocess_id": 22, "lane_number": "1", "top_number": "4", - "count": "1,000", + "count": "1000", "sequence": "AAGAGGCA+GTAATGAT", "generated_at": "2023-07-25T18:00:03.706" } @@ -11096,7 +11129,7 @@ "model": "wetlab.libusersamplesheet", "pk": 1, "fields": { - "register_user": 1, + "register_user": 2, "collection_index_kit_id": null, "sequencing_configuration": 10, "sample_sheet": "SampleSheet_20230725-170642.csv", @@ -11115,9 +11148,8 @@ "model": "wetlab.librarypool", "pk": 1, "fields": { - "register_user": 1, + "register_user": 2, "pool_state": 3, - "run_process_id": 20, "platform": 10, "pool_name": "20230725_pool", "sample_number": 77, @@ -11131,7 +11163,7 @@ "model": "wetlab.libprepare", "pk": 1, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 1, "sample_id": 147, "protocol_id": 2, @@ -11152,7 +11184,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0056-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0575", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -11162,7 +11194,7 @@ "model": "wetlab.libprepare", "pk": 2, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 2, "sample_id": 158, "protocol_id": 2, @@ -11183,7 +11215,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0067-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0586", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11195,7 +11227,7 @@ "model": "wetlab.libprepare", "pk": 3, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 3, "sample_id": 157, "protocol_id": 2, @@ -11216,7 +11248,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0066-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0585", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11228,7 +11260,7 @@ "model": "wetlab.libprepare", "pk": 4, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 4, "sample_id": 156, "protocol_id": 2, @@ -11249,7 +11281,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0065-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0584", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11261,7 +11293,7 @@ "model": "wetlab.libprepare", "pk": 5, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 5, "sample_id": 155, "protocol_id": 2, @@ -11282,7 +11314,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0064-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0583", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11294,7 +11326,7 @@ "model": "wetlab.libprepare", "pk": 6, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 6, "sample_id": 154, "protocol_id": 2, @@ -11315,7 +11347,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0063-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0582", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11327,7 +11359,7 @@ "model": "wetlab.libprepare", "pk": 7, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 7, "sample_id": 153, "protocol_id": 2, @@ -11348,7 +11380,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0062-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0581", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11360,7 +11392,7 @@ "model": "wetlab.libprepare", "pk": 8, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 8, "sample_id": 152, "protocol_id": 2, @@ -11381,7 +11413,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0061-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0580", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11393,7 +11425,7 @@ "model": "wetlab.libprepare", "pk": 9, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 9, "sample_id": 151, "protocol_id": 2, @@ -11414,7 +11446,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0060-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0579", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11426,7 +11458,7 @@ "model": "wetlab.libprepare", "pk": 10, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 10, "sample_id": 150, "protocol_id": 2, @@ -11447,7 +11479,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0059-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0578", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11459,7 +11491,7 @@ "model": "wetlab.libprepare", "pk": 11, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 11, "sample_id": 149, "protocol_id": 2, @@ -11480,7 +11512,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0058-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0577", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11492,7 +11524,7 @@ "model": "wetlab.libprepare", "pk": 12, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 12, "sample_id": 148, "protocol_id": 2, @@ -11513,7 +11545,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0057-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0576", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11525,7 +11557,7 @@ "model": "wetlab.libprepare", "pk": 13, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 13, "sample_id": 159, "protocol_id": 2, @@ -11546,7 +11578,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0068-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0587", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11558,7 +11590,7 @@ "model": "wetlab.libprepare", "pk": 14, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 14, "sample_id": 146, "protocol_id": 2, @@ -11579,7 +11611,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0055-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0574", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11591,7 +11623,7 @@ "model": "wetlab.libprepare", "pk": 15, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 15, "sample_id": 145, "protocol_id": 2, @@ -11612,7 +11644,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0054-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0573", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11624,7 +11656,7 @@ "model": "wetlab.libprepare", "pk": 16, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 16, "sample_id": 144, "protocol_id": 2, @@ -11645,7 +11677,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0053-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0572", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11657,7 +11689,7 @@ "model": "wetlab.libprepare", "pk": 17, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 17, "sample_id": 143, "protocol_id": 2, @@ -11678,7 +11710,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0052-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0554", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11690,7 +11722,7 @@ "model": "wetlab.libprepare", "pk": 18, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 18, "sample_id": 142, "protocol_id": 2, @@ -11711,7 +11743,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0051-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0553", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11723,7 +11755,7 @@ "model": "wetlab.libprepare", "pk": 19, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 19, "sample_id": 141, "protocol_id": 2, @@ -11744,7 +11776,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0050-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0552", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11756,7 +11788,7 @@ "model": "wetlab.libprepare", "pk": 20, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 20, "sample_id": 140, "protocol_id": 2, @@ -11777,7 +11809,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0049-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0551", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11789,7 +11821,7 @@ "model": "wetlab.libprepare", "pk": 21, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 21, "sample_id": 139, "protocol_id": 2, @@ -11810,7 +11842,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0048-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0550", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11822,7 +11854,7 @@ "model": "wetlab.libprepare", "pk": 22, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 22, "sample_id": 138, "protocol_id": 2, @@ -11843,7 +11875,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0047-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0548", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11855,7 +11887,7 @@ "model": "wetlab.libprepare", "pk": 23, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 23, "sample_id": 137, "protocol_id": 2, @@ -11876,7 +11908,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0046-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0547", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -11888,7 +11920,7 @@ "model": "wetlab.libprepare", "pk": 24, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 24, "sample_id": 170, "protocol_id": 2, @@ -11909,7 +11941,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0079-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0599", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -11919,7 +11951,7 @@ "model": "wetlab.libprepare", "pk": 25, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 25, "sample_id": 180, "protocol_id": 2, @@ -11940,7 +11972,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0089-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Entb0091", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -11950,7 +11982,7 @@ "model": "wetlab.libprepare", "pk": 26, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 26, "sample_id": 179, "protocol_id": 2, @@ -11971,7 +12003,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0088-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Ent0085", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -11981,7 +12013,7 @@ "model": "wetlab.libprepare", "pk": 27, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 27, "sample_id": 178, "protocol_id": 2, @@ -12002,7 +12034,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0087-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Sm0004", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12012,7 +12044,7 @@ "model": "wetlab.libprepare", "pk": 28, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 28, "sample_id": 177, "protocol_id": 2, @@ -12033,7 +12065,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0086-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Sm0008", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12043,7 +12075,7 @@ "model": "wetlab.libprepare", "pk": 29, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 29, "sample_id": 176, "protocol_id": 2, @@ -12064,7 +12096,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0085-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Sm0007", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12074,7 +12106,7 @@ "model": "wetlab.libprepare", "pk": 30, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 30, "sample_id": 175, "protocol_id": 2, @@ -12095,7 +12127,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0084-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Sm0006", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12105,7 +12137,7 @@ "model": "wetlab.libprepare", "pk": 31, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 31, "sample_id": 174, "protocol_id": 2, @@ -12126,7 +12158,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0083-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Sm0005", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12136,7 +12168,7 @@ "model": "wetlab.libprepare", "pk": 32, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 32, "sample_id": 173, "protocol_id": 2, @@ -12157,7 +12189,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0082-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Sm0003", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12167,7 +12199,7 @@ "model": "wetlab.libprepare", "pk": 33, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 33, "sample_id": 172, "protocol_id": 2, @@ -12188,7 +12220,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0081-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0601", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12198,7 +12230,7 @@ "model": "wetlab.libprepare", "pk": 34, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 34, "sample_id": 171, "protocol_id": 2, @@ -12219,7 +12251,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0080-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0600", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12231,7 +12263,7 @@ "model": "wetlab.libprepare", "pk": 35, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 35, "sample_id": 136, "protocol_id": 2, @@ -12252,7 +12284,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0045-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0546", "prefix_protocol": "illumina nextera lib prep", "pools": [] @@ -12262,7 +12294,7 @@ "model": "wetlab.libprepare", "pk": 36, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 36, "sample_id": 169, "protocol_id": 2, @@ -12283,7 +12315,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0078-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0598", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12295,7 +12327,7 @@ "model": "wetlab.libprepare", "pk": 37, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 37, "sample_id": 168, "protocol_id": 2, @@ -12316,7 +12348,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0077-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0597", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12328,7 +12360,7 @@ "model": "wetlab.libprepare", "pk": 38, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 38, "sample_id": 167, "protocol_id": 2, @@ -12349,7 +12381,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0076-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0595", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12361,7 +12393,7 @@ "model": "wetlab.libprepare", "pk": 39, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 39, "sample_id": 166, "protocol_id": 2, @@ -12382,7 +12414,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0075-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0594", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12394,7 +12426,7 @@ "model": "wetlab.libprepare", "pk": 40, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 40, "sample_id": 165, "protocol_id": 2, @@ -12415,7 +12447,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0074-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0593", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12427,7 +12459,7 @@ "model": "wetlab.libprepare", "pk": 41, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 41, "sample_id": 164, "protocol_id": 2, @@ -12448,7 +12480,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0073-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0592", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12460,7 +12492,7 @@ "model": "wetlab.libprepare", "pk": 42, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 42, "sample_id": 163, "protocol_id": 2, @@ -12481,7 +12513,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0072-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0591", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12493,7 +12525,7 @@ "model": "wetlab.libprepare", "pk": 43, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 43, "sample_id": 162, "protocol_id": 2, @@ -12514,7 +12546,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0071-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0590", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12526,7 +12558,7 @@ "model": "wetlab.libprepare", "pk": 44, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 44, "sample_id": 161, "protocol_id": 2, @@ -12547,7 +12579,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0070-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0589", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12559,7 +12591,7 @@ "model": "wetlab.libprepare", "pk": 45, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 45, "sample_id": 160, "protocol_id": 2, @@ -12580,7 +12612,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0069-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0588", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12592,7 +12624,7 @@ "model": "wetlab.libprepare", "pk": 46, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 46, "sample_id": 103, "protocol_id": 2, @@ -12613,7 +12645,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0012-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0189", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12625,7 +12657,7 @@ "model": "wetlab.libprepare", "pk": 47, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 47, "sample_id": 113, "protocol_id": 2, @@ -12646,7 +12678,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0022-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0523", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12658,7 +12690,7 @@ "model": "wetlab.libprepare", "pk": 48, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 48, "sample_id": 112, "protocol_id": 2, @@ -12679,7 +12711,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0021-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Entb0090", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12691,7 +12723,7 @@ "model": "wetlab.libprepare", "pk": 49, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 49, "sample_id": 111, "protocol_id": 2, @@ -12712,7 +12744,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0020-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Entb0089", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12724,7 +12756,7 @@ "model": "wetlab.libprepare", "pk": 50, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 50, "sample_id": 110, "protocol_id": 2, @@ -12745,7 +12777,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0019-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Entb0088", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12757,7 +12789,7 @@ "model": "wetlab.libprepare", "pk": 51, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 51, "sample_id": 109, "protocol_id": 2, @@ -12778,7 +12810,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0018-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Entb0011", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12790,7 +12822,7 @@ "model": "wetlab.libprepare", "pk": 52, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 52, "sample_id": 108, "protocol_id": 2, @@ -12811,7 +12843,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0017-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0196", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12823,7 +12855,7 @@ "model": "wetlab.libprepare", "pk": 53, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 53, "sample_id": 107, "protocol_id": 2, @@ -12844,7 +12876,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0016-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0195", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12856,7 +12888,7 @@ "model": "wetlab.libprepare", "pk": 54, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 54, "sample_id": 106, "protocol_id": 2, @@ -12877,7 +12909,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0015-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0194", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12889,7 +12921,7 @@ "model": "wetlab.libprepare", "pk": 55, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 55, "sample_id": 105, "protocol_id": 2, @@ -12910,7 +12942,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0014-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0193", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12922,7 +12954,7 @@ "model": "wetlab.libprepare", "pk": 56, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 56, "sample_id": 104, "protocol_id": 2, @@ -12943,7 +12975,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0013-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0192", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12955,7 +12987,7 @@ "model": "wetlab.libprepare", "pk": 57, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 57, "sample_id": 114, "protocol_id": 2, @@ -12976,7 +13008,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0023-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0524", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -12988,7 +13020,7 @@ "model": "wetlab.libprepare", "pk": 58, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 58, "sample_id": 102, "protocol_id": 2, @@ -13009,7 +13041,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0011-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0188", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13021,7 +13053,7 @@ "model": "wetlab.libprepare", "pk": 59, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 59, "sample_id": 101, "protocol_id": 2, @@ -13042,7 +13074,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0010-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0187", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13054,7 +13086,7 @@ "model": "wetlab.libprepare", "pk": 60, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 60, "sample_id": 100, "protocol_id": 2, @@ -13075,7 +13107,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0009-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0186", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13087,7 +13119,7 @@ "model": "wetlab.libprepare", "pk": 61, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 61, "sample_id": 99, "protocol_id": 2, @@ -13108,7 +13140,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0008-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0185", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13120,7 +13152,7 @@ "model": "wetlab.libprepare", "pk": 62, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 62, "sample_id": 98, "protocol_id": 2, @@ -13141,7 +13173,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0007-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0184", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13153,7 +13185,7 @@ "model": "wetlab.libprepare", "pk": 63, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 63, "sample_id": 97, "protocol_id": 2, @@ -13174,7 +13206,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0006-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0178", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13186,7 +13218,7 @@ "model": "wetlab.libprepare", "pk": 64, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 64, "sample_id": 96, "protocol_id": 2, @@ -13207,7 +13239,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0005-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0177", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13219,7 +13251,7 @@ "model": "wetlab.libprepare", "pk": 65, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 65, "sample_id": 95, "protocol_id": 2, @@ -13240,7 +13272,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0004-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0176", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13252,7 +13284,7 @@ "model": "wetlab.libprepare", "pk": 66, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 66, "sample_id": 94, "protocol_id": 2, @@ -13273,7 +13305,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0003-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0175", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13285,7 +13317,7 @@ "model": "wetlab.libprepare", "pk": 67, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 67, "sample_id": 93, "protocol_id": 2, @@ -13306,7 +13338,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0002-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23Eco0173", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13318,7 +13350,7 @@ "model": "wetlab.libprepare", "pk": 68, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 68, "sample_id": 125, "protocol_id": 2, @@ -13339,7 +13371,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0034-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0535", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13351,7 +13383,7 @@ "model": "wetlab.libprepare", "pk": 69, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 69, "sample_id": 135, "protocol_id": 2, @@ -13372,7 +13404,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0044-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0545", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13384,7 +13416,7 @@ "model": "wetlab.libprepare", "pk": 70, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 70, "sample_id": 134, "protocol_id": 2, @@ -13405,7 +13437,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0043-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0544", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13417,7 +13449,7 @@ "model": "wetlab.libprepare", "pk": 71, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 71, "sample_id": 133, "protocol_id": 2, @@ -13438,7 +13470,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0042-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0543", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13450,7 +13482,7 @@ "model": "wetlab.libprepare", "pk": 72, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 72, "sample_id": 132, "protocol_id": 2, @@ -13471,7 +13503,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0041-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0542", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13483,7 +13515,7 @@ "model": "wetlab.libprepare", "pk": 73, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 73, "sample_id": 131, "protocol_id": 2, @@ -13504,7 +13536,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0040-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0541", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13516,7 +13548,7 @@ "model": "wetlab.libprepare", "pk": 74, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 74, "sample_id": 130, "protocol_id": 2, @@ -13537,7 +13569,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0039-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0540", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13549,7 +13581,7 @@ "model": "wetlab.libprepare", "pk": 75, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 75, "sample_id": 129, "protocol_id": 2, @@ -13570,7 +13602,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0038-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0539", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13582,7 +13614,7 @@ "model": "wetlab.libprepare", "pk": 76, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 76, "sample_id": 128, "protocol_id": 2, @@ -13603,7 +13635,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0037-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0538", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13615,7 +13647,7 @@ "model": "wetlab.libprepare", "pk": 77, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 77, "sample_id": 127, "protocol_id": 2, @@ -13636,7 +13668,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0036-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0537", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13648,7 +13680,7 @@ "model": "wetlab.libprepare", "pk": 78, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 78, "sample_id": 126, "protocol_id": 2, @@ -13669,7 +13701,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0035-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0536", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13681,7 +13713,7 @@ "model": "wetlab.libprepare", "pk": 79, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 79, "sample_id": 92, "protocol_id": 2, @@ -13702,7 +13734,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0001-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "22K0483", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13714,7 +13746,7 @@ "model": "wetlab.libprepare", "pk": 80, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 80, "sample_id": 124, "protocol_id": 2, @@ -13735,7 +13767,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0033-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0534", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13747,7 +13779,7 @@ "model": "wetlab.libprepare", "pk": 81, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 81, "sample_id": 123, "protocol_id": 2, @@ -13768,7 +13800,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0032-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0533", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13780,7 +13812,7 @@ "model": "wetlab.libprepare", "pk": 82, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 82, "sample_id": 122, "protocol_id": 2, @@ -13801,7 +13833,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0031-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0532", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13813,7 +13845,7 @@ "model": "wetlab.libprepare", "pk": 83, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 83, "sample_id": 121, "protocol_id": 2, @@ -13834,7 +13866,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0030-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0531", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13846,7 +13878,7 @@ "model": "wetlab.libprepare", "pk": 84, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 84, "sample_id": 120, "protocol_id": 2, @@ -13867,7 +13899,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0029-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0530", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13879,7 +13911,7 @@ "model": "wetlab.libprepare", "pk": 85, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 85, "sample_id": 119, "protocol_id": 2, @@ -13900,7 +13932,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0028-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0529", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13912,7 +13944,7 @@ "model": "wetlab.libprepare", "pk": 86, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 86, "sample_id": 118, "protocol_id": 2, @@ -13933,7 +13965,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0027-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0528", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13945,7 +13977,7 @@ "model": "wetlab.libprepare", "pk": 87, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 87, "sample_id": 117, "protocol_id": 2, @@ -13966,7 +13998,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0026-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0527", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -13978,7 +14010,7 @@ "model": "wetlab.libprepare", "pk": 88, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 88, "sample_id": 116, "protocol_id": 2, @@ -13999,7 +14031,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0025-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0526", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -14011,7 +14043,7 @@ "model": "wetlab.libprepare", "pk": 89, "fields": { - "register_user": 1, + "register_user": 2, "molecule_id": 89, "sample_id": 115, "protocol_id": 2, @@ -14032,7 +14064,7 @@ "manifest": null, "reused_number": 0, "unique_id": "AAA-0024-1", - "user_in_samplesheet": "test_user1", + "user_in_samplesheet": "testuser1", "samplename_in_samplesheet": "23K0525", "prefix_protocol": "illumina nextera lib prep", "pools": [ @@ -15824,7 +15856,7 @@ "model": "wetlab.sambaconnectiondata", "pk": 1, "fields": { - "samba_folder_name": "", + "samba_folder_name": "Runs", "domain": "", "host_name": "samba", "ip_server": "", @@ -15848,7 +15880,6 @@ "service_request_number": "SRVSGAFI001", "service_request_int": "001", "service_run_specs": "", - "service_status": "Recorded", "service_notes": "I request the analysis of the recorded samples using the wgMLST pipeline for E. coli", "service_created_date": "2023-07-25", "service_approved_date": null, @@ -15873,7 +15904,6 @@ "service_request_number": "SRVSGAFI002", "service_request_int": "002", "service_run_specs": "", - "service_status": "Recorded", "service_notes": "I request plasmidID pipeline for these samples.", "service_created_date": "2023-07-25", "service_approved_date": null, @@ -15898,7 +15928,6 @@ "service_request_number": "SRVSGAFI003", "service_request_int": "003", "service_run_specs": "", - "service_status": "Recorded", "service_notes": "I'd like to generate the consensus genomes for these samples.", "service_created_date": "2023-07-25", "service_approved_date": "2023-07-25", @@ -16269,7 +16298,7 @@ "pk": 1, "fields": { "resolution_service_id": 3, - "resolution_assigned_user": 1, + "resolution_assigned_user": 2, "resolution_state": 6, "resolution_number": "SRVSGAFI003.1", "resolution_estimated_date": "2023-07-29", @@ -16278,7 +16307,7 @@ "resolution_in_progress_date": null, "resolution_delivery_date": null, "resolution_notes": "", - "resolution_full_number": "SRVSGAFI003_20230725_WGMLST01_test_user1_S", + "resolution_full_number": "SRVSGAFI003_20230725_WGMLST01_testuser1_S", "resolution_pdf_file": "", "resolution_pipelines": [], "available_services": [ @@ -16286,6 +16315,15 @@ ] } }, + { + "model": "wetlab.configsetting", + "pk": 9, + "fields": { + "configuration_name": "SENT_EMAIL_ON_CRONTAB_ERROR", + "configuration_value": "FALSE", + "generated_at": "2021-06-09T22:10:57.394" + } + }, { "model": "auth.user", "pk": 2, @@ -16293,7 +16331,7 @@ "password": "pbkdf2_sha256$260000$WoFbBL6zQ3F4nCr3oAFzFd$kT3I+1KotzgT1zT8on4zXZte5vS07KnQr6RmDw2N+UU=", "last_login": null, "is_superuser": false, - "username": "test_user1", + "username": "testuser1", "first_name": "", "last_name": "", "email": "", @@ -16304,4 +16342,4 @@ "user_permissions": [] } } -] \ No newline at end of file +] diff --git a/wetlab/.gitignore b/wetlab/.gitignore deleted file mode 100644 index b3b95c969..000000000 --- a/wetlab/.gitignore +++ /dev/null @@ -1,68 +0,0 @@ -## Custom -*.xml -*.bin -migrations/ -tmp/ -logs/ -tests/ - -## Cusotomized script - -script/private*.py -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ diff --git a/wetlab/admin.py b/wetlab/admin.py index ae484d5bd..647e5c54e 100644 --- a/wetlab/admin.py +++ b/wetlab/admin.py @@ -33,12 +33,11 @@ class LibParameterValueAdmin(admin.ModelAdmin): class LibraryPoolAdmin(admin.ModelAdmin): list_display = ( - "register_user", - "pool_state", "pool_name", + "pool_state", "platform", "pool_code_id", - "run_process_id", + "register_user", ) @@ -61,6 +60,12 @@ class AdditionaKitsLibraryPreparationAdmin(admin.ModelAdmin): list_display = ["kit_name", "protocol_id", "commercial_kit_id"] +class LibraryKitAdmin(admin.ModelAdmin): + list_display = [ + "library_name", + ] + + class AdditionalUserLotKitAdmin(admin.ModelAdmin): list_display = ["lib_prep_id", "additional_lot_kits", "user_lot_kit_id"] @@ -70,7 +75,7 @@ class RunErrorsAdmin(admin.ModelAdmin): class RunStatesAdmin(admin.ModelAdmin): - list_display = ("run_state_name",) + list_display = ["run_state_name", "state_display", "description"] class RunningParametersAdmin(admin.ModelAdmin): @@ -291,6 +296,7 @@ class RunConfigurationTestAdmin(admin.ModelAdmin): admin.site.register(wetlab.models.RunErrors, RunErrorsAdmin) admin.site.register(wetlab.models.RunStates, RunStatesAdmin) admin.site.register(wetlab.models.LibPrepareStates, StatesForLibraryPreparationAdmin) +admin.site.register(wetlab.models.LibraryKit, LibraryKitAdmin) admin.site.register(wetlab.models.PoolStates, StatesForPoolAdmin) admin.site.register(wetlab.models.SamplesInProject, SamplesInProjectAdmin) admin.site.register(wetlab.models.StatsRunSummary, StatsRunSummaryAdmin) diff --git a/wetlab/api/serializers.py b/wetlab/api/serializers.py index 074053a9b..2c7fc508e 100644 --- a/wetlab/api/serializers.py +++ b/wetlab/api/serializers.py @@ -241,12 +241,14 @@ class Meta: "lab_city", ] - def update(self, data): - self.labContactName = data["lab_contact_name"] - self.labPhone = data["lab_contact_telephone"] - self.labEmail = data["lab_contact_email"] - self.save() - return self + def update(self, instance, validated_data): + instance.lab_contact_name = validated_data.get( + "lab_contact_name", instance.lab_contact_name + ) + instance.lab_phone = validated_data.get("lab_phone", instance.lab_phone) + instance.lab_email = validated_data.get("lab_email", instance.lab_email) + instance.save() + return instance class SampleFields(object): diff --git a/wetlab/api/urls.py b/wetlab/api/urls.py index 5948504c1..b4c892ca7 100644 --- a/wetlab/api/urls.py +++ b/wetlab/api/urls.py @@ -5,7 +5,6 @@ urlpatterns = [ - path("run-info", views.fetch_run_information, name="fetch_run_information"), path( "lab-data", views.get_lab_information_contact, @@ -30,4 +29,9 @@ name="summarize_data_information", ), path("update-lab", views.update_lab, name="update_lab"), + path( + "lab-request-mapping", + views.get_lab_request_mapping, + name="get_lab_request_mapping", + ), ] diff --git a/wetlab/api/utils/sample.py b/wetlab/api/utils/sample.py index c32d5f6a9..30613d333 100644 --- a/wetlab/api/utils/sample.py +++ b/wetlab/api/utils/sample.py @@ -1,12 +1,20 @@ from datetime import datetime +from collections import defaultdict +from django.db.models import Count import core.models import core.utils.samples +import core.core_config import wetlab.api.serializers import wetlab.config -def create_state(state, apps_name): +def create_state_if_not_exists(state, apps_name): + # Check if state is defined in database + if core.models.StateInCountry.objects.filter(state_name__iexact=state).exists(): + return core.models.StateInCountry.objects.filter( + state_name__iexact=state + ).last() """Create state instance""" data = {"state": state, "apps_name": apps_name} return core.models.StateInCountry.objects.create_new_state(data) @@ -14,7 +22,9 @@ def create_state(state, apps_name): def create_city(data, apps_name): """Create a City instance""" - data["state"] = create_state(data["geo_loc_state"], apps_name).get_state_id() + data["state"] = create_state_if_not_exists( + data["geo_loc_state"], apps_name + ).get_state_id() data["city_name"] = data["geo_loc_city"] data["latitude"] = data["geo_loc_latitude"] data["longitude"] = data["geo_loc_longitude"] @@ -25,7 +35,7 @@ def create_city(data, apps_name): def create_new_laboratory(lab_data): """Create new laboratory instance with the data collected in the request""" if core.models.City.objects.filter( - city_name__exact=lab_data["geo_loc_city"] + city_name__iexact=lab_data["geo_loc_city"] ).exists(): city_id = ( core.models.City.objects.filter(city_name__exact=lab_data["geo_loc_city"]) @@ -34,17 +44,17 @@ def create_new_laboratory(lab_data): ) else: if core.models.StateInCountry.objects.filter( - state_name__exact=lab_data["geo_loc_state"] + state_name__iexact=lab_data["geo_loc_state"] ).exists(): lab_data["state"] = ( core.models.StateInCountry.objects.filter( - state_name__exact=lab_data["geo_loc_state"] + state_name__iexact=lab_data["geo_loc_state"] ) .last() .get_state_id() ) else: - lab_data["state"] = create_state( + lab_data["state"] = create_state_if_not_exists( lab_data["geo_loc_state"], lab_data["apps_name"] ).get_state_id() city_id = create_city(lab_data, lab_data["apps_name"]).get_city_id() @@ -60,7 +70,9 @@ def create_new_sample_type(sample_type, apps_name): data["optional_fields"] = "0,8" data["apps_name"] = apps_name data["sample_type"] = sample_type - sample_type_serializers = core.models.CreateSampleTypeSerializer(data=data) + sample_type_serializers = wetlab.api.serializers.CreateSampleTypeSerializer( + data=data + ) if sample_type_serializers.is_valid(): sample_type_obj = sample_type_serializers.save() return sample_type_obj @@ -75,7 +87,7 @@ def get_sample_fields(apps_name): "Type of Sample": {"field_name": "sample_type"}, "Species": {"field_name": "species"}, "Project/Service": {"field_name": "sample_project"}, - "Date sample reception": {"field_name": "smple_entry_date"}, + "Date sample reception": {"field_name": "sample_entry_date"}, "Collection Sample Date": {"field_name": "collection_sample_date"}, "Sample Storage": {"field_name": "sample_location"}, "Only recorded": {"field_name": "only_recorded"}, @@ -136,7 +148,7 @@ def get_sample_project_obj(project_name): def include_instances_in_sample(data, lab_data, apps_name): """Collect the instances before creating the sample instance - If laboratory will be created if it is not defined + Define laboratory if not defined yet """ if core.models.LabRequest.objects.filter( lab_name__iexact=data["lab_request"] @@ -344,31 +356,15 @@ def split_sample_data(data): lab_data["lab_unit"] = "" lab_data["lab_contact_name"] = "" lab_data["lab_phone"] = "" - lab_data_fields = [ - ("lab_email", "collecting_institution_email"), - ("address", "collecting_institution_address"), - ("geo_loc_city", "geo_loc_city"), - ("geo_loc_state", "geo_loc_state"), - ("geo_loc_latitude", "geo_loc_latitude"), - ("geo_loc_longitude", "geo_loc_longitude"), - ] + lab_req_mapping = core.core_config.LAB_REQUEST_ONTOLOGY_MAP + lab_data_fields = lab_req_mapping.values() for l_data, i_data in lab_data_fields: try: lab_data[l_data] = data[i_data] except KeyError: - lab_data[l_data] = "" - - """ - lab_data["lab_email"] = data["collecting_institution_email"] - lab_data["address"] = data["collecting_institution_address"] - lab_data["geo_loc_city"] = data["geo_loc_city"] - lab_data["geo_loc_state"] = data["geo_loc_state"] - lab_data["geo_loc_latitude"] = data["geo_loc_latitude"] - lab_data["geo_loc_longitude"] = data["geo_loc_longitude"] - """ + lab_data[l_data] = data.get(l_data, "") split_data["lab_data"] = lab_data - return split_data @@ -486,22 +482,22 @@ def summarize_samples(data): .distinct() ) for f_value in f_values: - summarize["parameters"][p_name][ - f_value - ] = core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id=s_project_field_obj, - sample_project_field_value__exact=f_value, - sample_id__sample_name__in=sample_list, - ).count() + summarize["parameters"][p_name][f_value] = ( + core.models.SampleProjectsFieldsValue.objects.filter( + sample_project_field_id=s_project_field_obj, + sample_project_field_value__exact=f_value, + sample_id__sample_name__in=sample_list, + ).count() + ) else: summarize["parameters"][p_name]["value"] = 0 else: - summarize["parameters"][p_name][ - "value" - ] = core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id=s_project_field_obj, - sample_id__sample_name__in=sample_list, - ).count() + summarize["parameters"][p_name]["value"] = ( + core.models.SampleProjectsFieldsValue.objects.filter( + sample_project_field_id=s_project_field_obj, + sample_id__sample_name__in=sample_list, + ).count() + ) summarize["parameters"][p_name][ "classification" ] = s_project_field_obj.get_classification_name() @@ -526,62 +522,45 @@ def collect_statistics_information(data): if len(query_params) > 2: return {"ERROR": ""} - stats_data = {} - par1_values = ( - core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id__sample_projects_id=s_project_obj, - sample_project_field_id__sample_project_field_name__iexact=query_params[ - 0 - ], - ) - .values_list("sample_project_field_value", flat=True) - .distinct() + base_values = core.models.SampleProjectsFieldsValue.objects.filter( + sample_project_field_id__sample_projects_id=s_project_obj ) - if len(query_params) == 2: - for par1_val in par1_values: - stats_data[par1_val] = {} - - samples = core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id__sample_projects_id=s_project_obj, - sample_project_field_id__sample_project_field_name__iexact=query_params[ - 0 - ], - sample_project_field_value__exact=par1_val, - ).values_list("sample_id", flat=True) - par2_values = ( - core.models.SampleProjectsFieldsValue.objects.filter( - sample_id__in=samples, - sample_project_field_id__sample_project_field_name__iexact=query_params[ - 1 - ], + par1_rows = base_values.filter( + sample_project_field_id__sample_project_field_name__iexact=query_params[ + 0 + ] + ).values_list("sample_id", "sample_project_field_value") + par2_rows = base_values.filter( + sample_project_field_id__sample_project_field_name__iexact=query_params[ + 1 + ] + ).values_list("sample_id", "sample_project_field_value") + + par1_by_sample = defaultdict(list) + for sample_id, par1_val in par1_rows: + par1_by_sample[sample_id].append(par1_val) + + stats_data = defaultdict(dict) + for sample_id, par2_val in par2_rows: + for par1_val in par1_by_sample.get(sample_id, []): + stats_data[par1_val][par2_val] = ( + stats_data[par1_val].get(par2_val, 0) + 1 ) - .values_list("sample_project_field_value", flat=True) - .distinct() - ) - for par2_val in par2_values: - value = core.models.SampleProjectsFieldsValue.objects.filter( - sample_id__in=samples, - sample_project_field_id__sample_project_field_name=query_params[ - 1 - ], - sample_project_field_value__exact=par2_val, - ).count() - if value > 0: - stats_data[par1_val][par2_val] = value - else: - for par1_val in par1_values: - stats_data[ - par1_val - ] = core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id__sample_projects_id=s_project_obj, - sample_project_field_id__sample_project_field_name__iexact=query_params[ - 0 - ], - sample_project_field_value=par1_val, - ).count() + return dict(stats_data) - return stats_data + counts = ( + base_values.filter( + sample_project_field_id__sample_project_field_name__iexact=query_params[ + 0 + ] + ) + .values("sample_project_field_value") + .annotate(count=Count("id")) + ) + return { + item["sample_project_field_value"]: item["count"] for item in counts + } else: # Collect info stats for all fields # Collect the fields utilization for sample projects stats_data = { @@ -597,21 +576,30 @@ def collect_statistics_information(data): s_project_field_objs = core.models.SampleProjectsFields.objects.filter( sample_projects_id=s_project_obj ) + field_ids = list(s_project_field_objs.values_list("id", flat=True)) + total_counts = dict( + core.models.SampleProjectsFieldsValue.objects.filter( + sample_project_field_id__in=field_ids + ) + .values_list("sample_project_field_id") + .annotate(total=Count("id")) + ) + not_none_counts = dict( + core.models.SampleProjectsFieldsValue.objects.filter( + sample_project_field_id__in=field_ids + ) + .exclude(sample_project_field_value__in=["None", ""]) + .values_list("sample_project_field_id") + .annotate(total=Count("id")) + ) for s_project_field_obj in s_project_field_objs: f_name = s_project_field_obj.get_field_name() - if not core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id=s_project_field_obj - ).exists(): + total_count = total_counts.get(s_project_field_obj.pk, 0) + if total_count == 0: stats_data["never_used"].append(f_name) stats_data["fields_value"][f_name] = 0 continue - count_not_none = ( - core.models.SampleProjectsFieldsValue.objects.filter( - sample_project_field_id=s_project_field_obj - ) - .exclude(sample_project_field_value__in=["None", ""]) - .count() - ) + count_not_none = not_none_counts.get(s_project_field_obj.pk, 0) stats_data["fields_value"][f_name] = count_not_none if count_not_none == 0: stats_data["always_none"].append(f_name) diff --git a/wetlab/api/views.py b/wetlab/api/views.py index 5d3ff5952..0ce09a933 100644 --- a/wetlab/api/views.py +++ b/wetlab/api/views.py @@ -1,4 +1,5 @@ from django.http import QueryDict +from django.db.models.functions import Lower from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status @@ -12,11 +13,13 @@ from rest_framework.response import Response import core.models +import core.core_config import wetlab.api.serializers import wetlab.api.utils.lab import wetlab.api.utils.sample import wetlab.models import wetlab.config +import wetlab.utils.common sample_project_fields = openapi.Parameter( "project", @@ -180,21 +183,71 @@ def create_sample_data(request): if isinstance(data, QueryDict): data = data.dict() if "sample_name" not in data or "sample_project" not in data: - return Response(status=status.HTTP_400_BAD_REQUEST) - if core.models.Samples.objects.filter( - sample_name__iexact=data["sample_name"] - ).exists(): + return Response( + { + "ERROR": "Missing fields `sample_name` or `sample_project` in data", + "data": data, + }, + status=status.HTTP_400_BAD_REQUEST, + ) + not_allowed_sample_names = [] + allowed_sample_repeat = ( + False + if wetlab.utils.common.get_configuration_from_database( + "ALLOW_REPEAT_SAMPLE_NAMES" + ) + == "FALSE" + else True + ) + if not allowed_sample_repeat: + if ( + wetlab.utils.common.get_configuration_from_database( + "ALLOW_REPEAT_USER_SAMPLE_NAMES" + ) + == "FALSE" + ): + not_allowed_sample_names = list( + core.models.Samples.objects.filter( + sample_user__username__iexact=request.user.username + ).values_list(Lower("sample_name"), flat=True) + ) + else: + not_allowed_sample_names = list( + core.models.Samples.objects.values_list( + Lower("sample_name"), flat=True + ) + ) + # get information it underscore is allowed in sample name + allow_underscore = ( + False + if wetlab.utils.common.get_configuration_from_database( + "ALLOW_UNDERSCORE_SAMPLE_NAMES" + ) + == "FALSE" + else True + ) + if data["sample_name"].lower() in not_allowed_sample_names: error = {"ERROR": "sample already defined"} return Response(error, status=status.HTTP_400_BAD_REQUEST) + if not allow_underscore: + if "_" in data["sample_name"]: + error = {"ERROR": "sample name cannot have underscore"} + return Response(error, status=status.HTTP_400_BAD_REQUEST) split_data = wetlab.api.utils.sample.split_sample_data(data) if not isinstance(split_data, dict): - return Response(split_data, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"ERROR": "Error splitting data", "data": split_data}, + status=status.HTTP_400_BAD_REQUEST, + ) apps_name = __package__.split(".")[0] inst_req_sample = wetlab.api.utils.sample.include_instances_in_sample( split_data["s_data"], split_data["lab_data"], apps_name ) if not isinstance(inst_req_sample, dict): - return Response(inst_req_sample, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"ERROR": "Error including data", "data": inst_req_sample}, + status=status.HTTP_400_BAD_REQUEST, + ) split_data["s_data"] = inst_req_sample split_data["s_data"]["sample_user"] = request.user.pk # Adding coding for sample @@ -208,7 +261,11 @@ def create_sample_data(request): ) if not sample_serializer.is_valid(): return Response( - sample_serializer.errors, status=status.HTTP_400_BAD_REQUEST + { + "ERROR": f"Error serializing sample {data['sample_name']}", + "data": sample_serializer.errors, + }, + status=status.HTTP_400_BAD_REQUEST, ) new_sample_id = sample_serializer.save().get_sample_id() for d_field in split_data["p_data"]: @@ -218,44 +275,23 @@ def create_sample_data(request): ) if not s_project_serializer.is_valid(): return Response( - s_project_serializer.errors, status=status.HTTP_400_BAD_REQUEST + { + "ERROR": "Error serializing project", + "data": s_project_serializer.errors, + }, + status=status.HTTP_400_BAD_REQUEST, ) s_project_serializer.save() - return Response("Successful upload information", status=status.HTTP_201_CREATED) - return Response(status=status.HTTP_400_BAD_REQUEST) - - -@swagger_auto_schema( - method="get", - operation_description="Get the stored Run information available in iSkyLIMS for the list of samples", - manual_parameters=[sample_in_run], -) -@api_view(["GET"]) -def fetch_run_information(request): - if "samples" in request.GET: - samples = request.GET["samples"] - # sample_run_info = get_run_info_for_sample(apps_name, samples) - s_list = samples.strip().split(",") - s_data = [] - for sample in s_list: - sample = sample.strip() - if wetlab.models.SamplesInProject.objects.filter( - sample_name__iexact=sample - ).exists(): - s_found_objs = wetlab.models.SamplesInProject.objects.filter( - sample_name__iexact=sample - ) - for s_found_obj in s_found_objs: - s_data.append( - wetlab.api.serializers.SampleRunInfoSerializers( - s_found_obj, many=False - ).data - ) - else: - s_data.append({"sample_name": sample, "Run data": "Not found"}) - return Response(s_data, status=status.HTTP_200_OK) - return Response(status=status.HTTP_400_BAD_REQUEST) + return Response( + {"message": "Successful upload information", "data": split_data["s_data"]}, + status=status.HTTP_201_CREATED, + ) + else: + return Response( + {"ERROR": f"Request method must be POST, received {request.method}"}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( @@ -265,7 +301,30 @@ def fetch_run_information(request): ) @api_view(["GET"]) def fetch_sample_information(request): + """This request is used to get the sample information from the database. If + sequencing is received in the request, the request will return the run + information for this sample. + If not then all infromation is related to the sample creation, which means + sample project, project fields. + """ sample_data = {} + if "sequencing" in request.GET and "sample" in request.GET: + sample = request.GET["sample"].strip() + s_data = [] + if wetlab.models.SamplesInProject.objects.filter( + sample_name__iexact=sample + ).exists(): + sample_obj = wetlab.models.SamplesInProject.objects.filter( + sample_name__iexact=sample + ) + s_data.append( + wetlab.api.serializers.SampleRunInfoSerializers( + sample_obj, many=False + ).data + ) + else: + s_data.append({"sample_name": sample, "Run data": "Not found"}) + return Response(s_data, status=status.HTTP_200_OK) if "sample" in request.GET: sample = request.GET["sample"] if not core.models.Samples.objects.filter(sample_name__iexact=sample).exists(): @@ -375,6 +434,18 @@ def sample_project_fields(request): return Response(status=status.HTTP_400_BAD_REQUEST) +@swagger_auto_schema( + method="get", + operation_description="Use this request to get the fields used to fill lab_request model", + manual_parameters=[laboratory], +) +@api_view(["GET"]) +def get_lab_request_mapping(request): + return Response( + {"data": core.core_config.LAB_REQUEST_ONTOLOGY_MAP}, status=status.HTTP_200_OK + ) + + @swagger_auto_schema(method="get", manual_parameters=[laboratory]) @api_view(["GET"]) def get_lab_information_contact(request): @@ -473,6 +544,35 @@ def statistic_information(request): @api_view(["PUT"]) @permission_classes([IsAuthenticated]) def update_lab(request): + """ + Handles updating or creating a laboratory instance based on incoming PUT request data. + + If a lab with the provided `lab_name` exists, it updates the existing record. + If the lab does not exist and the `create_if_missing` flag is set in request.data, + it attempts to create a new lab using the provided information. + + Required key in request.data: + - lab_name (str): The name of the laboratory to update or create. + + Optional required keys (if lab does not exist and `create_if_missing` is True): + - lab_contact_name (str) <- updates data if lab exists + - lab_phone (str) <- updates data if lab exists + - lab_email (str) <- updates data if lab exists + - apps_name (str) + - geo_loc_city (str) + - geo_loc_state (str) + - lab_name_coding (str) + - lab_unit (str) + + Args: + request (HttpRequest): The incoming HTTP request containing PUT data. + + Returns: + Response: + - 201 Created: If the lab is successfully created or updated. + - 406 Not Acceptable: If the lab is not found or cannot be created, or if the data is invalid. + - 400 Bad Request: If the HTTP method is not PUT. + """ if request.method == "PUT": data = request.data if isinstance(data, QueryDict): @@ -480,11 +580,33 @@ def update_lab(request): if "lab_name" in data: lab_obj = wetlab.api.utils.lab.get_laboratory_instance(data["lab_name"]) if lab_obj is None: - error_message = wetlab.config.ERROR_LABORATORY_NOT_FOUND - return Response(error_message, status=status.HTTP_406_NOT_ACCEPTABLE) - wetlab.api.serializers.LabRequestSerializer.update(lab_obj, data) - + if data.get("create_if_missing"): + wetlab.api.utils.sample.create_new_laboratory(data) + return Response( + "Successful Creation of new laboratory", + status=status.HTTP_201_CREATED, + ) + else: + error_message = wetlab.config.ERROR_LABORATORY_NOT_FOUND + return Response( + error_message, status=status.HTTP_406_NOT_ACCEPTABLE + ) + else: + serializer = wetlab.api.serializers.LabRequestSerializer( + lab_obj, data=data, partial=True + ) + if serializer.is_valid(): + serializer.save() + return Response( + "Successful Update information", status=status.HTTP_201_CREATED + ) + else: + return Response( + serializer.errors, status=status.HTTP_406_NOT_ACCEPTABLE + ) + else: return Response( - "Successful Update information", status=status.HTTP_201_CREATED + "Missing 'lab_name' field in request data", + status=status.HTTP_406_NOT_ACCEPTABLE, ) return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/wetlab/config.py b/wetlab/config.py index d3c66df32..3beaa3f9a 100644 --- a/wetlab/config.py +++ b/wetlab/config.py @@ -64,6 +64,8 @@ RUN_LOG_FOLDER = "Logs" STATS_FILE_PATH = "Data/Intensities/BaseCalls/Stats" +STATS_FILE_PATH_ALTERNATIVE = "Data/Intensities/BaseCalls/Reports/legacy/Stats" +STATS_FILE_PATHS = [STATS_FILE_PATH, STATS_FILE_PATH_ALTERNATIVE] CONVERSION_STATS_FILE = "ConversionStats.xml" @@ -74,16 +76,24 @@ ["NextSeq", "xml_file"], ["MiSeq", "xml_file"], ["NovaSeq", "txt_file"], + ["iSeq 100", "xml_file"], ] # ########### VALUE TAG FOR XML FILES ######################### -COMPLETION_TAG = "CompletionStatus" -COMPLETION_SUCCESS = ["CompletedAsPlanned", "SuccessfullyCompleted"] +COMPLETION_TAG = ["CompletionStatus", "RunStatus"] +COMPLETION_SUCCESS = ["CompletedAsPlanned", "SuccessfullyCompleted", "RunCompleted"] EXPERIMENT_NAME_TAG = "ExperimentName" APPLICATION_NAME_TAG = "ApplicationName" +APPLICATION_TAG_ALIASES = ["ApplicationName", "Application"] NUMBER_CYCLES_TAG = "NumCycles" RUN_INFO_READ_TAG = "RunInfoRead" NUMBER_TAG = "Number" +PLANNED_READS_TAG = "PlannedReads" +PLANNED_READ_TAG = "Read" +READ_NAME_TAG = "ReadName" +READ_CYCLES_FALLBACK_TAGS = ["NumCycles", "Cycles"] +RUN_INFO_FLOWCELL_LAYOUT_LANE_TAG = "LaneCount" +RUN_DATE_FORMATS = ["%y%m%d", "%Y-%m-%d", "%m/%d/%Y"] ############################################################## RUN_METRIC_GRAPHIC_COMMANDS = [ @@ -121,12 +131,24 @@ "ApplicationVersion", "NumTilesPerSwath", ] +FIELDS_WITHOUT_SETUP_TAG = [ + "NumLanes", + "Application", + "ApplicationVersion", + "NumTilesPerSwath", +] READ_NUMBER_OF_CYCLES = [ "PlannedRead1Cycles", "PlannedIndex1ReadCycles", "PlannedIndex2ReadCycles", "PlannedRead2Cycles", ] +PLANNED_READ_FIELD_MAP = { + "Read1": "PlannedRead1Cycles", + "Index1": "PlannedIndex1ReadCycles", + "Index2": "PlannedIndex2ReadCycles", + "Read2": "PlannedRead2Cycles", +} # NOVASEQ 6000 FIELDS_NOVASEQ_TO_FETCH_TAG = [ "NumLanes", @@ -164,6 +186,7 @@ "[Settings]", "[I7]", ] +ERROR_NO_LIBRARY_KIT_DEFINED = ["Library Kits are not defined yet"] ############################################################## MIGRATION_DIRECTORY_FILES = "wetlab/BaseSpaceMigrationFiles/" @@ -225,15 +248,10 @@ HEADING_FOR_SAMPLE_SHEET_TWO_INDEX = [ "Unique_Sample_ID", "Sample_Name", - "Sample_Plate", - "Sample_Well", - "Index_Plate_Well", - "I7_Index_ID", "index", - "I5_Index_ID", "index2", "Sample_Project", - "Description", + "custom_description", ] @@ -247,6 +265,7 @@ ("index", "i7Index"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] MAP_USER_SAMPLE_SHEET_TO_DATABASE_NEXTSEQ_PAIRED_END = [ @@ -260,6 +279,7 @@ ("index2", "i5Index"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] MAP_USER_SAMPLE_SHEET_TO_DATABASE_MISEQ_SINGLE_READ_VERSION_5 = [ @@ -271,6 +291,7 @@ ("index", "i7Index"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] MAP_USER_SAMPLE_SHEET_TO_DATABASE_MISEQ_PAiRED_END_VERSION_5 = [ @@ -284,6 +305,7 @@ ("index2", "i5Index"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] MAP_USER_SAMPLE_SHEET_TO_DATABASE_MISEQ_SINGLE_READ_VERSION_4 = [ @@ -295,6 +317,7 @@ ("index", "i7Index"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] MAP_USER_SAMPLE_SHEET_TO_DATABASE_MISEQ_PAiRED_END_VERSION_4 = [ @@ -308,6 +331,7 @@ ("index2", "i5Index"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] @@ -325,6 +349,7 @@ ("GenomeFolder", "genomeFolder"), ("Sample_Project", "projectInSampleSheet"), ("Description", "userInSampleSheet"), + ("custom_description", "userInSampleSheet"), ] # ######## MAPPING OPTIONAL COLUMNS THAT COULD BE IN SAMPLE SHEET FROM USER TO DATABASE ############# MAP_USER_SAMPLE_SHEET_ADDITIONAL_FIELDS_FROM_TYPE_OF_SECUENCER = [ @@ -334,7 +359,26 @@ ] # Sections to check in the IEM file created by user -SECTIONS_IN_IEM_SAMPLE_SHEET = ["[Header]", "[Reads]", "[Settings]", "[Data]"] +SECTIONS_IN_IEM_SAMPLE_SHEET = ["Header", "Reads", "Settings", "Data"] +SECTIONS_IN_V2_SAMPLE_SHEET = [ + "Header", + "Reads", + "Sequencing_Settings", + "BCLConvert_Settings", + "BCLConvert_Data", + "Cloud_Settings", + "Cloud_Data", + "CustomCustomer_Data", +] + +# Possible names of the Tabular data iskylims user field + +TABULAR_DATA_ISKYLIMS_USER_COLUMN = {"1": "Description", "2": "custom_description"} + +# Tabular data sections in samplesheets. +TABULAR_DATA_SECTIONS_SAMPLE_SHEET = {"1": "Data", "2": "BCLConvert_Data"} + +SETTINGS_SECTIONS_SAMPLE_SHEET = ["Settings", "BCLConvert_Settings"] FIELDS_IN_SAMPLE_SHEET_HEADER_IEM_VERSION_5 = [ "Date", @@ -348,26 +392,31 @@ "Description", ] +ADAPTER_1_FIELD_NAMES = ["Adapter", "AdapterRead1"] + +ADAPTER_2_FIELD_NAMES = ["Adapter", "AdapterRead2"] + + # #### HEADINGS VALUES # # Heading for pending Library Preparation state HEADING_FOR_SAMPLES_TO_DEFINE_PROTOCOL = [ "Sample Name", - "Molecule Code ID", + "Extraction Code ID", "Library Preparation Protocol", ] HEADING_FOR_LIBRARY_PREPARATION_STATE = [ "Sample extraction date", "Sample", - "Molecule Code ID", + "Extraction Code ID", "Molecule Extraction Date", "Used Protocol", "UserID", ] -# ######HEADING_FOR_ADD_LIBRARY_PREPARATION = ['Molecule Code ID', 'Protocol', 'Extraction Date', 'To be included'] +# ######HEADING_FOR_ADD_LIBRARY_PREPARATION = ['Extraction Code ID', 'Protocol', 'Extraction Date', 'To be included'] HEADING_FOR_ADD_LIBRARY_PREPARATION_PARAMETERS = [ "Library Preparation Code ID", "Sample Name", @@ -381,7 +430,7 @@ ] HEADING_FIX_FOR_ASSING_ADDITIONAL_KITS = ["Sample Name", "Library Preparation Code ID"] HEADING_FOR_CREATION_LIBRARY_PREPARATION = [ - "Molecule Code ID", + "Extraction Code ID", "Protocol used", "Single/Paired end", "Length read", @@ -401,7 +450,7 @@ # ## Heading for display information on library Preparation definition HEADING_FOR_LIBRARY_PREPARATION_DEFINITION = [ "Library CodeID", - "Molecule CodeID ", + "Extraction Code ID ", "Lib Preparation State", "Protocol name", "Project Name", @@ -567,20 +616,46 @@ HEADING_FOR_STATISTICS_RUNS_BASIC_DATA = ["Run Name", "Date sequencer start"] -HEADING_STATISTICS_FOR_RESEARCHER_SAMPLE = [ - "Samples", +HEADING_STATISTICS_FOR_SECUENCED_RESEARCHER_SAMPLE = [ + "Sample name", "Project name", "Run name", "Platform", ] +HEADING_STATISTICS_FOR_RECORDED_RESEARCHER_SAMPLE = [ + "sample name", + "unique sample ID", + "sample type", + "specimen type", + "sample state", + "project name", +] +HEADING_STATISTICS_FOR_RECORDED_LAB_SAMPLE = [ + "sample name", + "unique ID", + "sample type", + "specimen type", + "sample state", + "project name", + "user name", +] HEADING_STATISTICS_FOR_TIME_RUN = ["Run name", "Run state", "Sequencer", "Run date"] -HEADING_STATISTICS_FOR_TIME_SAMPLE = [ +HEADING_STATISTICS_FOR_TIME_SEQUENCED_SAMPLE = [ "Sample name", "Researcher", "Project name", "Run name", "Barcode", ] +HEADING_STATISTICS_FOR_TIME_DEFINED_SAMPLE = [ + "Sample name", + "Unique ID", + "State", + "Recorded date", + "Species", + "Sample type", + "lab code", +] HEADING_STATISTICS_FOR_SEQUENCER_RUNS = [ "Run name", "Run state", @@ -645,6 +720,10 @@ ERROR_SAMPLE_SHEET_BOTH_INSTRUMENT_AND_INDEX_NOT_INCLUDED = [ "Sample Sheet does not have Instrument type neither Index Adapters" ] +ERROR_SAMPLE_SHEET_HAS_INVALID_HEADING = [ + "Sample sheet does not have a valid sample heading" +] +ERROR_SAMPLE_SHEET_DOES_NOT_HAVE_PROJECTS = ["Sample sheet does not have Projects"] ERROR_SAMPLE_SHEET_USER_IS_NOT_DEFINED = ["User in sample sheet is not defined"] ERROR_SAMPLE_SHEET_DOES_NOT_HAVE_DESCRIPTION_FIELD = [ "Sample sheet does not have Description column " @@ -655,14 +734,16 @@ ERROR_SAMPLE_SHEET_USER_ARE_NOT_DEFINED = ( "Sample sheet has users which are not defined : " ) +ERROR_SAMPLE_SHEET_HAS_INVALID_LINES = ( + "Sample sheet has an invalid (Non-empty, non-comma-delimited) line: " +) ERROR_USER_SAMPLE_SHEET_NO_LONGER_EXISTS = [ "The Sample Sheet that you are uploaded does not longer exists", "Upload again the sample sheet", ] ERROR_EMPTY_VALUES = [ - "Your request cannot be recorded because ", - "it contains empty values", + "Your request cannot be recorded because it contains empty values", ] ERROR_SAMPLE_PROJECT_ALREADY_EXISTS = ["Sample Project is already defined"] @@ -736,15 +817,15 @@ ] # ERROR TEXT FOR SEACHING ############################################# -ERROR_INVALID_FORMAT_FOR_DATES = "Invalid date format. Use the format (DD-MM-YYYY)" +ERROR_INVALID_FORMAT_FOR_DATES = ["Invalid date format. Use the format (DD-MM-YYYY)"] ERROR_MANY_USER_MATCHES_FOR_INPUT_CONDITIONS = [ "There are many user names that matches your request" ] -ERROR_NO_MATCHES_FOR_INPUT_CONDITIONS = ( +ERROR_NO_MATCHES_FOR_INPUT_CONDITIONS = [ "There is not any match for your input conditions" -) +] ERROR_NO_MATCHES_FOR_LIBRARY_STATISTICS = ( "There is not any Index Library Kit that mathes your input conditions" ) @@ -762,9 +843,9 @@ ] ERROR_NO_SAMPLES_SELECTED = "They were not selected any Sample on your request" -ERROR_NOT_RUNS_FOUND_IN_SELECTED_PERIOD = ( +ERROR_NOT_RUNS_FOUND_IN_SELECTED_PERIOD = [ "There are not runs for the selected period of time" -) +] ERROR_NOT_SAMPLES_FOR_USER_FOUND_BECAUSE_OF_CONFIGURATION_SETTINGS = [ "No results. This could because the DESCRIPTION_IN_SAMPLE_SHEET_MUST_HAVE_USERNAME setting is set fo FALSE" ] @@ -784,6 +865,7 @@ ERROR_WRONG_SAMBA_CONFIGURATION_SETTINGS = ( "Unsuccessful configuration settings for Samba connection" ) +ERROR_USER_NOT_DEFINED = ["User is not defined"] ERROR_WRONG_SAMBA_FOLDER_SETTINGS = ( "Unsuccessful configuration. Samba folder not reachable" ) @@ -807,7 +889,7 @@ # ######################## Configuration test errors ##################################### ERROR_NOT_FOLDER_RUN_TEST_WAS_FOUND = [ "Unable to run the configuration test", - "Run test folder was found on remote server", + "Run test folder was not found on remote server", ] ERROR_NO_RUN_TEST_WAS_CREATED = [ "Unable to continue with configuration testing", diff --git a/wetlab/cron.py b/wetlab/cron.py index a7190a837..124411f7d 100644 --- a/wetlab/cron.py +++ b/wetlab/cron.py @@ -113,7 +113,7 @@ def delete_invalid_run(): days=int(wetlab.config.RETENTION_TIME) ) run_found_for_deleting = wetlab.models.RunProcess.objects.filter( - state__run_state_name="Pre-Recorded", generate_dat__lte=date_for_removing + state__run_state_name="pre_recorded", generate_dat__lte=date_for_removing ) for run_found in run_found_for_deleting: diff --git a/wetlab/migrations/0001_initial.py b/wetlab/migrations/0001_initial.py new file mode 100644 index 000000000..b2d84ab8b --- /dev/null +++ b/wetlab/migrations/0001_initial.py @@ -0,0 +1,1149 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0001_initial"), + ("django_utils", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="AdditionaKitsLibPrepare", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("kit_name", models.CharField(max_length=255)), + ( + "description", + models.CharField(blank=True, max_length=400, null=True), + ), + ("kit_order", models.IntegerField()), + ("kit_used", models.BooleanField()), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "commercial_kit_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.commercialkits", + ), + ), + ( + "protocol_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="core.protocols" + ), + ), + ( + "register_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "wetlab_lib_additional_kits_lib_prepare", + }, + ), + migrations.CreateModel( + name="CollectionIndexKit", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("collection_index_name", models.CharField(max_length=125)), + ("version", models.CharField(max_length=80, null=True)), + ("plate_extension", models.CharField(max_length=125, null=True)), + ("adapter_1", models.CharField(max_length=125, null=True)), + ("adapter_2", models.CharField(max_length=125, null=True)), + ( + "collection_index_file", + models.FileField( + max_length=500, upload_to="wetlab/collection_index_kits/" + ), + ), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ], + options={ + "db_table": "wetlab_collection_index_kit", + }, + ), + migrations.CreateModel( + name="ConfigSetting", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("configuration_name", models.CharField(max_length=80)), + ( + "configuration_value", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "wetlab_config_setting", + }, + ), + migrations.CreateModel( + name="LibPrepareStates", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("lib_prep_state", models.CharField(max_length=50)), + ], + options={ + "db_table": "wetlab_lib_prepare_states", + }, + ), + migrations.CreateModel( + name="LibraryKit", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("library_name", models.CharField(max_length=125)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "wetlab_library_kit", + }, + ), + migrations.CreateModel( + name="PoolStates", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("pool_state", models.CharField(max_length=50)), + ], + options={ + "db_table": "wetlab_pool_states", + }, + ), + migrations.CreateModel( + name="Projects", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "base_space_library", + models.CharField(blank=True, max_length=45, null=True), + ), + ("project_name", models.CharField(max_length=45)), + ( + "library_kit", + models.CharField(blank=True, max_length=125, null=True), + ), + ( + "base_space_file", + models.CharField(blank=True, max_length=255, null=True), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ("project_run_date", models.DateField(blank=True, null=True)), + ( + "library_kit_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.librarykit", + ), + ), + ], + options={ + "db_table": "wetlab_projects", + }, + ), + migrations.CreateModel( + name="RunConfigurationTest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("run_test_name", models.CharField(max_length=80)), + ("run_test_folder", models.CharField(max_length=200)), + ], + options={ + "db_table": "wetlab_lib_run_configuration_test", + }, + ), + migrations.CreateModel( + name="RunErrors", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("error_code", models.CharField(max_length=10)), + ("error_text", models.CharField(max_length=255)), + ], + options={ + "db_table": "wetlab_run_errors", + }, + ), + migrations.CreateModel( + name="RunProcess", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("run_name", models.CharField(max_length=45)), + ( + "sample_sheet", + models.FileField( + blank=True, null=True, upload_to="wetlab/sample_sheet/" + ), + ), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ("run_date", models.DateField(blank=True, null=True)), + ("run_finish_date", models.DateTimeField(blank=True, null=True)), + ("bcl2fastq_finish_date", models.DateTimeField(blank=True, null=True)), + ("run_completed_date", models.DateTimeField(blank=True, null=True)), + ( + "run_forced_continue_on_error", + models.BooleanField(blank=True, default=False, null=True), + ), + ( + "index_library", + models.CharField(blank=True, max_length=85, null=True), + ), + ("samples", models.CharField(blank=True, max_length=45)), + ("use_space_img_mb", models.CharField(blank=True, max_length=10)), + ("use_space_fasta_mb", models.CharField(blank=True, max_length=10)), + ("use_space_other_mb", models.CharField(blank=True, max_length=10)), + ( + "center_requested_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="django_utils.center", + ), + ), + ( + "reagent_kit", + models.ManyToManyField(blank=True, to="core.userlotcommercialkits"), + ), + ( + "run_error", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runerrors", + ), + ), + ], + options={ + "db_table": "wetlab_run_process", + }, + ), + migrations.CreateModel( + name="RunStates", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("run_state_name", models.CharField(max_length=50)), + ], + options={ + "db_table": "wetlab_run_states", + }, + ), + migrations.CreateModel( + name="SambaConnectionData", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "samba_folder_name", + models.CharField(blank=True, max_length=80, null=True), + ), + ("domain", models.CharField(blank=True, max_length=80, null=True)), + ("host_name", models.CharField(blank=True, max_length=80, null=True)), + ("ip_server", models.CharField(blank=True, max_length=20, null=True)), + ("port_server", models.CharField(blank=True, max_length=10, null=True)), + ( + "remote_server_name", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "shared_folder_name", + models.CharField(blank=True, max_length=80, null=True), + ), + ("user_id", models.CharField(blank=True, max_length=80, null=True)), + ( + "user_password", + models.CharField(blank=True, max_length=20, null=True), + ), + ("is_direct_tcp", models.BooleanField(default=True)), + ("ntlm_used", models.BooleanField(default=True)), + ], + options={ + "db_table": "wetlab_samba_connection_data", + }, + ), + migrations.CreateModel( + name="RunningParameters", + fields=[ + ( + "run_name_id", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="wetlab.runprocess", + ), + ), + ("run_id", models.CharField(max_length=255)), + ("experiment_name", models.CharField(max_length=255)), + ( + "rta_version", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "system_suite_version", + models.CharField(blank=True, max_length=255, null=True), + ), + ("library_id", models.CharField(blank=True, max_length=255, null=True)), + ("chemistry", models.CharField(blank=True, max_length=255, null=True)), + ( + "run_start_date", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "analysis_workflow_type", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "run_management_type", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "planned_read1_cycles", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "planned_read2_cycles", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "planned_index1_read_cycles", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "planned_index2_read_cycles", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "application_version", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "num_tiles_per_swath", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "image_channel", + models.CharField(blank=True, max_length=255, null=True), + ), + ("flowcell", models.CharField(blank=True, max_length=255, null=True)), + ( + "image_dimensions", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "flowcell_layout", + models.CharField(blank=True, max_length=255, null=True), + ), + ], + options={ + "db_table": "wetlab_running_parameters", + }, + ), + migrations.CreateModel( + name="StatsRunSummary", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("level", models.CharField(max_length=20)), + ("yield_total", models.CharField(max_length=10)), + ("projected_total_yield", models.CharField(max_length=10)), + ("aligned", models.CharField(max_length=10)), + ("error_rate", models.CharField(max_length=10)), + ("intensity_cycle", models.CharField(max_length=10)), + ("bigger_q30", models.CharField(max_length=10)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ("stats_summary_run_date", models.DateField(null=True)), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_stats_run_summary", + }, + ), + migrations.CreateModel( + name="StatsRunRead", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("read", models.CharField(max_length=10)), + ("lane", models.CharField(max_length=10)), + ("tiles", models.CharField(max_length=10)), + ("density", models.CharField(max_length=40)), + ("cluster_pf", models.CharField(max_length=40)), + ("phas_prephas", models.CharField(max_length=40)), + ("reads", models.CharField(max_length=40)), + ("reads_pf", models.CharField(max_length=40)), + ("q30", models.CharField(max_length=40)), + ("yields", models.CharField(max_length=40)), + ("cycles_err_rated", models.CharField(max_length=40)), + ("aligned", models.CharField(max_length=40)), + ("error_rate", models.CharField(max_length=40)), + ("error_rate_35", models.CharField(max_length=40)), + ("error_rate_50", models.CharField(max_length=40)), + ("error_rate_75", models.CharField(max_length=40)), + ("error_rate_100", models.CharField(max_length=40)), + ("intensity_cycle", models.CharField(max_length=40)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ("stats_read_run_date", models.DateField(null=True)), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_stats_run_read", + }, + ), + migrations.CreateModel( + name="StatsLaneSummary", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("default_all", models.CharField(max_length=40, null=True)), + ("lane", models.CharField(max_length=10)), + ("pf_cluster", models.CharField(max_length=64)), + ("percent_lane", models.CharField(max_length=64)), + ("perfect_barcode", models.CharField(max_length=64)), + ("one_mismatch", models.CharField(max_length=64)), + ("yield_mb", models.CharField(max_length=64)), + ("bigger_q30", models.CharField(max_length=64)), + ("mean_quality", models.CharField(max_length=64)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "project_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.projects", + ), + ), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_stats_lane_summary", + }, + ), + migrations.CreateModel( + name="StatsFlSummary", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("default_all", models.CharField(max_length=40, null=True)), + ("flow_raw_cluster", models.CharField(max_length=40)), + ("flow_pf_cluster", models.CharField(max_length=40)), + ("flow_yield_mb", models.CharField(max_length=40)), + ("sample_number", models.CharField(max_length=40)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "project_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.projects", + ), + ), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_stats_fl_summary", + }, + ), + migrations.CreateModel( + name="SamplesInProject", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sample_name", models.CharField(max_length=255)), + ("barcode_name", models.CharField(max_length=255)), + ("pf_clusters", models.CharField(max_length=55)), + ("percent_in_project", models.CharField(max_length=25)), + ("yield_mb", models.CharField(max_length=55)), + ("quality_q30", models.CharField(max_length=55)), + ("mean_quality", models.CharField(max_length=55)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "project_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.projects", + ), + ), + ( + "run_process_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ( + "sample_in_core", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.samples", + ), + ), + ( + "user_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "wetlab_samples_in_project", + }, + ), + migrations.AddField( + model_name="runprocess", + name="state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_of_run", + to="wetlab.runstates", + ), + ), + migrations.AddField( + model_name="runprocess", + name="state_before_error", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runstates", + ), + ), + migrations.AddField( + model_name="runprocess", + name="used_sequencer", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencerinlab", + ), + ), + migrations.CreateModel( + name="RawTopUnknowBarcodes", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("lane_number", models.CharField(max_length=4)), + ("top_number", models.CharField(max_length=4)), + ("count", models.CharField(max_length=40)), + ("sequence", models.CharField(max_length=40)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_raw_top_unknown_barcodes", + }, + ), + migrations.CreateModel( + name="RawDemuxStats", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("default_all", models.CharField(max_length=40, null=True)), + ("raw_yield", models.CharField(max_length=255)), + ("raw_yield_q30", models.CharField(max_length=255)), + ("raw_quality", models.CharField(max_length=255)), + ("pf_yield", models.CharField(max_length=255)), + ("pf_yield_q30", models.CharField(max_length=255)), + ("pf_quality_score", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "project_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.projects", + ), + ), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_raw_demux_stats", + }, + ), + migrations.AddField( + model_name="projects", + name="run_process", + field=models.ManyToManyField(to="wetlab.runprocess"), + ), + migrations.AddField( + model_name="projects", + name="user_id", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="LibUserSampleSheet", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sample_sheet", + models.FileField(upload_to="wetlab/sample_sheets_lib_prep/"), + ), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ("application", models.CharField(blank=True, max_length=70, null=True)), + ("instrument", models.CharField(blank=True, max_length=70, null=True)), + ("adapter_1", models.CharField(blank=True, max_length=70, null=True)), + ("adapter_2", models.CharField(blank=True, max_length=70, null=True)), + ("assay", models.CharField(blank=True, max_length=70, null=True)), + ("reads", models.CharField(blank=True, max_length=10, null=True)), + ("confirmed_used", models.BooleanField(default=False)), + ("iem_version", models.CharField(blank=True, max_length=5, null=True)), + ( + "collection_index_kit_id", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.collectionindexkit", + ), + ), + ( + "register_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "sequencing_configuration", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencingconfiguration", + ), + ), + ], + options={ + "db_table": "wetlab_lib_user_samplesheet", + }, + ), + migrations.CreateModel( + name="LibraryPool", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("pool_name", models.CharField(max_length=50)), + ("sample_number", models.IntegerField(default=0)), + ("pool_code_id", models.CharField(blank=True, max_length=50)), + ("adapter", models.CharField(blank=True, max_length=50, null=True)), + ("paired_end", models.CharField(blank=True, max_length=10, null=True)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "platform", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.sequencingplatform", + ), + ), + ( + "pool_state", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.poolstates", + ), + ), + ( + "register_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "run_process_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_library_pool", + "ordering": ("pool_name",), + }, + ), + migrations.CreateModel( + name="LibPrepare", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "lib_prep_code_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "user_sample_id", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "project_in_samplesheet", + models.CharField(blank=True, max_length=80, null=True), + ), + ( + "sample_plate", + models.CharField(blank=True, max_length=50, null=True), + ), + ("sample_well", models.CharField(blank=True, max_length=20, null=True)), + ( + "index_plate_well", + models.CharField(blank=True, max_length=20, null=True), + ), + ("i7_index_id", models.CharField(blank=True, max_length=25, null=True)), + ("i7_index", models.CharField(blank=True, max_length=30, null=True)), + ("i5_index_id", models.CharField(blank=True, max_length=25, null=True)), + ("i5_index", models.CharField(blank=True, max_length=30, null=True)), + ( + "genome_folder", + models.CharField(blank=True, max_length=180, null=True), + ), + ("manifest", models.CharField(blank=True, max_length=80, null=True)), + ("reused_number", models.IntegerField(default=0)), + ("unique_id", models.CharField(blank=True, max_length=16, null=True)), + ( + "user_in_samplesheet", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "samplename_in_samplesheet", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "prefix_protocol", + models.CharField(blank=True, max_length=25, null=True), + ), + ( + "lib_prep_state", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.libpreparestates", + ), + ), + ( + "molecule_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.moleculepreparation", + ), + ), + ("pools", models.ManyToManyField(blank=True, to="wetlab.librarypool")), + ( + "protocol_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.protocols", + ), + ), + ( + "register_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "sample_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.samples", + ), + ), + ( + "user_lot_kit_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.userlotcommercialkits", + ), + ), + ( + "user_sample_sheet", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.libusersamplesheet", + ), + ), + ], + options={ + "db_table": "wetlab_lib_prepare", + "ordering": ("lib_prep_code_id",), + }, + ), + migrations.CreateModel( + name="LibParameterValue", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("parameter_value", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "library_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.libprepare", + ), + ), + ( + "parameter_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.protocolparameters", + ), + ), + ], + options={ + "db_table": "wetlab_lib_parameter_value", + }, + ), + migrations.CreateModel( + name="GraphicsStats", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("folder_run_graphic", models.CharField(max_length=255)), + ("cluster_count_graph", models.CharField(max_length=255)), + ("flowcell_graph", models.CharField(max_length=255)), + ("intensity_by_cycle_graph", models.CharField(max_length=255)), + ("heatmap_graph", models.CharField(max_length=255)), + ("histogram_graph", models.CharField(max_length=255)), + ("sample_qc_graph", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True, null=True)), + ( + "runprocess_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.runprocess", + ), + ), + ], + options={ + "db_table": "wetlab_graphics_stats", + }, + ), + migrations.CreateModel( + name="CollectionIndexValues", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("default_well", models.CharField(max_length=10, null=True)), + ("index_7", models.CharField(max_length=25, null=True)), + ("i_7_seq", models.CharField(max_length=25, null=True)), + ("index_5", models.CharField(max_length=25, null=True)), + ("i_5_seq", models.CharField(max_length=25, null=True)), + ( + "collection_index_kit_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.collectionindexkit", + ), + ), + ], + options={ + "db_table": "wetlab_collection_index_values", + }, + ), + migrations.CreateModel( + name="AdditionalUserLotKit", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.CharField(max_length=255)), + ("generated_at", models.DateTimeField(auto_now_add=True)), + ( + "additional_lot_kits", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.additionakitslibprepare", + ), + ), + ( + "lib_prep_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="wetlab.libprepare", + ), + ), + ( + "user_lot_kit_id", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.userlotcommercialkits", + ), + ), + ], + options={ + "db_table": "wetlab_lib_additional_user_lot_kit", + }, + ), + ] diff --git a/wetlab/migrations/0002_remove_librarypool_run_process_id_and_more.py b/wetlab/migrations/0002_remove_librarypool_run_process_id_and_more.py new file mode 100644 index 000000000..46552de57 --- /dev/null +++ b/wetlab/migrations/0002_remove_librarypool_run_process_id_and_more.py @@ -0,0 +1,92 @@ +# Generated by Django 4.2.25 on 2026-02-11 16:33 + +from django.db import migrations, models + + +def drop_librarypool_run_process_id(apps, schema_editor): + with schema_editor.connection.cursor() as cursor: + cursor.execute(""" + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'wetlab_library_pool' + AND COLUMN_NAME = 'run_process_id_id' + """) + if cursor.fetchone() is None: + return + + cursor.execute(""" + SELECT CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'wetlab_library_pool' + AND COLUMN_NAME = 'run_process_id_id' + AND REFERENCED_TABLE_NAME IS NOT NULL + """) + for (constraint_name,) in cursor.fetchall(): + cursor.execute( + f"ALTER TABLE wetlab_library_pool DROP FOREIGN KEY `{constraint_name}`" + ) + + cursor.execute("ALTER TABLE wetlab_library_pool DROP COLUMN run_process_id_id") + + +class Migration(migrations.Migration): + + dependencies = [ + ("wetlab", "0001_initial"), + ] + + operations = [ + migrations.RunSQL( + """ + UPDATE wetlab_raw_top_unknown_barcodes + SET count = NULL + WHERE count IS NOT NULL AND count NOT REGEXP '^[0-9]+$' + """, + reverse_sql=migrations.RunSQL.noop, + ), + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunPython( + drop_librarypool_run_process_id, migrations.RunPython.noop + ), + ], + state_operations=[ + migrations.RemoveField( + model_name="librarypool", + name="run_process_id", + ), + ], + ), + migrations.AddField( + model_name="runprocess", + name="library_pool", + field=models.ManyToManyField(blank=True, to="wetlab.librarypool"), + ), + migrations.AddField( + model_name="runstates", + name="description", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name="runstates", + name="show_in_stats", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="runstates", + name="state_display", + field=models.CharField(blank=True, max_length=80, null=True), + ), + migrations.AlterField( + model_name="libprepare", + name="prefix_protocol", + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name="rawtopunknowbarcodes", + name="count", + field=models.IntegerField(), + ), + ] diff --git a/wetlab/migrations/__init__.py b/wetlab/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wetlab/models.py b/wetlab/models.py index fec88a843..4948ca7c1 100644 --- a/wetlab/models.py +++ b/wetlab/models.py @@ -1,4 +1,5 @@ # Generic imports +import ast import os import re @@ -11,6 +12,131 @@ import wetlab.config +class PoolStates(models.Model): + pool_state = models.CharField(max_length=50) + + class Meta: + db_table = "wetlab_pool_states" + + def __str__(self): + return "%s" % (self.pool_state) + + def get_pool_state(self): + return "%s" % (self.pool_state) + + +class LibraryPoolManager(models.Manager): + def create_lib_pool(self, pool_data): + platform_obj = core.models.SequencingPlatform.objects.filter( + platform_name__exact=pool_data["platform"] + ).last() + new_library_pool = self.create( + register_user=pool_data["registerUser"], + pool_state=PoolStates.objects.get(pool_state__exact="Defined"), + pool_name=pool_data["poolName"], + pool_code_id=pool_data["poolCodeID"], + adapter=pool_data["adapter"], + paired_end=pool_data["pairedEnd"], + sample_number=pool_data["n_samples"], + platform=platform_obj, + ) + return new_library_pool + + +class LibraryPool(models.Model): + register_user = models.ForeignKey(User, on_delete=models.CASCADE) + pool_state = models.ForeignKey(PoolStates, on_delete=models.CASCADE) + platform = models.ForeignKey( + core.models.SequencingPlatform, on_delete=models.CASCADE, null=True, blank=True + ) + pool_name = models.CharField(max_length=50) + sample_number = models.IntegerField(default=0) + pool_code_id = models.CharField(max_length=50, blank=True) + adapter = models.CharField(max_length=50, null=True, blank=True) + paired_end = models.CharField(max_length=10, null=True, blank=True) + generated_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ("pool_name",) + db_table = "wetlab_library_pool" + + def __str__(self): + return "%s" % (self.pool_name) + + def get_adapter(self): + return "%s" % (self.adapter) + + def get_id(self): + return "%s" % (self.pk) + + def get_info(self): + pool_info = [] + pool_info.append(self.pool_name) + pool_info.append(self.pool_code_id) + pool_info.append(self.sample_number) + return pool_info + + def get_number_of_samples(self): + return "%s" % (self.sample_number) + + def get_pool_name(self): + return "%s" % (self.pool_name) + + def get_pool_code_id(self): + return "%s" % (self.pool_code_id) + + def get_pool_single_paired(self): + return "%s" % (self.paired_end) + + def get_platform_name(self): + if self.platform is not None: + return "%s" % (self.platform.get_platform_name()) + else: + return "Not defined" + + def _get_run_processes(self): + return self.runprocess_set.order_by("-generated_at") + + def _get_latest_run_process(self): + return self._get_run_processes().first() + + def get_run_names(self): + return [ + "%s" % (run_process.get_run_name()) + for run_process in self._get_run_processes() + ] + + def get_run_name(self): + run_process = self._get_latest_run_process() + if run_process is not None: + return "%s" % (run_process.get_run_name()) + return "Not defined yet" + + def get_run_id(self): + run_process = self._get_latest_run_process() + if run_process is not None: + return "%s" % (run_process.get_run_id()) + return None + + def get_run_obj(self): + return self._get_latest_run_process() + + def set_pool(self, pool_obj): + self.pool.add(pool_obj) + return self + + def set_pool_state(self, state): + self.pool_state = PoolStates.objects.get(pool_state__exact=state) + self.save() + + def update_number_samples(self, number_s_in_pool): + self.sample_number = number_s_in_pool + self.save() + return self + + objects = LibraryPoolManager() + + class RunErrors(models.Model): error_code = models.CharField(max_length=10) error_text = models.CharField(max_length=255) @@ -24,6 +150,9 @@ def __str__(self): class RunStates(models.Model): run_state_name = models.CharField(max_length=50) + state_display = models.CharField(max_length=80, null=True, blank=True) + description = models.CharField(max_length=255, null=True, blank=True) + show_in_stats = models.BooleanField(default=False) class Meta: db_table = "wetlab_run_states" @@ -37,7 +166,7 @@ def get_run_state_name(self): class RunProcessManager(models.Manager): def create_new_run_from_crontab(self, run_data): - run_state = RunStates.objects.get(run_state_name__exact="Recorded") + run_state = RunStates.objects.get(run_state_name__exact="recorded") new_run = self.create(state=run_state, run_name=run_data["experiment_name"]) return new_run @@ -62,6 +191,7 @@ class RunProcess(models.Model): center_requested_by = models.ForeignKey( django_utils.models.Center, on_delete=models.CASCADE, null=True, blank=True ) + library_pool = models.ManyToManyField(LibraryPool, blank=True) reagent_kit = models.ManyToManyField(core.models.UserLotCommercialKits, blank=True) run_name = models.CharField(max_length=45) sample_sheet = models.FileField( @@ -234,6 +364,10 @@ def update_sample_sheet(self, full_path, file_name): self.sample_sheet.save(file_name, open(full_path, "r"), save=True) return self + def set_library_pool(self, library_pool): + self.library_pool.add(library_pool) + return self + def set_used_space(self, disk_utilization): self.use_space_fasta_mb = disk_utilization["useSpaceFastaMb"] self.use_space_img_mb = disk_utilization["useSpaceImgMb"] @@ -267,7 +401,7 @@ def set_run_error_code(self, error_code): else: self.run_error = RunErrors.objects.get(error_text__exact="Undefined") self.state_before_error = self.state - self.state = RunStates.objects.get(run_state_name__exact="Error") + self.state = RunStates.objects.get(run_state_name__exact="error") self.save() return True @@ -540,10 +674,32 @@ def get_run_parameters_info(self): return run_parameters_data def get_number_of_lanes(self): - match_flowcell = re.match( - r".*LaneCount.*'(\d+)'.*SurfaceCount.*", self.flowcell_layout + if not self.flowcell_layout: + return "" + + if isinstance(self.flowcell_layout, dict): + return self.flowcell_layout.get( + wetlab.config.RUN_INFO_FLOWCELL_LAYOUT_LANE_TAG, "" + ) + + try: + flowcell_layout = ast.literal_eval(self.flowcell_layout) + except (ValueError, SyntaxError): + flowcell_layout = None + + if isinstance(flowcell_layout, dict): + return flowcell_layout.get( + wetlab.config.RUN_INFO_FLOWCELL_LAYOUT_LANE_TAG, "" + ) + + match_flowcell = re.search( + r"'%s':\s*'?(\\d+)'?" % wetlab.config.RUN_INFO_FLOWCELL_LAYOUT_LANE_TAG, + str(self.flowcell_layout), ) - return match_flowcell.group(1) + if match_flowcell: + return match_flowcell.group(1) + + return "" def get_number_of_reads(self): count = 0 @@ -759,7 +915,7 @@ class RawTopUnknowBarcodes(models.Model): runprocess_id = models.ForeignKey(RunProcess, on_delete=models.CASCADE) lane_number = models.CharField(max_length=4) top_number = models.CharField(max_length=4) - count = models.CharField(max_length=40) + count = models.IntegerField() sequence = models.CharField(max_length=40) generated_at = models.DateTimeField(auto_now_add=True) @@ -1275,123 +1431,6 @@ def update_confirm_used(self, confirmation): objects = LibPreparationUserSampleSheetManager() -class PoolStates(models.Model): - pool_state = models.CharField(max_length=50) - - class Meta: - db_table = "wetlab_pool_states" - - def __str__(self): - return "%s" % (self.pool_state) - - def get_pool_state(self): - return "%s" % (self.pool_state) - - -class LibraryPoolManager(models.Manager): - def create_lib_pool(self, pool_data): - platform_obj = core.models.SequencingPlatform.objects.filter( - platform_name__exact=pool_data["platform"] - ).last() - new_library_pool = self.create( - register_user=pool_data["registerUser"], - pool_state=PoolStates.objects.get(pool_state__exact="Defined"), - pool_name=pool_data["poolName"], - pool_code_id=pool_data["poolCodeID"], - adapter=pool_data["adapter"], - paired_end=pool_data["pairedEnd"], - sample_number=pool_data["n_samples"], - platform=platform_obj, - ) - return new_library_pool - - -class LibraryPool(models.Model): - register_user = models.ForeignKey(User, on_delete=models.CASCADE) - pool_state = models.ForeignKey(PoolStates, on_delete=models.CASCADE) - run_process_id = models.ForeignKey( - RunProcess, on_delete=models.CASCADE, null=True, blank=True - ) - - platform = models.ForeignKey( - core.models.SequencingPlatform, on_delete=models.CASCADE, null=True, blank=True - ) - pool_name = models.CharField(max_length=50) - sample_number = models.IntegerField(default=0) - pool_code_id = models.CharField(max_length=50, blank=True) - adapter = models.CharField(max_length=50, null=True, blank=True) - paired_end = models.CharField(max_length=10, null=True, blank=True) - generated_at = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ("pool_name",) - db_table = "wetlab_library_pool" - - def __str__(self): - return "%s" % (self.pool_name) - - def get_adapter(self): - return "%s" % (self.adapter) - - def get_id(self): - return "%s" % (self.pk) - - def get_info(self): - pool_info = [] - pool_info.append(self.pool_name) - pool_info.append(self.pool_code_id) - pool_info.append(self.sample_number) - return pool_info - - def get_number_of_samples(self): - return "%s" % (self.sample_number) - - def get_pool_name(self): - return "%s" % (self.pool_name) - - def get_pool_code_id(self): - return "%s" % (self.pool_code_id) - - def get_pool_single_paired(self): - return "%s" % (self.paired_end) - - def get_platform_name(self): - if self.platform is not None: - return "%s" % (self.platform.get_platform_name()) - else: - return "Not defined" - - def get_run_name(self): - if self.run_process_id is not None: - return "%s" % (self.run_process_id.get_run_name()) - else: - return "Not defined yet" - - def get_run_id(self): - if self.run_process_id is not None: - return "%s" % (self.run_process_id.get_run_id()) - return None - - def get_run_obj(self): - return self.run_process_id - - def set_pool_state(self, state): - self.pool_state = PoolStates.objects.get(pool_state__exact=state) - self.save() - - def update_number_samples(self, number_s_in_pool): - self.sample_number = number_s_in_pool - self.save() - return self - - def update_run_name(self, run_name): - self.run_process_id = run_name - self.save() - return self - - objects = LibraryPoolManager() - - class LibPrepareStates(models.Model): lib_prep_state = models.CharField(max_length=50) @@ -1476,7 +1515,7 @@ class LibPrepare(models.Model): unique_id = models.CharField(max_length=16, null=True, blank=True) user_in_samplesheet = models.CharField(max_length=255, null=True, blank=True) samplename_in_samplesheet = models.CharField(max_length=255, null=True, blank=True) - prefix_protocol = models.CharField(max_length=25, null=True, blank=True) + prefix_protocol = models.CharField(max_length=50, null=True, blank=True) class Meta: db_table = "wetlab_lib_prepare" diff --git a/wetlab/scripts/convert_rawtop_counter_to_int.py b/wetlab/scripts/convert_rawtop_counter_to_int.py new file mode 100644 index 000000000..54a80fa8d --- /dev/null +++ b/wetlab/scripts/convert_rawtop_counter_to_int.py @@ -0,0 +1,19 @@ +import wetlab.models + + +def run(): + """Upgrade: 3.0.0 -> 3.1.0 + The script implemted the issue #158 Unable to convert barcode count to + integer, to convert the counter that are in a string format, separated + by "," to int. + """ + top_bar_objs = wetlab.models.RawTopUnknowBarcodes.objects.all() + for top_bar_obj in top_bar_objs: + # Check if table has been updated to int + if isinstance(top_bar_obj.count, int) is False: + try: + top_bar_obj.count = int(top_bar_obj.count.replace(",", "")) + top_bar_obj.save() + except AttributeError: + continue + return diff --git a/wetlab/scripts/library_pool_to_many_relation.py b/wetlab/scripts/library_pool_to_many_relation.py new file mode 100644 index 000000000..925a08014 --- /dev/null +++ b/wetlab/scripts/library_pool_to_many_relation.py @@ -0,0 +1,49 @@ +import wetlab.models + + +def run(f_name): + """Upgrade: 3.0.0 -> 3.1.0 + This script is part of the issue "#180,when deleting run , pool and + library_preparations are also deleted" on Class LibraryPool. + This script expects a file created externally that contains the pk of the + LibraryPool instance and the pk of the run_process_id. + Export example: + mysql -u -p -h -D \ + -e "SELECT id, run_process_id_id FROM wetlab_library_pool" \ + > /tmp/library_pool_run_process.tsv + The script fetches the data from the file and adds the + run_process_pk to the run_process field of the LibraryPool instance + """ + if hasattr(wetlab.models.LibraryPool, "run_process"): + with open(f_name, "r") as fh: + lines = fh.readlines() + heading = lines[0].strip().split("\t") + if "run_process_id_id" in heading: + r_proc_idx = heading.index("run_process_id_id") + pk_idx = 0 + for line in lines[1:]: + l_data = line.strip().split("\t") + if l_data[r_proc_idx] == "NULL": + continue + library_pool_pk = l_data[pk_idx] + run_process_pk = l_data[r_proc_idx] + + try: + library_pool = wetlab.models.LibraryPool.objects.get( + id=library_pool_pk + ) + run_process = wetlab.models.RunProcess.objects.get( + id=run_process_pk + ) + except Exception as e: + print(f"Error: {e}") + continue + if not library_pool.run_process.filter(id=run_process_pk).exists(): + library_pool.run_process.add(run_process) + print( + f"LibraryPool: {library_pool_pk} added run_process: {run_process_pk}" + ) + else: + print("Data migration for LibraryPool was already done") + else: + print("LibraryPool model does not have run_process field") diff --git a/wetlab/scripts/rename_sample_sheet_folder.py b/wetlab/scripts/rename_sample_sheet_folder.py deleted file mode 100644 index 6f33d535a..000000000 --- a/wetlab/scripts/rename_sample_sheet_folder.py +++ /dev/null @@ -1,33 +0,0 @@ -import wetlab.models - - -""" - The script is applicable for the upgrade from 2.3.0 to 3.0.0. - Because the folder where the sample sheets were recorded have been - modified to sample_sheet. All the recorded run must update the field - of sample sheet. - Script reads RunProcess table and check if sample_sheet contains the - SampleSheet string and change it to sample_sheet. -""" - - -def run(): - # update SampleSheets - run_objs = wetlab.models.RunProcess.objects.filter( - sample_sheet__contains="SampleSheet" - ) - for run_obj in run_objs: - old_name = run_obj.get_sample_file() - new_name = old_name.replace("/SampleSheets/", "/sample_sheet/") - run_obj.set_run_sample_sheet(new_name) - # update SampleSheets4LibPrep - lib_user_objs = wetlab.models.LibUserSampleSheet.objects.filter( - sample_sheet__contains="SampleSheets4LibPrep" - ) - for lib_user_obj in lib_user_objs: - old_name = lib_user_obj.get_lib_user_sample_sheet() - new_name = old_name.replace( - "/SampleSheets4LibPrep/", "/sample_sheets_lib_prep/" - ) - lib_user_obj.set_lib_user_sample_sheet(new_name) - return diff --git a/wetlab/templates/wetlab/configuration_samba.html b/wetlab/templates/wetlab/configuration_samba.html index dcfcf1b81..0a2e4ee8d 100644 --- a/wetlab/templates/wetlab/configuration_samba.html +++ b/wetlab/templates/wetlab/configuration_samba.html @@ -97,7 +97,7 @@
Your Samba settings are OK
- +
diff --git a/wetlab/templates/wetlab/configuration_test.html b/wetlab/templates/wetlab/configuration_test.html index 41e793c6a..236ab6d87 100644 --- a/wetlab/templates/wetlab/configuration_test.html +++ b/wetlab/templates/wetlab/configuration_test.html @@ -263,7 +263,7 @@
You can not go further in the testing while the Basic test are not passed OK

Results execution for {{run_test_result.run_test_name}} test Run

- {% if run_test_result.Completed == "OK" %} + {% if run_test_result.completed == "OK" %}

Run test was successfully completed

{% else %} @@ -302,6 +302,8 @@
You can not go further in the testing while the Basic test are not passed OK

Result of run test for the state {{key}} {% if value == 'OK' %} + {% elif value == 'SKIP' %} + {% else %} {% endif %} diff --git a/wetlab/templates/wetlab/create_new_run.html b/wetlab/templates/wetlab/create_new_run.html index 3601d3081..74a3fefd8 100644 --- a/wetlab/templates/wetlab/create_new_run.html +++ b/wetlab/templates/wetlab/create_new_run.html @@ -1,116 +1,183 @@ {% extends "core/base.html" %} {% load static %} +{% load replace_spaces %} {% block content %} -{%include 'core/jexcel_functionality.html' %} -{% include "wetlab/menu.html" %} -

-
- {% include 'registration/login_inline.html' %} - {% if error_message %} -
-
+ {% include 'core/jexcel_functionality.html' %} + {% include "wetlab/menu.html" %} +
+
+ {% include 'registration/login_inline.html' %} + {% if error_message %} +
+
-

Unable to accept your request

+

Unable to process your request

-

{{error_message}}

+

{{error_message}}

-
-
- {% endif %} - {% if info_message %} -
-
-
-

Successful creation of the run

-
-

{{info_message}}

+
+
+ {% endif %} + {% if info_message %} +
+
+
+
+

Successful creation of the run

+
+
+

{{ info_message }}

+
-
- {% endif %} - {% if display_sample_information %} -
-
-
-
-

Create the new Run with pool {{display_sample_information.experiment_name}}

-
-
-
- {% csrf_token %} - - - - - - - - -
Confirm/ Update library preparation indexes to create the new Run {{display_sample_information.experiment_name}}
-
-
-
- - -
-
-
- {% if display_sample_information.instrument %} + {% endif %} + {% if display_sample_information %} +
+
+
+
+
+

Create the new Run with pool {{ display_sample_information.experiment_name }}

+
+
+
+ + {% csrf_token %} + + + + + + +
+ Confirm/ Update library preparation indexes to create the new Run {{ display_sample_information.experiment_name }} +
+
+
- - + +
- {% endif %} -
-
-
-
-
- -
-
-
-
- - +
+ {% if display_sample_information.instrument %} +
+ + +
+ {% endif %}
-
-
-
-
- - +
+
+
+ + +
+
+
+
+ + +
-
-
- - +
+
+
+ + +
- {% if display_sample_information.adapter2 %} +
- - + +
- {% endif %} + {% if display_sample_information.adapter2 %} +
+ + +
+ {% endif %} +
-
-
-
-
Samples that will be included in the run
-
-
-
-
-
- - - - -
-
+ $(document).ready(function () { + $("#storeDataNewRun").submit(function (e) { + //stop submitting the form to see the disabled button effect + // e.preventDefault(); + //disable the submit button + var table_data = $('#spreadsheet').jexcel('getData') + var data_json = JSON.stringify(table_data) + $("").attr("type", "hidden") + .attr("name", "s_sheet_data") + .attr("value", data_json) + .appendTo("#storeDataNewRun"); + $("#btnSubmit").attr("disabled", true); + return true; + }); + }); + + +
+
+
-
- {% elif created_new_run %} -
-
-
-
-

Successful creation of Run {{created_new_run.exp_name}}

-
-
Run name {{created_new_run.exp_name}} is Recorded State
-
-
-
-
-

Click on the link below to check the Run information

-

{{created_new_run.exp_name}}

+ {% elif created_new_run %} +
+
+
+
+
+

Successful creation of Run {{ created_new_run.exp_name }}

+
+
+
Run name {{ created_new_run.exp_name }} is Recorded State
+
+
+
+
+

Click on the link below to check the Run information

+

+ {{ created_new_run.exp_name }} +

+
-
-
-
-
-
Click on the link to download the Sample Sheet File
- Sample Sheet File +
+
+
+
Click on the link to download the Sample Sheet File
+ Sample Sheet File +
-
+
-
+
-
- - {% else %} - - {% if display_pools_for_run.invalid_run_data.heading %} -
-
-
-

Invalid pools

-
-

Some of the samples in the following pools are missing.

-
- - - - {% for value in display_pools_for_run.invalid_run_data.heading%} - - {% endfor %} - - - - {% for p_name, p_code, number, p_id in display_pools_for_run.invalid_run_data.data %} + {% else %} + {% if display_pools_for_run.invalid_run_data.heading %} +
+
+
+
+

Invalid pools

+
+
+

Some of the samples in the following pools are missing.

+
+
{{value}}
+ - - - + {% for value in display_pools_for_run.invalid_run_data.heading %}{% endfor %} - {% endfor %} - - -
{{p_name}}{{p_code}}{{number}}{{ value }}
+ + + {% for p_name, p_code, number, p_id in display_pools_for_run.invalid_run_data.data %} + + {{ p_name }} + {{ p_code }} + {{ number }} + + {% endfor %} + + +
- -
-
- {% endif %} - {% if display_pools_for_run.run_data.heading %} -
-
-
-
-

Run defined, but not completed

-
-
- {% csrf_token %} - - {% for r_name, p_values, r_id in display_pools_for_run.run_data.data %} -
-
-
-

Run name {{r_name }}

-
- - - - {% for value in display_pools_for_run.run_data.heading %} - - {% endfor %} - - - - - {% for p_name, p_code, p_number in p_values %} + + + {% endif %} + {% if display_pools_for_run.run_data.heading %} +
+
+
+
+
+

Run defined, but not completed

+
+
+ + {% csrf_token %} + + {% for r_name, p_values, r_id in display_pools_for_run.run_data.data %} +
+
+
+
+

Run name {{ r_name }}

+
+
+
{{value}} Select Run
+ - - - - + {% for value in display_pools_for_run.run_data.heading %}{% endfor %} + - {% endfor %} - -
{{p_name}}{{p_code}}{{p_number}}{{ value }}Select Run
+ + + {% for p_name, p_code, p_number in p_values %} + + {{ p_name }} + {{ p_code }} + {{ p_number }} + + + + + {% endfor %} + + +
-
- {% endfor %} - - + {% endfor %} + + +
-
- {% endif %} - {% load user_text %} - {% if display_pools_for_run.pool_data %} -
-
-

Fill the information to create the New Run

-
- -
- +
+
{% endblock %} diff --git a/wetlab/templates/wetlab/create_next_seq_run.html b/wetlab/templates/wetlab/create_next_seq_run.html index 03d73dd62..2d37c3915 100644 --- a/wetlab/templates/wetlab/create_next_seq_run.html +++ b/wetlab/templates/wetlab/create_next_seq_run.html @@ -1,218 +1,322 @@ {% extends "core/base.html" %} {% load static %} {% block content %} -{% include "wetlab/menu.html" %} -{% include 'registration/login_inline.html' %} - -
- {% if get_user_names %} -
-
-
-
-

Run creation progress

-
-

Fill the user name and the Used Library Kit for each project in the run {{ get_user_names.experiment_name }}

-

This is the second, and the last, FORM that user need to fill down to have the information about the run

-

Once the name of the user and the libray Kit used in the run, were filled and submited, the next page will show the successful run creation

+ {% include "wetlab/menu.html" %} + +
+
+ {% include 'registration/login_inline.html' %} + {% if error_message %} +
+
+
+
+

Unable to process your request

+
+
+ {% for message in error_message %}

{{ message }}

{% endfor %} +
-
-
    -
  1. Upload Sample Sheet
  2. Assign Library Kit
  3. Show Results
  4. -
+
+
+ {% endif %} + {% if get_user_names %} +
+
+
+
+
+

Run creation progress

+
+
+

+ Fill the user name and the Used Library Kit for each project in the run {{ get_user_names.experiment_name }} +

+

This is the second, and the last, FORM that user need to fill down to have the information about the run

+

+ Once the name of the user and the libray Kit used in the run, were filled and submited, the next page will show the successful run creation +

+
+
+
+
    +
  1. + Upload Sample Sheet +
  2. + +
  3. + Assign Library Kit +
  4. + +
  5. + Show Results +
  6. +
+
-
-
-
- -
-
-
-
-
Form to upload the Sample Sheet file. Run Parameters definition
-
-
- {% csrf_token %} - - -
- -
- -
- -
- -
- {% for key, values in get_user_names.projects_user %} -

Project name assigned to run :

-

{{ key }}

- -
- -
- -
- +
+ +
+
+
+
+
Form to upload the Sample Sheet file. Run Parameters definition
+
+ + {% csrf_token %} + + +
+
- +
- +
- +
- {% endfor %} -
- - - + {% for key, values in get_user_names.projects_user %} +

Project name assigned to run :

+

{{ key }}

+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ {% endfor %} +
+ + +
+
-
-
-
-
-
Notes for user validation
-
-

The following information have been collected from the Sample Sheet uploaded previously.

-

It contains the userid information. Correct in the user name field if this information need to be changed

-

Write down the Library kit used for each project

-

Submit button is disabled if any of the user names is not previoulsy defined in database

+
+
+
Notes for user validation
+
+

The following information have been collected from the Sample Sheet uploaded previously.

+

It contains the userid information. Correct in the user name field if this information need to be changed

+

Write down the Library kit used for each project

+

+ Submit button is disabled if any of the user names is not previoulsy defined in database +

+
-
-
-
-
-
- - + //Enable or disable button based on if user names are valid or not + $('input').filter('[id=users]').on('keyup',function() { + valid = checkInputs(); + }) + checkInputs() + - {% elif completed_form %} - {% for key, val in completed_form%} - {% if key == "runname" %} -
-
-
    -
  1. Upload Sample Sheet
  2. Assign Library Kit
  3. Show Results
  4. -
-
-
-
-
-
-
Run configuration have been sucessfully stored
-
-

All the required information is stored now on database.

-

For the run name : {{ val }}

-
+ {% elif completed_form %} + {% for key, val in completed_form %} + {% if key == "runname" %} +
+
+
    +
  1. + Upload Sample Sheet +
  2. + +
  3. + Assign Library Kit +
  4. + +
  5. + Show Results +
  6. +
-
- {% endif %} - {% endfor %} - {% else %} - -
-
-
    -
  1. Upload Sample Sheet
  2. Assign Library Kit
  3. Show Results
  4. -
-
-
-
-
-

This FORM will be used to generated the input file that BaseSpace requires to execute the run

-
-
-
-
-
-
-
Form to upload the Sample Sheet file
-
-
- {% csrf_token %} - -
- -
- +
+
+
+
+
Run configuration have been sucessfully stored
+
+
+

All the required information is stored now on database.

+

+ For the run name : {{ val }} +

-

Fields marked with * are mandatory

- - -
+
+
+ {% endif %} + {% endfor %} + {% else %} + +
+
+
    +
  1. + Upload Sample Sheet +
  2. + +
  3. + Assign Library Kit +
  4. + +
  5. + Show Results +
  6. +
-
-
-
Form to upload the Sample Sheet file
-
-

This Form is used to upload the Sample Sheet generated by Illumina Experience Manager tool.

-

Guide for Sample Sheet creation and download the IEM tool can be found at illumina Web page.

-

Click here for getting this information.

-
+
+
+

+ This FORM will be used to generated the input file that BaseSpace requires to execute the run +

+
-
-
- - - {% endif %} -
-{% endblock %} + +
+ {% endif %} +
+ {% endblock %} diff --git a/wetlab/templates/wetlab/create_pool.html b/wetlab/templates/wetlab/create_pool.html index 430837684..88307e752 100644 --- a/wetlab/templates/wetlab/create_pool.html +++ b/wetlab/templates/wetlab/create_pool.html @@ -1,5 +1,6 @@ {% extends "core/base.html" %} {% load static %} +{% load replace_spaces %} {% block content %} {% include "wetlab/menu.html" %} {% include 'core/jexcel_functionality.html' %} @@ -109,7 +110,7 @@
Samples included in pool
- {% for s_name, s_index in display_list.incompatible_index %} + {% for s_name, s_index in display_list.duplicated_index.incompatible_index %} {{s_name}} {{s_index}} @@ -157,57 +158,61 @@

The following Samples are ready to be included inside a

diff --git a/wetlab/templates/wetlab/create_protocol.html b/wetlab/templates/wetlab/create_protocol.html index 2afa95d7a..99d7d88be 100644 --- a/wetlab/templates/wetlab/create_protocol.html +++ b/wetlab/templates/wetlab/create_protocol.html @@ -93,7 +93,7 @@

{{ERROR}}

-

Protocols already defined for Molecules

+

Protocols already defined

{% if defined_protocols %} diff --git a/wetlab/templates/wetlab/define_protocol_parameters.html b/wetlab/templates/wetlab/define_protocol_parameters.html index 7f8a36b94..4fe3004de 100644 --- a/wetlab/templates/wetlab/define_protocol_parameters.html +++ b/wetlab/templates/wetlab/define_protocol_parameters.html @@ -9,7 +9,7 @@
-

Invalid definition

+

Invalid definition

{{error_message}}

@@ -56,10 +56,11 @@

{{error_message}}

Define the parameters used for protocol {{prot_parameters.protocol_name}}

-
- - - +
+
+
+ +
- +
@@ -313,7 +315,7 @@
If some information is wrong then modify the sample sheet and upload again t

Click on the bottom below to continue with adding Library Preparation information

- +
@@ -415,7 +417,7 @@
If some information is wrong then modify the sample sheet and upload again t

Click on the bottom below to continue with adding Library Preparation information

- +
diff --git a/wetlab/templates/wetlab/handling_molecules.html b/wetlab/templates/wetlab/manage_molecules.html similarity index 90% rename from wetlab/templates/wetlab/handling_molecules.html rename to wetlab/templates/wetlab/manage_molecules.html index 37d6edf76..97a80b05d 100644 --- a/wetlab/templates/wetlab/handling_molecules.html +++ b/wetlab/templates/wetlab/manage_molecules.html @@ -1,5 +1,6 @@ {% extends "core/base.html" %} {% load static %} +{% load replace_spaces %} {% block content %} {% include "wetlab/menu.html" %} {% include 'core/jexcel_functionality.html' %} @@ -36,12 +37,12 @@

Assign the Molecule Protocol to following samples.

+ name="updateExtractionProtocol" + id="updateExtractionProtocol"> {% csrf_token %} - +
Information missing

Please add the missing information and click on the submit bottom

{% csrf_token %} - + @@ -108,15 +109,15 @@

Information missing

-

Update molecules data with defined parameters

+

Update sample extraction data with defined parameters

+ name="addExtractionParameters" + id="addExtractionParameters"> {% csrf_token %} - + {% for prot, value_dict in molecule_parameters.items %} Update molecules data with defined parameters
Protocol: {{ prot }}
-
+
{% endfor %} @@ -147,14 +148,14 @@
Protocol: {{ prot }}
-

Molecules have been updated with required parameters.

+

Samples have been updated with Extraction required parameters.

+ value="Return to manage extraction samples" + onclick="location.href ='/wetlab/manageMolecules' ;" />
@@ -189,7 +190,7 @@

Molecules are updated with their use.

+ onclick="location.href ='/wetlab/manageLibraryPreparation' ;" />
@@ -208,7 +209,7 @@

Molecules are updated with their use.

type="button" role="tab" aria-controls="n_samples" - aria-selected="true">New Samples + aria-selected="true">Recorded Samples
@@ -286,19 +287,19 @@

There is no samples to add Molecule Extraction information

-

Select the Samples to add Molecule information

+

Select the Samples to add Pending extraction information

{% if molecules_availables %}
{% csrf_token %}
-
+
Select the use that will be given for the sample {% if molecule_use_defined %} {% if pending_to_use.heading %} @@ -412,11 +413,13 @@

Not molecule uses have been defined yet

return true; }); }); - // excel for pending molecules + {% endif %} + {% if molecules_availables.molecule_heading %} + // excel for pending Extraction data var data2 = [{% for values in molecules_availables.data %} [{% for value in values %}'{{value}}',{% endfor %}],{% endfor %} ]; - var table2 = jexcel(document.getElementById('pending_molecules'), { + var table2 = jexcel(document.getElementById('pending_extraction'), { data:data2, columns: [{% for values in molecules_availables.molecule_heading %} {% if forloop.last %} @@ -437,21 +440,37 @@

Not molecule uses have been defined yet

pagination:20, csvFileName:'molecule_use', }); - + // Function to check if at least one checkbox in the "select sample" column is set to true + function isAnySelectedColumnChecked() { + var table_data2 = table2.getData(); + // Assuming the "Select sample" column is the third column (index 5) + for (var i = 0; i < table_data2.length; i++) { + if (table_data2[i][5] === true) { // Check if "Select sample" column (index 5) is true + return true; + } + } + return false; + } // send form for molecule in use $(document).ready(function () { $("#selectedOwnerMolecules").submit(function (e) { + if (!isAnySelectedColumnChecked()) { + e.preventDefault(); // Prevent form submission + alert('You must select at least one sample before submitting.'); + return false; + } var table_data2 = table2.getData() var data_json = JSON.stringify(table_data2) $("").attr("type", "hidden") - .attr("name", "pending_molecules") + .attr("name", "pending_extraction") .attr("value", data_json) .appendTo("#selectedOwnerMolecules"); $("#btnSubmit").attr("disabled", true); return true; }); }); - + {% endif %} + {% if pending_to_use.heading %} // excel for selecting the molecule use var data3 = [{% for values in pending_to_use.data %} [{% for value in values %}'{{value}}',{% endfor %}],{% endfor %} @@ -484,7 +503,7 @@

Not molecule uses have been defined yet

var table_data3 = table3.getData() var data_json = JSON.stringify(table_data3) $("").attr("type", "hidden") - .attr("name", "molecule_used_for") + .attr("name", "sample_continues_on") .attr("value", data_json) .appendTo("#requestMoleculeUse"); $("#btnSubmit").attr("disabled", true); @@ -541,7 +560,7 @@

Not molecule uses have been defined yet

}); $(document).ready(function () { - $("#updateMoleculeProtocol").submit(function (e) { + $("#updateExtractionProtocol").submit(function (e) { //stop submitting the form to see the disabled button effect // e.preventDefault(); //disable the submit button @@ -550,7 +569,7 @@

Not molecule uses have been defined yet

$("").attr("type", "hidden") .attr("name", "molecule_data") .attr("value", data_json) - .appendTo("#updateMoleculeProtocol"); + .appendTo("#updateExtractionProtocol"); $("#btnSubmit").attr("disabled", true); return true; }); @@ -604,13 +623,13 @@

Not molecule uses have been defined yet

} }); $(document).ready(function () { - $("#updateMoleculeProtocol").submit(function (e) { + $("#updateExtractionProtocol").submit(function (e) { var table_data = $('#missing_data').jexcel('getData') var data_json = JSON.stringify(table_data) $("").attr("type", "hidden") .attr("name", "molecule_data") .attr("value", data_json) - .appendTo("#updateMoleculeProtocol"); + .appendTo("#updateExtractionProtocol"); $("#btnSubmit").attr("disabled", true); return true; }); @@ -619,12 +638,12 @@

Not molecule uses have been defined yet

{% if molecule_parameters %} $(document).ready(function () { {% for prot , value_dict in molecule_parameters.items %} - var {{ prot }}_data = [{% for values in value_dict.m_data %} + var {{ prot|replace_spaces_with_underscores }}_data = [{% for values in value_dict.m_data %} [{% for value in values %}'{{value}}',{% endfor %}],{% endfor %} ]; - var {{ prot }}_data_table = jspreadsheet(document.getElementById('spreadsheet_{{ prot }}'),{ - data:{{ prot }}_data, + var {{ prot|replace_spaces_with_underscores }}_data_table = jspreadsheet(document.getElementById('spreadsheet_{{ prot|replace_spaces_with_underscores }}'),{ + data:{{ prot|replace_spaces_with_underscores }}_data, columns: [ { type: 'hidden' }, { type: 'text', title:'{{value_dict.fix_heading.0}}', width:180 , readOnly:true }, @@ -651,13 +670,13 @@

Not molecule uses have been defined yet

}); - $("#addMoleculeParameters").submit(function (e) { - var table_data = {{ prot }}_data_table.getData() + $("#addExtractionParameters").submit(function (e) { + var table_data = {{ prot|replace_spaces_with_underscores }}_data_table.getData() var data_json = JSON.stringify(table_data) $("").attr("type", "hidden") .attr("name", "{{ prot }}") .attr("value", data_json) - .appendTo("#addMoleculeParameters"); + .appendTo("#addExtractionParameters"); $("#button-submit").attr("disabled", true); return true; }); diff --git a/wetlab/templates/wetlab/menu.html b/wetlab/templates/wetlab/menu.html index 9fb49ad32..931d6da7a 100644 --- a/wetlab/templates/wetlab/menu.html +++ b/wetlab/templates/wetlab/menu.html @@ -1,107 +1,233 @@ {% load static %} {% load user_groups %}
-