Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,8 @@ venv.bak/
# vagrant
.vagrant/

.DS_Store
.DS_Store

.env.dev

db_dump.sql
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Comment on lines +11 to +23
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed needs for local_settings.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next step: remove the need for Vagrant 😁



## Vagrant
```
Expand Down
36 changes: 36 additions & 0 deletions app/portal/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM python:3.12-slim
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather use this base image and try to bump django versions than use a docker image for an older python version.


# 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"]
34 changes: 34 additions & 0 deletions app/portal/portal/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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
Comment thread
paigewilliams marked this conversation as resolved.

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
22 changes: 16 additions & 6 deletions app/portal/portal/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(","))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1



# Application definition
Expand Down Expand Up @@ -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),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PASSWORD, HOST, and PORT were omitted in the past to support the database using postgres as the owner and default user, which also bypassed the need for a password (providing these fields actually broke that workflow).

Explicitly creating a new PostgreSQL user with a password and making them the owner of the tables is MUCH more secure, and I like that you are re-investing some of the gains from the streamlining gains of dockerizing this tool into making deployments follow best practices!

'HOST': os.environ.get('SQL_HOST', default='db'),
'PORT': os.environ.get('SQL_PORT', default='5432'),
}
}

Expand Down Expand Up @@ -175,5 +181,9 @@

MIN_SEARCH_RANK=0.1
MIN_SEARCH_SIMILARITY=0.25
MAPBOX_TOKEN = os.environ.get('MAPBOX_TOKEN', default='')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we using MapBox for? GeoLocation?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked and found a commit where you, @rhodges, added MAPBOX_TOKEN to the local settings template in 2019. As well as its use for geocoding in models.py


from .local_settings import *
try:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

from .local_settings import *
except ImportError:
pass
48 changes: 28 additions & 20 deletions app/portal/providers/forms.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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')
]
1 change: 0 additions & 1 deletion app/portal/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the pendulum had swung and psycopg2 was now being preferred over psycopg2-binary -- maybe this shift was between Django 3.2 and Django 4.2?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be 100% wrong, too.

ptyprocess==0.6.0
py-moneyed==0.8.0
Expand Down
10 changes: 10 additions & 0 deletions docker/.env.template
Original file line number Diff line number Diff line change
@@ -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=
55 changes: 55 additions & 0 deletions docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -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:
22 changes: 22 additions & 0 deletions docker/init-db.sh
Original file line number Diff line number Diff line change
@@ -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!"