diff --git a/.gitignore b/.gitignore index 9c091a6..3358e10 100644 --- a/.gitignore +++ b/.gitignore @@ -115,4 +115,8 @@ venv.bak/ # vagrant .vagrant/ -.DS_Store \ No newline at end of file +.DS_Store + +.env.dev + +db_dump.sql \ No newline at end of file diff --git a/README.md b/README.md index 8387b13..51c17e4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ Oregon Harvest For Schools Portal git clone https://github.com/Ecotrust/OH4S_Proteins.git cd OH4S_Proteins ``` +## Choose your development environment: Vagrant or Docker + +## Docker + +- In order to run the application with production data, locate a database dump of a production install. Add the dump to the `docker` directory and name it `db_dump.sql`. This is necessary for the `init-db.sh` script to run correctly. + +- Create a copy of the `.env.template` file as `.env` in the `docker` directory. Add your environment variables to that file. + +- Move to the docker directory: `cd docker` + +- Start the docker containers: `docker compose up` + +- View the app at `http://localhost:8000` + ## Vagrant ``` diff --git a/app/portal/Dockerfile b/app/portal/Dockerfile new file mode 100644 index 0000000..816c40e --- /dev/null +++ b/app/portal/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.12-slim + +# Prevent Python from writing .pyc files and enable unbuffered stdout/stderr +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy system requirements +COPY requirements.txt /app/ + +# Upgrade pip and install dependencies +RUN pip install --upgrade pip setuptools wheel + +# Install dependencies +RUN pip install --upgrade pip && \ + pip install -r requirements.txt + +# Copy application code +COPY . /app/ + +COPY portal/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 8000 + +ENV DJANGO_SETTINGS_MODULE=portal.settings + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/app/portal/portal/entrypoint.sh b/app/portal/portal/entrypoint.sh new file mode 100644 index 0000000..46ee211 --- /dev/null +++ b/app/portal/portal/entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Exit on errors +set -e + +# If a SQL_HOST is provided, wait for Postgres to become available before running +# migrations. This prevents race conditions when using docker-compose where the +# web container starts before the DB is ready. +if [ -n "$SQL_HOST" ]; then + echo "Waiting for database at ${SQL_HOST}:${SQL_PORT:-5432}..." + # pg_isready is available after installing postgresql-client in the image + until pg_isready -h "$SQL_HOST" -p "${SQL_PORT:-5432}" >/dev/null 2>&1; do + echo "Postgres is unavailable - sleeping" + sleep 1 + done + echo "Postgres is up" +fi + +echo "Applying database migrations..." +python manage.py migrate --noinput +echo "Collecting static files..." +python manage.py collectstatic --noinput + +echo "Checking for existing providers..." +if [ "$(python manage.py shell -c 'from providers.models import PoliticalRegion; print(PoliticalRegion.objects.count())' 2>/dev/null | tail -1)" = "0" ]; then + echo "No providers found, loading default providers fixture..." + python manage.py loaddata fixtures/providers_20210524.json +else + echo "number of political regions: $(python manage.py shell -c 'from providers.models import PoliticalRegion; print(PoliticalRegion.objects.count())' 2>/dev/null | tail -1)" + echo "Providers content already exist, skipping fixture load." +fi + +echo "Starting python development server on :8000" +python manage.py runserver 0.0.0.0:8000 diff --git a/app/portal/portal/settings.py b/app/portal/portal/settings.py index ea67ea0..5b21844 100644 --- a/app/portal/portal/settings.py +++ b/app/portal/portal/settings.py @@ -32,12 +32,15 @@ # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 't8(b^k=eh4pu6o6to8px!7pmcf)@x#p$&nyp&ksm!oc00s2s-(' +SECRET_KEY = os.environ.get('SECRET_KEY', default='set in .env file') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.environ.get('DEBUG', 'True') == 'True' ALLOWED_HOSTS = [] +ALLOWED_HOSTS_ENV = os.environ.get("ALLOWED_HOSTS") +if ALLOWED_HOSTS_ENV: + ALLOWED_HOSTS.extend(ALLOWED_HOSTS_ENV.split(",")) # Application definition @@ -112,9 +115,12 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'set_in_local_settings.py', - 'USER': 'set_in_local_settings.py' + 'ENGINE': os.environ.get('SQL_ENGINE', default='django.db.backends.postgresql'), + 'NAME': os.environ.get('SQL_DATABASE', default='postgres'), + 'USER': os.environ.get('SQL_USER', default='postgres'), + 'PASSWORD': os.environ.get('SQL_PASSWORD', default=None), + 'HOST': os.environ.get('SQL_HOST', default='db'), + 'PORT': os.environ.get('SQL_PORT', default='5432'), } } @@ -175,5 +181,9 @@ MIN_SEARCH_RANK=0.1 MIN_SEARCH_SIMILARITY=0.25 +MAPBOX_TOKEN = os.environ.get('MAPBOX_TOKEN', default='') -from .local_settings import * +try: + from .local_settings import * +except ImportError: + pass \ No newline at end of file diff --git a/app/portal/providers/forms.py b/app/portal/providers/forms.py index e9e72fb..058c99e 100644 --- a/app/portal/providers/forms.py +++ b/app/portal/providers/forms.py @@ -1,25 +1,8 @@ from django import forms +from providers.models import DeliveryMethod, PoliticalSubregion class FilterForm(forms.Form): - def __init__(self,*args,**kwargs): - try: - product_details_choices = kwargs.pop('product_details') - self.base_fields['product_category'].choices = product_details_choices - self.base_fields['product_category'].initial = [ x[0] for x in product_details_choices ] - except Exception as e: - pass - try: - self.base_fields['capacity'].label = "%s per year" % kwargs.pop('production_capacity_unit') - except Exception as e: - pass - - super(FilterForm,self).__init__(*args,**kwargs) - - from providers.models import DeliveryMethod, PoliticalSubregion - deliveryMethodChoices = [(-1, 'Any')] + [(x.pk, str(x)) for x in DeliveryMethod.objects.all().order_by('name')] - availabilityChoices = [(-1, 'Anywhere')] + [(x.pk, str(x)) for x in PoliticalSubregion.objects.all().order_by('name')] - product_category = forms.MultipleChoiceField( widget=forms.CheckboxSelectMultiple, label='...by Product Details', @@ -35,13 +18,38 @@ def __init__(self,*args,**kwargs): distribution = forms.ChoiceField( initial="Any", label="Method of distribution", - choices=deliveryMethodChoices, + choices=[], required=False, ) availability = forms.ChoiceField( initial="Anywhere", label="Confirmed available in", - choices=availabilityChoices, + choices=[], required=False, ) + + def __init__(self,*args,**kwargs): + try: + product_details_choices = kwargs.pop('product_details') + except KeyError: + product_details_choices = None + try: + production_capacity_unit = kwargs.pop('production_capacity_unit') + except KeyError: + production_capacity_unit = None + + super(FilterForm,self).__init__(*args,**kwargs) + + if product_details_choices is not None: + self.fields['product_category'].choices = product_details_choices + self.fields['product_category'].initial = [x[0] for x in product_details_choices] + if production_capacity_unit is not None: + self.fields['capacity'].label = "%s per year" % production_capacity_unit + + self.fields['distribution'].choices = [(-1, 'Any')] + [ + (x.pk, str(x)) for x in DeliveryMethod.objects.all().order_by('name') + ] + self.fields['availability'].choices = [(-1, 'Anywhere')] + [ + (x.pk, str(x)) for x in PoliticalSubregion.objects.all().order_by('name') + ] diff --git a/app/portal/requirements.txt b/app/portal/requirements.txt index 16cd3e1..4e6f256 100644 --- a/app/portal/requirements.txt +++ b/app/portal/requirements.txt @@ -39,7 +39,6 @@ pexpect==4.6.0 phonenumbers==8.10.4 pickleshare==0.7.5 prompt-toolkit>=3.0.30 -psycopg2==2.9.10 psycopg2-binary==2.9.10 ptyprocess==0.6.0 py-moneyed==0.8.0 diff --git a/docker/.env.template b/docker/.env.template new file mode 100644 index 0000000..3b4f7fe --- /dev/null +++ b/docker/.env.template @@ -0,0 +1,10 @@ +DEBUG= +SECRET_KEY= +ALLOWED_HOSTS= +SQL_ENGINE=django.db.backends.postgresql +SQL_DATABASE= +SQL_USER= +SQL_PASSWORD= +SQL_HOST=db +SQL_PORT=5432 +MAPBOX_TOKEN= \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..c69adb8 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,55 @@ +services: + db: + image: postgis/postgis:16-3.5-alpine + restart: always + platform: linux/amd64 + environment: + POSTGRES_DB: ${SQL_DATABASE} + POSTGRES_USER: ${SQL_USER} + POSTGRES_PASSWORD: ${SQL_PASSWORD} + volumes: + - oh4s:/var/lib/postgresql/data + - ./db_dump.sql:/tmp/db_dump.sql:ro + - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${SQL_USER} -d ${SQL_DATABASE} -h localhost -p ${SQL_PORT:-5432}"] + interval: 10s + timeout: 5s + retries: 5 + + web: + build: + context: ../app/portal + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - path: .env + required: true + ports: + - "8000:8000" + healthcheck: + test: ["CMD-SHELL", "python manage.py check --deploy 2>/dev/null || python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/\")' 2>/dev/null"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + environment: + ALLOWED_HOSTS: ${ALLOWED_HOSTS} + DEBUG: ${DEBUG} + SQL_ENGINE: ${SQL_ENGINE} + SQL_HOST: ${SQL_HOST} + SQL_PORT: ${SQL_PORT} + SQL_DATABASE: ${SQL_DATABASE} + SQL_USER: ${SQL_USER} + SQL_PASSWORD: ${SQL_PASSWORD} + SECRET_KEY: ${SECRET_KEY} + depends_on: + db: + condition: service_healthy + volumes: + - ../app/portal:/app + +volumes: + oh4s: \ No newline at end of file diff --git a/docker/init-db.sh b/docker/init-db.sh new file mode 100755 index 0000000..fd204c4 --- /dev/null +++ b/docker/init-db.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +# This script runs automatically when the PostgreSQL container is first initialized +# It will only run if the database is empty (first time setup) + + DB_DUMP_FILE="/tmp/db_dump.sql" + if [ ! -f "$DB_DUMP_FILE" ]; then + echo "Error: database dump file not found at $DB_DUMP_FILE. Mount or copy the SQL dump before initializing the database." >&2 + exit 1 + fi + if [ ! -r "$DB_DUMP_FILE" ]; then + echo "Error: database dump file is not readable at $DB_DUMP_FILE. Check file permissions before initializing the database." >&2 + exit 1 + fi + +echo "Importing database dump..." + +# Import the SQL dump, ignoring meta-command errors +PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=0 < "$DB_DUMP_FILE" + +echo "Database import completed!" \ No newline at end of file