diff --git a/Dockerfile b/Dockerfile index a61264b..7b01a89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,67 +1,55 @@ -FROM python:3.9.7-bullseye as base - -ENV PYTHONUNBUFFERED 1 - -COPY requirements.txt /tmp/requirements.txt - -RUN python -m pip install -U pip setuptools - -RUN pip install --no-cache-dir -r /tmp/requirements.txt - -FROM base as dev - -COPY requirements.dev.txt /tmp/requirements.dev.txt -COPY requirements.txt /tmp/requirements.txt - -RUN pip install --no-cache-dir -r /tmp/requirements.dev.txt - -RUN sed -i "s/'_headers'/'headers'/" /usr/local/lib/python3.9/site-packages/revproxy/utils.py -RUN sed -i "s/'_headers'/'headers'/" /usr/local/lib/python3.9/site-packages/revproxy/response.py - -WORKDIR /primerdriver - -ENTRYPOINT python manage.py migrate \ - && SHORT_SHA=$(git show --format="%h" --no-patch) gunicorn primerx.wsgi -b 0.0.0.0:$PORT --log-file - --reload - -FROM base as make-linux - -RUN apt-get update -RUN apt-get install upx-ucl libgfortran-10-dev libquadmath0 -y - -ENV PYTHONDONTWRITEBYTECODE 1 - -COPY requirements.dev.txt /tmp/requirements.dev.txt -COPY requirements.txt /tmp/requirements.txt - -RUN pip install --no-cache-dir -r /tmp/requirements.dev.txt - -WORKDIR /primerdriver - -ENTRYPOINT [ "sh", "build.sh" ] - -FROM node:16-alpine as build - -WORKDIR /web - -COPY ./web/app/ ./ - -RUN yarn install --prod - -RUN yarn build - -FROM base as prod - -WORKDIR /primerdriver - -COPY ./primerdriver/ ./primerdriver/ -COPY ./primerx/ ./primerx/ -COPY ./sdm/ ./sdm/ -COPY ./manage.py ./manage.py -COPY ./runserver.sh ./runserver.sh -COPY --from=build /web/build ./web/app/ - -ARG SHORT_SHA=$SHORT_SHA - -ENV SHORT_SHA $SHORT_SHA - -ENTRYPOINT [ "sh", "runserver.sh" ] +FROM python:3.9.7-bullseye as base + +ENV PYTHONUNBUFFERED 1 + +COPY requirements.txt /tmp/requirements.txt + +RUN python -m pip install -U pip setuptools + +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +FROM base as dev + +COPY requirements.dev.txt /tmp/requirements.dev.txt +COPY requirements.txt /tmp/requirements.txt + +RUN pip install --no-cache-dir -r /tmp/requirements.dev.txt + +WORKDIR /primerdriver + +ENTRYPOINT python manage.py migrate && \ + SHORT_SHA=$(git show --format="%h" --no-patch) gunicorn primerx.wsgi \ + -b 0.0.0.0:5000 \ + --workers 2 \ + --threads 4 \ + --log-file - \ + --capture-output \ + --reload + +FROM node:16-alpine as build + +WORKDIR /web + +COPY ./web/app/ ./ + +RUN yarn install --prod + +RUN yarn build + +FROM base as prod + +WORKDIR /primerdriver + +COPY ./primerdriver/ ./primerdriver/ +COPY ./primerx/ ./primerx/ +COPY ./sdm/ ./sdm/ +COPY ./manage.py ./manage.py +COPY --from=build /web/build ./web/app/ + +ARG SHORT_SHA=$SHORT_SHA + +ENV SHORT_SHA $SHORT_SHA + +ENTRYPOINT python manage.py collectstatic --noinput && \ + python manage.py migrate && \ + gunicorn primerx.wsgi -b 0.0.0.0:$PORT --workers 1 --threads 2 --log-file - diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..80f75b0 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,18 @@ +FROM python:3.9.7-bullseye + +RUN apt-get update +RUN apt-get install upx-ucl libgfortran-10-dev libquadmath0 -y + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + +COPY requirements.dev.txt /tmp/requirements.dev.txt +COPY requirements.txt /tmp/requirements.txt + +RUN pip install --no-cache-dir -r /tmp/requirements.dev.txt + +RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /bin + +WORKDIR /primerdriver + +ENTRYPOINT [ "/bin/task", "build" ] diff --git a/README.md b/README.md index b156162..2bfad70 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,103 @@ -# PrimerDriver: Automated design of mutagenic PCR primers -![PrimerDriver](https://res.cloudinary.com/kdphotography-assets/image/upload/v1587460290/primerdriver/PrimerDriver_logo.png) - -![GitHub](https://img.shields.io/github/license/kvdomingo/primerdriver) -![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/kvdomingo/primerdriver?include_prereleases) -![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django) - -## CLI - -You can access and download the CLI from the -[releases page](https://github.com/kvdomingo/primerdriver/releases). -Run the program in a terminal using -```shell -primerdriver -h -``` - -This will run the help program. For first-time users, the program can be run in -interactive mode by passing the `-i` flag: -```shell -primerdriver -i -``` - -This will walk you through each option step-by-step. -Batch design can be performed by including -[`primerdriver`](primerdriver/__main__.py) as part of a shell script. - -## Web application -For a more interactive experience, visit the -[web application](https://primerdriver.kvdstudio.app). - -## Documentation -The documentation is available at https://kvdomingo.github.io/primerdriver/. - -## Contributing -Open a PR or raise an -[issue](https://github.com/kvdomingo/primerdriver/issues). -You may also email Nomer or Kenneth, depending on the nature of the issue. - -## Developing locally - -### Prerequisites -- Python 3.9 or above -- Node.js LTS 14 or above -- Docker - -### Installing -A step by step series of examples that tell you how to get a -development environment running - -1. Clone and extract the repo. -2. Create a virtual environment and install backend dependencies: -```shell -pip install -r requirements.dev.txt -``` - -### Running local server - -Setup the Docker containers: -```shell -docker compose up --build -``` - -Wait a few minutes for all the containers to start, then access the -local server in your browser at http://localhost:8000. - -### Building from source -Run the script: -```shell -./build.sh -``` - -### Deployment -```shell -git add . -git commit -m "DESCRIPTIVE_COMMIT_MESSAGE" -git push origin your_feature_branch -``` - -where `your_feature_branch` should summarize the changes you are implementing -(e.g., `feature/implementing-xxxx-feature`, `bugfix/crush-critical-yyyy-bug`). - - -## Authors -- **Numeriano Amer "Nomer" E. Gutierrez** - Project Lead, Molecular Biologist - [Email](mailto:ngutierrez@evc.pshs.edu.ph) | [GitHub](https://github.com/nomgutierrez) -- **Kenneth V. Domingo** - Lead Developer, Technical Consultant - [Email](mailto:kvdomingo@up.edu.ph) | [Website](https://kvdomingo.xyz) | [GitHub](https://github.com/kvdomingo) -- **Shebna Rose D. Fabilloren** - Technical Consultant - [Email](mailto:sdfabilloren@up.edu.ph) -- **Carlo M. Lapid** - Project Adviser - [Email](mailto:cmlapid@up.edu.ph) - -## Versioning -This project complies with [SemVer](https://semver.org) for versioning. For -all available versions, see -[tags](https://github.com/kvdomingo/primerdriver/tags). - -## License -This project is licensed under the [GPLv3 License](./LICENSE). +# PrimerDriver: Automated design of mutagenic PCR primers +![PrimerDriver](https://res.cloudinary.com/kdphotography-assets/image/upload/v1587460290/primerdriver/PrimerDriver_logo.png) + +![GitHub](https://img.shields.io/github/license/kvdomingo/primerdriver) +![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/kvdomingo/primerdriver?include_prereleases) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django) + +## CLI + +You can access and download the CLI from the +[releases page](https://github.com/kvdomingo/primerdriver/releases). +Run the program in a terminal using +```shell +primerdriver -h +``` + +This will run the help program. For first-time users, the program can be run in +interactive mode by passing the `-i` flag: +```shell +primerdriver -i +``` + +This will walk you through each option step-by-step. +Batch design can be performed by including +[`primerdriver`](primerdriver/__main__.py) as part of a shell script. + +## Web application +For a more interactive experience, visit the +[web application](https://primerdriver.kvdstudio.app). + +## Documentation +The documentation is available at https://kvdomingo.github.io/primerdriver/. + +## Contributing +Open a PR or raise an +[issue](https://github.com/kvdomingo/primerdriver/issues). +You may also email Nomer or Kenneth, depending on the nature of the issue. + +## Developing locally + +### Prerequisites +- [Python](https://www.python.org/downloads/) 3.9 or above +- [Node.js](https://nodejs.org/en/download/) LTS 14 or above +- [Docker](https://www.docker.com/get-started) +- [Task](https://taskfile.dev/#/installation) + +### Installing +A step by step series of examples that tell you how to get a +development environment running + +1. Clone and extract the repo. +2. Create a virtual environment and install backend dependencies: +```shell +pip install -r requirements.dev.txt +``` + +### Running local server + +Setup the Docker containers: +```shell +task + +# To see log stream +task logs +``` + +Wait a few minutes for all the containers to start, then access the +local server in your browser at http://localhost:8000. + +### Building from source +Run the script: +```shell +# Build for Windows on a Windows machine / for Linux on a Linux machine +task build + +# Build for Linux on a Windows machine with Docker +task build-linux +``` + +### Deployment +```shell +git add . +git commit -m "DESCRIPTIVE_COMMIT_MESSAGE" +git push origin your_feature_branch +``` + +where `your_feature_branch` should summarize the changes you are implementing +(e.g., `feature/implementing-xxxx-feature`, `bugfix/crush-critical-yyyy-bug`). + + +## Authors +- **Numeriano Amer "Nomer" E. Gutierrez** - Project Lead, Molecular Biologist - [Email](mailto:ngutierrez@evc.pshs.edu.ph) | [GitHub](https://github.com/nomgutierrez) +- **Kenneth V. Domingo** - Lead Developer, Technical Consultant - [Email](mailto:kvdomingo@up.edu.ph) | [Website](https://kvdomingo.xyz) | [GitHub](https://github.com/kvdomingo) +- **Shebna Rose D. Fabilloren** - Technical Consultant - [Email](mailto:sdfabilloren@up.edu.ph) +- **Carlo M. Lapid** - Project Adviser - [Email](mailto:cmlapid@up.edu.ph) + +## Versioning +This project complies with [SemVer](https://semver.org) for versioning. For +all available versions, see +[tags](https://github.com/kvdomingo/primerdriver/tags). + +## License +This project is licensed under the [GPLv3 License](./LICENSE). diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..2c2fabb --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,42 @@ +version: '3' + +tasks: + default: + cmds: + - docker compose up -d --build --remove-orphans + + logs: + cmds: + - docker compose logs --follow + + make-builder: + cmds: + - docker build -t kvdomingo/primerdriver-makelinux:latest -f Dockerfile.build . + + publish-builder: + cmds: + - docker push kvdomingo/primerdriver-makelinux:latest + + build-linux: + cmds: + - docker run -v "$(pwd)":/primerdriver --rm --name primerdriver-makelinux kvdomingo/primerdriver-makelinux:latest + + build: + cmds: + - pyinstaller --clean -F --name primerdriver setup.py + + shutdown: + cmds: + - docker compose stop + + restart: + cmds: + - docker compose restart + + restart-proxy: + cmds: + - docker compose restart proxy + + clean: + cmds: + - docker compose down -v --remove-orphans diff --git a/build.sh b/build.sh deleted file mode 100644 index 1acfeef..0000000 --- a/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -pyinstaller --clean -F --name primerdriver build.py diff --git a/docker-compose.yml b/docker-compose.yml index 16aec91..dfc91aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,34 @@ -version: "3.8" - -services: - apiserver: - build: - context: . - target: dev - image: kvdomingo/primerdriver-api:latest - env_file: .env - ports: - - ${PORT}:${PORT} - volumes: - - .:/primerdriver - - frontend: - build: - context: ./web/app - target: dev - image: kvdomingo/primerdriver-app:latest - env_file: ./web/app/.env - volumes: - - ./web:/web - - make-linux: - build: - context: . - target: make-linux - image: kvdomingo/primerdriver-makelinux:latest - volumes: - - .:/primerdriver +version: "3.8" + +services: + apiserver: + build: + context: . + target: dev + image: kvdomingo/primerdriver-api:latest + env_file: .env + volumes: + - .:/primerdriver + + frontend: + build: + context: ./web/app + target: dev + image: kvdomingo/primerdriver-app:latest + env_file: ./web/app/.env + volumes: + - ./web:/web + + proxy: + image: nginx:latest + ports: + - 8000:8000 + command: + - nginx + - "-g" + - daemon off; + volumes: + - ./proxy/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - apiserver + restart: unless-stopped diff --git a/primerx/settings.py b/primerx/settings.py index 28b9ce3..3f14df3 100644 --- a/primerx/settings.py +++ b/primerx/settings.py @@ -1,153 +1,150 @@ -import os -import jinja2 -import urllib -import dj_database_url -from django.core.management.utils import get_random_secret_key -from pathlib import Path -from dotenv import load_dotenv - -load_dotenv() - -BASE_DIR = Path(__file__).resolve().parent.parent - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get("SECRET_KEY", default=get_random_secret_key()) - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(os.environ.get("DEBUG", False)) - -DEBUG_PROPAGATE_EXCEPTIONS = DEBUG - -PYTHON_ENV = os.environ.get("PYTHON_ENV", "production") - -ALLOWED_HOSTS = [".kvdstudio.app"] - -if PYTHON_ENV == "development": - ALLOWED_HOSTS = ["*"] - -# Application definition - -INSTALLED_APPS = [ - "sdm.apps.SdmConfig", - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "corsheaders", - "rest_framework", -] - -if PYTHON_ENV == "development": - INSTALLED_APPS.append("revproxy") - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "corsheaders.middleware.CorsMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -CORS_ORIGIN_ALLOW_ALL = True - -# CORS_ORIGIN_WHITELIST = [ -# 'https://primerdriver.vercel.app', -# ] - -ROOT_URLCONF = "primerx.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.jinja2.Jinja2", - "DIRS": [ - BASE_DIR / "jinjatemplates", - BASE_DIR / "web" / "app", - ], - "APP_DIRS": True, - "OPTIONS": { - "environment": "primerx.jinja2.environment", - "autoescape": False, - "undefined": jinja2.DebugUndefined if DEBUG else jinja2.Undefined, - }, - }, - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": ["sdm.admin"], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "primerx.wsgi.application" - -# Database -# https://docs.djangoproject.com/en/3.0/ref/settings/#databases - -if PYTHON_ENV == "development": - DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } - } -else: - _database_url = os.environ.get("DATABASE_URL") - _database_config = dj_database_url.parse(_database_url) - _database_config["HOST"] = urllib.parse.unquote(_database_config["HOST"]) - DATABASES = {"default": _database_config} - -# Password validation -# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/3.0/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "Asia/Manila" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.0/howto/static-files/ - - -STATIC_URL = "/static/" - -STATIC_ROOT = BASE_DIR / "static" - -STATICFILES_DIRS = [BASE_DIR / "web" / "app" / "static"] - -STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" +import os +import jinja2 +import urllib +import dj_database_url +from django.core.management.utils import get_random_secret_key +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get("SECRET_KEY", default=get_random_secret_key()) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = bool(os.environ.get("DEBUG", False)) + +DEBUG_PROPAGATE_EXCEPTIONS = DEBUG + +PYTHON_ENV = os.environ.get("PYTHON_ENV", "production") + +ALLOWED_HOSTS = [".kvdstudio.app"] + +if PYTHON_ENV == "development": + ALLOWED_HOSTS = ["*"] + +# Application definition + +INSTALLED_APPS = [ + "sdm.apps.SdmConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "corsheaders", + "rest_framework", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "corsheaders.middleware.CorsMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +CORS_ORIGIN_ALLOW_ALL = True + +# CORS_ORIGIN_WHITELIST = [ +# 'https://primerdriver.vercel.app', +# ] + +ROOT_URLCONF = "primerx.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.jinja2.Jinja2", + "DIRS": [ + BASE_DIR / "jinjatemplates", + BASE_DIR / "web" / "app", + ], + "APP_DIRS": True, + "OPTIONS": { + "environment": "primerx.jinja2.environment", + "autoescape": False, + "undefined": jinja2.DebugUndefined if DEBUG else jinja2.Undefined, + }, + }, + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["sdm.admin"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "primerx.wsgi.application" + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +if PYTHON_ENV == "development": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } +else: + _database_url = os.environ.get("DATABASE_URL") + _database_config = dj_database_url.parse(_database_url) + _database_config["HOST"] = urllib.parse.unquote(_database_config["HOST"]) + DATABASES = {"default": _database_config} + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "Asia/Manila" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + + +STATIC_URL = "/static/" + +STATIC_ROOT = BASE_DIR / "static" + +STATICFILES_DIRS = [BASE_DIR / "web" / "app" / "static"] + +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" diff --git a/primerx/urls.py b/primerx/urls.py index f453216..54e681e 100644 --- a/primerx/urls.py +++ b/primerx/urls.py @@ -1,16 +1,12 @@ -from django.contrib import admin -from django.conf import settings -from django.urls import path, re_path, include -from django.shortcuts import render - -urlpatterns = [ - path("admin/", admin.site.urls), - path("api/", include("sdm.urls")), -] - -if settings.PYTHON_ENV == "development": - from revproxy.views import ProxyView - - urlpatterns.append(re_path(r"^(?P.*)$", ProxyView.as_view(upstream="http://frontend:3000"))) -else: - urlpatterns.append(re_path(r"^.*/?$", lambda req: render(req, "index.html"))) +from django.contrib import admin +from django.conf import settings +from django.urls import path, re_path, include +from django.shortcuts import render + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/", include("sdm.urls")), +] + +if settings.PYTHON_ENV == "production": + urlpatterns.append(re_path(r"^.*/?$", lambda req: render(req, "index.html"))) diff --git a/proxy/nginx.conf b/proxy/nginx.conf new file mode 100644 index 0000000..82afebf --- /dev/null +++ b/proxy/nginx.conf @@ -0,0 +1,65 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + log_format main '[$time_local] $request $status ${request_time}s ${body_bytes_sent}B'; + access_log /dev/stdout main; + error_log /dev/stderr info; + sendfile on; + keepalive_timeout 65; + + upstream apiserver { + least_conn; + server apiserver:5000 max_fails=3 fail_timeout=30s; + } + + upstream frontend { + least_conn; + server frontend:3000 max_fails=3 fail_timeout=30s; + } + + server { + listen 8000; + listen [::]:8000; + server_name localhost; + + location /api/ { + proxy_pass http://apiserver; + proxy_set_header Host $host; + proxy_set_header Access-Control-Allow-Origin *; + proxy_cache_bypass $http_upgrade; + } + + location /admin/ { + proxy_pass http://apiserver; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + location ~ /static/(admin|rest_framework)/ { + proxy_pass http://apiserver; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + break; + } + } + + include /etc/nginx/conf.d/*.conf; +} diff --git a/requirements.dev.txt b/requirements.dev.txt index b183c4e..94c9c85 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,11 +1,10 @@ -black -django-revproxy -pyinstaller -Sphinx==3.5.1 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==1.0.3 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.4 --r requirements.txt +black +pyinstaller +Sphinx==3.5.1 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.4 +-r requirements.txt diff --git a/runserver.sh b/runserver.sh deleted file mode 100644 index a3a19b3..0000000 --- a/runserver.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -python manage.py collectstatic --noinput -python manage.py migrate -gunicorn primerx.wsgi -b 0.0.0.0:$PORT --log-file - diff --git a/build.py b/setup.py similarity index 94% rename from build.py rename to setup.py index 95ff09b..53a93e7 100644 --- a/build.py +++ b/setup.py @@ -1,5 +1,5 @@ -from primerdriver.__main__ import main - - -if __name__ == "__main__": - main() +from primerdriver.__main__ import main + + +if __name__ == "__main__": + main() diff --git a/web/app/src/components/Station.js b/web/app/src/components/Station.js index 18d36e8..9060d70 100644 --- a/web/app/src/components/Station.js +++ b/web/app/src/components/Station.js @@ -7,6 +7,7 @@ import Characterize from "./menu/Characterize"; import Dna from "./menu/DnaView"; import Protein from "./menu/ProteinView"; import Results from "./menu/Result"; +import ErrorBoundary from "../utils/ErrorBoundary"; const styles = { appContainer: { @@ -40,7 +41,14 @@ function Station() { } /> } /> } /> - + ( + + + + )} + /> diff --git a/web/app/src/components/menu/Characterize.js b/web/app/src/components/menu/Characterize.js index 9cd9446..71f90cb 100644 --- a/web/app/src/components/menu/Characterize.js +++ b/web/app/src/components/menu/Characterize.js @@ -52,6 +52,10 @@ function Characterize(props) { data = "Request failed. Please try again later."; }) .finally(() => { + PDDispatch({ + type: "updateLoadedResults", + payload: true, + }); PDDispatch({ type: "updateResults", payload: { diff --git a/web/app/src/components/menu/DnaView.js b/web/app/src/components/menu/DnaView.js index cca2a20..4bc878c 100644 --- a/web/app/src/components/menu/DnaView.js +++ b/web/app/src/components/menu/DnaView.js @@ -76,6 +76,10 @@ function DnaView(props) { data = "Request failed. Please try again later."; }) .finally(() => { + PDDispatch({ + type: "updateLoadedResults", + payload: true, + }); PDDispatch({ type: "updateResults", payload: { diff --git a/web/app/src/components/menu/ProteinView.js b/web/app/src/components/menu/ProteinView.js index 164362d..4092769 100644 --- a/web/app/src/components/menu/ProteinView.js +++ b/web/app/src/components/menu/ProteinView.js @@ -98,12 +98,18 @@ function ProteinView(props) { let data; api.data .primerDriver(formData) - .then(res => (data = res.data)) + .then(res => { + data = res.data; + }) .catch(err => { console.log(err.message); data = "Request failed. Please try again later."; }) .finally(() => { + PDDispatch({ + type: "updateLoadedResults", + payload: true, + }); PDDispatch({ type: "updateResults", payload: { diff --git a/web/app/src/components/menu/Result.js b/web/app/src/components/menu/Result.js index 09c6a23..90530dc 100644 --- a/web/app/src/components/menu/Result.js +++ b/web/app/src/components/menu/Result.js @@ -1,5 +1,5 @@ import { Fragment, useEffect, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, Redirect } from "react-router-dom"; import { MDBTable as Table, MDBTableHead as TableHead, @@ -22,7 +22,16 @@ function Result() { const [mode, setMode] = useState(""); const [results, setResults] = useState({}); const [viewLimit, setViewLimit] = useState(10); - const { PDState } = usePrimerDriverContext(); + const { PDState, PDDispatch } = usePrimerDriverContext(); + + useEffect(() => { + return () => { + PDDispatch({ + type: "updateLoadedResults", + payload: false, + }); + }; + }, []); useEffect(() => { if (PDState.results.loaded) { @@ -54,87 +63,91 @@ function Result() { ); } - if (typeof results === "object") { - if (mode === "CHAR") - return ( -
-
- - - {Object.keys(results).map((key, i) => ( - - - - - ))} - -
{key}{results[key]["1"]}
-
- ); - else { - if (Object.keys(results).length >= 1) { - return ( -
-
- - {Object.keys(results).length} result{Object.keys(results).length !== 1 && "s"} - - {Object.keys(results).map( - (item, i) => - i < viewLimit && ( - - - - - - - - - {Object.keys(results[(i + 1).toString()]).map((key, j) => ( - - - - - ))} - -
- {`Primer ${i + 1}`}
{key}{results[(i + 1).toString()][key]}
- {i + 1 === viewLimit && i + 1 < Object.keys(results).length && ( -
- -
- )} -
- ), - )} -
- ); - } else { + if (!PDState.loadedResultsFromModule) { + return ; + } else { + if (typeof results === "object") { + if (mode === "CHAR") return (
- - {results.data} - + + + {Object.keys(results).map((key, i) => ( + + + + + ))} + +
{key}{results[key]["1"]}
); + else { + if (Object.keys(results).length >= 1) { + return ( +
+
+ + {Object.keys(results).length} result{Object.keys(results).length !== 1 && "s"} + + {Object.keys(results).map( + (item, i) => + i < viewLimit && ( + + + + + + + + + {Object.keys(results[(i + 1).toString()]).map((key, j) => ( + + + + + ))} + +
+ {`Primer ${i + 1}`}
{key}{results[(i + 1).toString()][key]}
+ {i + 1 === viewLimit && i + 1 < Object.keys(results).length && ( +
+ +
+ )} +
+ ), + )} +
+ ); + } else { + return ( +
+
+ + {results.data} + +
+ ); + } } + } else { + return ( +
+
+

+ Oops! Something went wrong on the server. Please try again later, or{" "} + + report an issue + + . +

+
+ ); } - } else { - return ( -
-
-

- Oops! Something went wrong on the server. Please try again later, or{" "} - - report an issue - - . -

-
- ); } } diff --git a/web/app/src/contexts/PrimerDriverContext.js b/web/app/src/contexts/PrimerDriverContext.js index 0d2f870..bc359d2 100644 --- a/web/app/src/contexts/PrimerDriverContext.js +++ b/web/app/src/contexts/PrimerDriverContext.js @@ -3,6 +3,7 @@ import { createContext, useReducer, useContext } from "react"; let PrimerDriverContext = createContext(); let initialState = { + loadedResultsFromModule: false, results: { loaded: false, data: [], @@ -12,6 +13,12 @@ let initialState = { let reducer = (state, action) => { switch (action.type) { + case "updateLoadedResults": { + return { + ...state, + loadedResultsFromModule: action.payload, + }; + } case "updateResults": { return { ...state, diff --git a/web/app/src/utils/ErrorBoundary.js b/web/app/src/utils/ErrorBoundary.js new file mode 100644 index 0000000..a068358 --- /dev/null +++ b/web/app/src/utils/ErrorBoundary.js @@ -0,0 +1,26 @@ +import { Component } from "react"; +import { MDBTypography as Typography } from "mdbreact"; + +class ErrorBoundary extends Component { + state = { + hasError: false, + }; + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) {} + + render() { + return this.state.hasError ? ( + + Something went wrong 😢 + + ) : ( + this.props.children + ); + } +} + +export default ErrorBoundary;