diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb3a07b --- /dev/null +++ b/Makefile @@ -0,0 +1,142 @@ +SHELL:=/bin/bash +UNAME:=$(shell uname) +export LOG_DIR:=logs + +# full installation of all the software and config setup +install: conda-install django conf-setup + +# starts the web server and app server +# wait a second for gunicorn to start before starting nginx +start: gunicorn-start + sleep 1 + $(MAKE) nginx-start + +# stop all the servers +stop: gunicorn-kill nginx-stop + + +# ~~~~~ Setup Conda ~~~~~ # +# this sets the system PATH to ensure we are using in included 'conda' installation for all software +PATH:=$(CURDIR)/conda/bin:$(PATH) +unexport PYTHONPATH +unexport PYTHONHOME + +# install versions of conda for Mac or Linux +ifeq ($(UNAME), Darwin) +CONDASH:=Miniconda3-4.5.4-MacOSX-x86_64.sh +endif + +ifeq ($(UNAME), Linux) +CONDASH:=Miniconda3-4.5.4-Linux-x86_64.sh +endif + +CONDAURL:=https://repo.continuum.io/miniconda/$(CONDASH) + +# install conda +conda: + @echo ">>> Setting up conda..." + @wget "$(CONDAURL)" && \ + bash "$(CONDASH)" -b -p conda && \ + rm -f "$(CONDASH)" + +# install the conda packages required +conda-install: conda + conda install -y -c anaconda \ + django=2.1.2 \ + gunicorn=19.9.0 \ + nginx=1.15.5 + +# ~~~~~ SETUP DJANGO APP ~~~~~ # +# create the app for development; only need to run this when first creating repo +# django-start: +# django-admin startproject webapp . +# python manage.py startapp helloworld + +# all the steps needed to set up the django app +django: django-init django-import django-collectstatic + +# setup the app for the first time +django-init: + python manage.py makemigrations + python manage.py migrate + python manage.py createsuperuser + +# import items to the database +django-import: + python db_import.py + +# copy static files over for web hosting +django-collectstatic: + python manage.py collectstatic + +# run the Django dev server +django-runserver: + python manage.py runserver + +# ~~~~~~ setup Gunicorn WSGI server ~~~~~ # +# set the path to the Unix socket to bind Gunicorn to +SOCKET_FILE:=$(CURDIR)/django.sock +SOCKET:=unix:$(SOCKET_FILE) + +GUNICORN_CONFIG:=config/gunicorn_config.py +GUNICORN_PIDFILE:=$(LOG_DIR)/gunicorn.pid +GUNICORN_ACCESS_LOG:=$(LOG_DIR)/gunicorn.access.log +GUNICORN_ERROR_LOG:=$(LOG_DIR)/gunicorn.error.log +GUNICORN_LOG:=$(LOG_DIR)/gunicorn.log +GUNICORN_PID:= + +# start gunicorn as a background process; direct paths to the logs and pid files +gunicorn-start: + gunicorn config.wsgi \ + --bind "$(SOCKET)" \ + --config "$(GUNICORN_CONFIG)" \ + --pid "$(GUNICORN_PIDFILE)" \ + --access-logfile "$(GUNICORN_ACCESS_LOG)" \ + --error-logfile "$(GUNICORN_ERROR_LOG)" \ + --log-file "$(GUNICORN_LOG)" \ + --daemon + +# check to see if gunicorn is running +gunicorn-check: + ps -ax | grep gunicorn + +# stop gunicorn; get the process ID from the pid file +gunicorn-kill: GUNICORN_PID=$(shell head -1 $(GUNICORN_PIDFILE)) +gunicorn-kill: $(GUNICORN_PIDFILE) + kill "$(GUNICORN_PID)" + + +# ~~~~~ set nginx web sever ~~~~~ # +# location of nginx "prefix" directory in this repo +NGINX_PREFIX:=$(CURDIR)/nginx +# conf file is located inside the prefix dir +NGINX_CONF:=nginx.conf + +# need to edit the myapp.conf file to update it with the paths for this directory +OLD_CONF:=nginx/myapp.conf.og +NEW_CONF:=nginx/myapp.conf +conf-setup: $(NEW_CONF) +$(NEW_CONF): + cat "$(OLD_CONF)" | \ + sed 's|unix:/usr/local/bin/apps/myapp/myapp.sock|$(SOCKET)|' | \ + sed 's|/usr/local/bin/apps/myapp/|$(CURDIR)|' > "$(NEW_CONF)" + +# start nginx; make sure the socket and config files exist first +nginx-start: $(NEW_CONF) $(SOCKET_FILE) + nginx -p "$(NGINX_PREFIX)" -c "$(NGINX_CONF)" + +# stop nginx +nginx-stop: + nginx -p "$(NGINX_PREFIX)" -c "$(NGINX_CONF)" -s quit + +# reload any updates to the nginx config files +nginx-reload: + nginx -p "$(NGINX_PREFIX)" -c "$(NGINX_CONF)" -s reload + +# test to make sure nginx config files can be found and look valid +nginx-test: + nginx -p "$(NGINX_PREFIX)" -c "$(NGINX_CONF)" -t + +# check to see if nginx is running +nginx-check: + ps -ax | grep nginx diff --git a/config/gunicorn_config.py b/config/gunicorn_config.py new file mode 100644 index 0000000..3265a2a --- /dev/null +++ b/config/gunicorn_config.py @@ -0,0 +1,217 @@ +# Sample Gunicorn configuration file. +# https://raw.githubusercontent.com/benoitc/gunicorn/master/examples/example_config.py +# +# Server socket +# +# bind - The socket to bind. +# +# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'. +# An IP is a valid HOST. +# +# backlog - The number of pending connections. This refers +# to the number of clients that can be waiting to be +# served. Exceeding this number results in the client +# getting an error when attempting to connect. It should +# only affect servers under significant load. +# +# Must be a positive integer. Generally set in the 64-2048 +# range. +# + + +bind = 'myserver.com:1234' +backlog = 2048 + +# +# Worker processes +# +# workers - The number of worker processes that this server +# should keep alive for handling requests. +# +# A positive integer generally in the 2-4 x $(NUM_CORES) +# range. You'll want to vary this a bit to find the best +# for your particular application's work load. +# +# worker_class - The type of workers to use. The default +# sync class should handle most 'normal' types of work +# loads. You'll want to read +# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type +# for information on when you might want to choose one +# of the other worker classes. +# +# A string referring to a Python path to a subclass of +# gunicorn.workers.base.Worker. The default provided values +# can be seen at +# http://docs.gunicorn.org/en/latest/settings.html#worker-class +# +# worker_connections - For the eventlet and gevent worker classes +# this limits the maximum number of simultaneous clients that +# a single process can handle. +# +# A positive integer generally set to around 1000. +# +# timeout - If a worker does not notify the master process in this +# number of seconds it is killed and a new worker is spawned +# to replace it. +# +# Generally set to thirty seconds. Only set this noticeably +# higher if you're sure of the repercussions for sync workers. +# For the non sync workers it just means that the worker +# process is still communicating and is not tied to the length +# of time required to handle a single request. +# +# keepalive - The number of seconds to wait for the next request +# on a Keep-Alive HTTP connection. +# +# A positive integer. Generally set in the 1-5 seconds range. +# + +workers = 1 +worker_class = 'sync' +worker_connections = 1000 +timeout = 30 +keepalive = 2 + +# +# spew - Install a trace function that spews every line of Python +# that is executed when running the server. This is the +# nuclear option. +# +# True or False +# + +spew = False + +# +# Server mechanics +# +# daemon - Detach the main Gunicorn process from the controlling +# terminal with a standard fork/fork sequence. +# +# True or False +# +# raw_env - Pass environment variables to the execution environment. +# +# pidfile - The path to a pid file to write +# +# A path string or None to not write a pid file. +# +# user - Switch worker processes to run as this user. +# +# A valid user id (as an integer) or the name of a user that +# can be retrieved with a call to pwd.getpwnam(value) or None +# to not change the worker process user. +# +# group - Switch worker process to run as this group. +# +# A valid group id (as an integer) or the name of a user that +# can be retrieved with a call to pwd.getgrnam(value) or None +# to change the worker processes group. +# +# umask - A mask for file permissions written by Gunicorn. Note that +# this affects unix socket permissions. +# +# A valid value for the os.umask(mode) call or a string +# compatible with int(value, 0) (0 means Python guesses +# the base, so values like "0", "0xFF", "0022" are valid +# for decimal, hex, and octal representations) +# +# tmp_upload_dir - A directory to store temporary request data when +# requests are read. This will most likely be disappearing soon. +# +# A path to a directory where the process owner can write. Or +# None to signal that Python should choose one on its own. +# + +daemon = False +# raw_env = [ +# 'DJANGO_SECRET_KEY=something', +# 'SPAM=eggs', +# ] +pidfile = None +umask = 0 +user = None +group = None +tmp_upload_dir = None + +# +# Logging +# +# logfile - The path to a log file to write to. +# +# A path string. "-" means log to stdout. +# +# loglevel - The granularity of log output +# +# A string of "debug", "info", "warning", "error", "critical" +# + +errorlog = '-' +loglevel = 'info' +accesslog = '-' +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + +# +# Process naming +# +# proc_name - A base to use with setproctitle to change the way +# that Gunicorn processes are reported in the system process +# table. This affects things like 'ps' and 'top'. If you're +# going to be running more than one instance of Gunicorn you'll +# probably want to set a name to tell them apart. This requires +# that you install the setproctitle module. +# +# A string or None to choose a default of something like 'gunicorn'. +# + +proc_name = None + +# +# Server hooks +# +# post_fork - Called just after a worker has been forked. +# +# A callable that takes a server and worker instance +# as arguments. +# +# pre_fork - Called just prior to forking the worker subprocess. +# +# A callable that accepts the same arguments as after_fork +# +# pre_exec - Called just prior to forking off a secondary +# master process during things like config reloading. +# +# A callable that takes a server instance as the sole argument. +# + +def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def pre_fork(server, worker): + pass + +def pre_exec(server): + server.log.info("Forked child, re-executing.") + +def when_ready(server): + server.log.info("Server is ready. Spawning workers") + +def worker_int(worker): + worker.log.info("worker received INT or QUIT signal") + + ## get traceback info + import threading, sys, traceback + id2name = {th.ident: th.name for th in threading.enumerate()} + code = [] + for threadId, stack in sys._current_frames().items(): + code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), + threadId)) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, + lineno, name)) + if line: + code.append(" %s" % (line.strip())) + worker.log.debug("\n".join(code)) + +def worker_abort(worker): + worker.log.info("worker received SIGABRT signal") diff --git a/config/settings.py b/config/settings.py index 5686b59..da9ec0f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -72,7 +72,7 @@ MIDDLEWARE = [ MEDIA_ROOT = '/tmp/media/' MEDIA_URL = '/media/' -STATIC_ROOT = os.path.join(BASE_DIR,'sitestatic') +STATIC_ROOT = secrets.get('STATIC_ROOT',os.path.join(BASE_DIR,'sitestatic')) STATIC_URL = '/static/' TEMPLATES = [ diff --git a/yeast/migrations/0028_alter_batch_id_alter_manufacturer_id_and_more.py b/yeast/migrations/0028_alter_batch_id_alter_manufacturer_id_and_more.py new file mode 100644 index 0000000..1b7e82b --- /dev/null +++ b/yeast/migrations/0028_alter_batch_id_alter_manufacturer_id_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.6 on 2024-05-31 11:40 + +import config.extras +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('yeast', '0027_alter_batch_source_batch_alter_batch_strain_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='batch', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, max_length=22, primary_key=True, serialize=False, unique=True), + ), + migrations.AlterField( + model_name='manufacturer', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, max_length=22, primary_key=True, serialize=False, unique=True), + ), + migrations.AlterField( + model_name='storage', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, max_length=22, primary_key=True, serialize=False, unique=True), + ), + migrations.AlterField( + model_name='strain', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, max_length=22, primary_key=True, serialize=False, unique=True), + ), + migrations.AlterField( + model_name='yeast', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, max_length=22, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/yeast/migrations/0029_alter_manufacturer_id_alter_storage_id_and_more.py b/yeast/migrations/0029_alter_manufacturer_id_alter_storage_id_and_more.py new file mode 100644 index 0000000..655a57c --- /dev/null +++ b/yeast/migrations/0029_alter_manufacturer_id_alter_storage_id_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.6 on 2024-05-31 11:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yeast', '0028_alter_batch_id_alter_manufacturer_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='manufacturer', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='storage', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='strain', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/yeast/migrations/0030_alter_batch_id_alter_yeast_id.py b/yeast/migrations/0030_alter_batch_id_alter_yeast_id.py new file mode 100644 index 0000000..ad4e7d3 --- /dev/null +++ b/yeast/migrations/0030_alter_batch_id_alter_yeast_id.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2024-05-31 12:03 + +import config.extras +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('yeast', '0029_alter_manufacturer_id_alter_storage_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='batch', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, primary_key=True, serialize=False, unique=True), + ), + migrations.AlterField( + model_name='yeast', + name='id', + field=config.extras.DateUUIDField(blank=True, editable=False, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/yeast/templates/yeast/sample.html b/yeast/templates/yeast/sample.html index 7692ebf..12ec652 100644 --- a/yeast/templates/yeast/sample.html +++ b/yeast/templates/yeast/sample.html @@ -16,7 +16,8 @@
{% for sample in batch.remaining_samples %}