Compare commits

...

10 commits

Author SHA1 Message Date
Dave Lane
20c80852b3 updates to make this run on the new RightPlant server 2025-07-21 10:54:26 +12:00
Matthew Northcott
d09e6f3914 Actually fix the bugs this time 2023-03-30 15:30:35 +13:00
Matthew Northcott
00afd05abb Further production updates
- add collectstatic job to docker-compose.yaml
- remove old dev script
- add a recipe for building the frontend distributable
- fix nginx location for react-router endpoints
- fix bug in tasks.py
2023-03-30 14:27:56 +13:00
Matthew Northcott
7e505ad493 Add recipe for creating certificates 2023-03-30 10:35:08 +13:00
Matthew Northcott
cd93fe8708 Run backend as non-root user 2023-03-29 16:41:46 +13:00
Matthew Northcott
625291b4bf Update docker-compose
- add healthcheck to celery
- update redis
- update healthcheck for postgres
2023-03-29 16:40:45 +13:00
Matthew Northcott
c980479d6c Small fixes and updates 2023-03-29 16:37:39 +13:00
Matthew Northcott
b30fd62f48 Fix JS console errors 2023-03-28 13:00:29 +13:00
Matthew Northcott
c8851d552f Update deployment config 2023-03-28 13:00:15 +13:00
Matthew Northcott
5990144005 Allow Stripe configuration via environment variables 2023-03-28 12:05:03 +13:00
21 changed files with 151 additions and 304 deletions

View file

@ -10,8 +10,8 @@ export GID
frontend/node_modules: frontend/node_modules:
docker run --rm -v ${PWD}/frontend:/app -w /app -u ${UID}:${GID} node:16-bullseye npm i docker run --rm -v ${PWD}/frontend:/app -w /app -u ${UID}:${GID} node:16-bullseye npm i
backend/right_tree/staticfiles: frontend/build: frontend/node_modules
docker run --rm -v ${PWD}/backend:/app -w /app -u ${UID}:${GID} right-tree python manage.py collectstatic --noinput docker run --rm -v ${PWD}/frontend:/app -w /app -u ${UID}:${GID} node:16-bullsye npm build
ingest: ingest:
docker-compose up -d backend postgres docker-compose up -d backend postgres
@ -63,6 +63,20 @@ logs:
stop: stop:
docker-compose down docker-compose down
cert:
docker run --rm \
--name certbot \
-p 443:443 \
-p 80:80 \
-v /etc/letsencrypt:/etc/letsencrypt \
certbot/certbot \
certonly \
--standalone \
--non-interactive \
--preferred-challenges http \
--logs-dir /etc/letsencrypt/logs \
-d rightplant.nz
clean: stop clean: stop
git clean -dxf git clean -dxf

View file

@ -1,3 +1,4 @@
__pycache__/ __pycache__/
staticfiles/ staticfiles/
media/
*.pyc *.pyc

View file

@ -7,10 +7,12 @@ RUN apt update \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& apt clean && apt clean
COPY ./requirements.txt /app/requirements.txt
RUN pip install -U --no-cache-dir -r requirements.txt
COPY . /app COPY . /app
RUN pip install -U --no-cache-dir -r requirements.txt && \
useradd -Mu 1000 righttree && \
chown -R righttree:righttree /app
ENV DJANGO_SETTINGS_MODULE="right_tree.settings" ENV DJANGO_SETTINGS_MODULE="right_tree.settings"
USER righttree

View file

@ -8,6 +8,6 @@ gunicorn==20.1.0
pandas==1.5.3 pandas==1.5.3
pdfkit==1.0.0 pdfkit==1.0.0
PyPDF2==1.28.6 PyPDF2==1.28.6
redis==4.5.1 redis==4.5.3
celery[redis]==5.2.7 celery[redis]==5.2.7
stripe==5.2.0 stripe==5.2.0

View file

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ("celery_app",)

View file

@ -1,5 +1,10 @@
import os
from celery import Celery from celery import Celery
app = Celery('righttree')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'right_tree.settings')
app = Celery('right_tree')
app.config_from_object('django.conf:settings', namespace='CELERY') app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks() app.autodiscover_tasks()

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.17 on 2023-03-29 03:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('api', '0015_auto_20230306_1620'),
]
operations = [
migrations.AlterField(
model_name='activationkey',
name='key_set',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api.activationkeyset'),
),
]

View file

@ -222,7 +222,7 @@ class ActivationKey(models.Model):
) )
key = models.CharField(max_length=20, unique=True, default=key_default) key = models.CharField(max_length=20, unique=True, default=key_default)
key_set = models.ForeignKey(ActivationKeySet, on_delete=models.CASCADE, null=True) key_set = models.ForeignKey(ActivationKeySet, on_delete=models.PROTECT, null=True)
remaining_activations = models.SmallIntegerField(default=1) remaining_activations = models.SmallIntegerField(default=1)
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)

View file

@ -1,3 +1,4 @@
from pathlib import Path
from shutil import rmtree from shutil import rmtree
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
@ -10,7 +11,10 @@ from .resource_generation_utils import storage
@receiver(post_delete, sender=Export) @receiver(post_delete, sender=Export)
def delete_export(sender, instance, *args, **kwargs): def delete_export(sender, instance, *args, **kwargs):
"""Clean up created files on the filesystem when an export is deleted""" """Clean up created files on the filesystem when an export is deleted"""
rmtree(storage.path(f"export_{instance.pk}")) path = storage.path(f"export_{instance.pk}")
if Path(path).exists():
rmtree(path)
@receiver(post_save, sender=ActivationKeySet) @receiver(post_save, sender=ActivationKeySet)

View file

@ -14,9 +14,10 @@ from .resource_generation_utils import create_planting_guide_pdf, get_filter_val
@shared_task @shared_task
def generate_pdf(questionnaire_id, export_id): def generate_pdf(questionnaire_id, export_id):
q = Questionnaire.objects.get(pk=questionnaire_id) q = Questionnaire.objects.get(pk=questionnaire_id)
e = Export.objects.get(pk=export_id)
z = q.zone z = q.zone
filename = f"export_{e.pk}/{q.slug}.pdf"
export = Export.objects.get(pk=export_id)
filename = f"export_{export.pk}/{q.slug}.pdf"
try: try:
create_planting_guide_pdf( create_planting_guide_pdf(
@ -34,9 +35,9 @@ def generate_pdf(questionnaire_id, export_id):
else: else:
if not storage.exists(filename): if not storage.exists(filename):
raise FileNotFoundError(f"There was an error creating file: {filename}") raise FileNotFoundError(f"There was an error creating file: {filename}")
finally:
if e.completion >= 1: if export.completion >= 1:
generate_zip.delay(export_id) generate_zip.delay(export_id)
@shared_task @shared_task

View file

@ -3,6 +3,7 @@ import stripe
from datetime import timedelta from datetime import timedelta
from django.conf import settings
from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseBadRequest, FileResponse from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseBadRequest, FileResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
@ -222,12 +223,15 @@ def activate_key(request):
def purchase_key(request): def purchase_key(request):
"""Generate a prospective key and redirect to the Stripe payment portal""" """Generate a prospective key and redirect to the Stripe payment portal"""
stripe.api_key = settings.STRIPE_API_KEY
key = ActivationKey.key_default() key = ActivationKey.key_default()
redirect_url = request.build_absolute_uri(reverse(activate_key)) + f"?key={key}" redirect_url = request.build_absolute_uri(reverse(activate_key)) + f"?key={key}"
stripe_session = stripe.checkout.Session.create( stripe_session = stripe.checkout.Session.create(
line_items=[ line_items=[
{ {
"price": "price_1Mh1I6GLlkkooLVio8W3TGkR", "price": settings.STRIPE_PRICE_ID,
"quantity": 1, "quantity": 1,
}, },
], ],
@ -235,7 +239,7 @@ def purchase_key(request):
invoice_creation={ invoice_creation={
'enabled': True, 'enabled': True,
'invoice_data': { 'invoice_data': {
'description': f'Your product code is {key}', 'description': f'Your activation key is {key}',
'rendering_options': {'amount_tax_display': 'include_inclusive_tax'}, 'rendering_options': {'amount_tax_display': 'include_inclusive_tax'},
'footer': 'BioSphere Capital Limited', 'footer': 'BioSphere Capital Limited',
}, },

View file

@ -11,7 +11,6 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
import os import os
import stripe
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -164,4 +163,5 @@ CELERY_TIMEZONE = TIME_ZONE
CELERY_BROKER_URL = REDIS_CELERY_URL CELERY_BROKER_URL = REDIS_CELERY_URL
# Stripe payment processing # Stripe payment processing
stripe.api_key = os.environ['STRIPE_API_KEY'] STRIPE_API_KEY = os.environ['STRIPE_API_KEY']
STRIPE_PRICE_ID = os.environ['STRIPE_PRICE_ID']

View file

@ -1,9 +1,14 @@
LINZ_API_KEY=myapikey LINZ_API_KEY=myapikey
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
DATABASE_NAME=righttree DATABASE_NAME=righttree
DATABASE_USER=righttree DATABASE_USER=righttree
DATABASE_PASSWORD=righttree DATABASE_PASSWORD=righttree
DATABASE_HOST=postgres DATABASE_HOST=postgres
CELERY_BROKER_URL=redis://redis:6379/0 REDIS_HOST=redis
BASE_URL=localhost:8000 REDIS_PASSWORD=redis
DJANGO_SECRET_KEY=changeme DJANGO_SECRET_KEY=changeme
DJANGO_DEBUG_MODE=True DJANGO_DEBUG_MODE=True
STRIPE_API_KEY=sk_test_key
STRIPE_PRICE_ID=price_priceid

138
dev
View file

@ -1,138 +0,0 @@
#!/bin/bash
# Load .env file if it exists
if [ -f .env ]
then
export $(cat .env | sed 's/#.*//g' | xargs)
fi
cmd_create_database() {
echo "Creating right_tree database..."
docker-compose down --remove-orphans --volumes
docker-compose -f docker-compose.yaml up postgres | sed '/PostgreSQL init process complete; ready for start up./q'
docker-compose down
}
cmd_makemigrations() {
echo "Creating database migrations..."
docker-compose exec backend python manage.py makemigrations --no-input
}
cmd_migrate() {
echo "Running database migrations..."
docker-compose exec backend python manage.py migrate
}
cmd_createsuperuser() {
echo "Creating django superuser..."
docker-compose run backend python manage.py createsuperuser
}
cmd_load_fixtures() {
echo "Loading fixtures..."
docker-compose exec backend bash -c "python manage.py loaddata right_tree/api/data/fixtures/*.json"
}
cmd_load_shapefiles() {
echo "Loading shapefiles into the database..."
docker-compose exec backend python manage.py loadshapefiles
}
cmd_create_plant_fixtures() {
echo "Creates fixtures for plants using spreadsheet."
docker-compose exec backend python manage.py createplantfixtures
}
cmd_reset_plants() {
echo "Resetting plants..."
docker-compose exec backend python manage.py resetplants
}
cmd_load_plant_fixtures() {
echo "Loading plants..."
docker-compose exec backend python manage.py loaddata right_tree/api/data/fixtures/plants.json
}
cmd_load_plants() {
cmd_create_plant_fixtures
cmd_reset_plants
cmd_load_plant_fixtures
}
cmd_load_sites_from_spreadsheet() {
echo "Loading habitats and zones..."
docker-compose exec backend python manage.py loadsitedata
}
cmd_populate_database() {
echo "Populating the database..."
docker-compose up -d backend postgres
cmd_makemigrations
cmd_migrate
cmd_load_fixtures
cmd_load_shapefiles
cmd_load_plants
docker-compose down
}
cmd_init_database() {
cmd_create_database
cmd_populate_database
}
cmd_reset_database() {
cmd_init_database
}
cmd_build() {
docker-compose build
}
cmd_start() {
docker-compose up --remove-orphans
}
cmd_collectstatic() {
docker-compose -f docker-compose.production.yaml build
docker-compose -f docker-compose.production.yaml run backend python manage.py collectstatic --no-input
}
cmd_build_frontend() {
docker run -v $PWD/frontend:/app -w /app node:16-alpine3.11 npm install
docker run -v $PWD/frontend:/app -w /app node:16-alpine3.11 mkdir -p node_modules/.cache
docker run -v $PWD/frontend:/app -w /app node:16-alpine3.11 chmod -R 777 node_modules/.cache
docker run -v $PWD/frontend:/app -w /app node:16-alpine3.11 npm run build
}
cmd_create_staticfiles() {
cmd_collectstatic
cmd_build_frontend
}
cmd_build_production() {
docker-compose -f docker-compose.production.yaml build
}
cmd_start_production() {
docker-compose -f docker-compose.production.yaml up -d --remove-orphans
}
cmd_stop_production() {
docker-compose -f docker-compose.production.yaml stop --remove-orphans
}
cmd_renew_certifcate() {
cmd_stop_production
sudo docker run -i --rm --name certbot -p 443:443 -p 80:80 -v /etc/letsencrypt:/etc/letsencrypt/ certbot/certbot renew --dry-run -d $BASE_URL --logs-dir /etc/letsencrypt/logs
cmd_start_production
}
cmd_process_svg_files() {
docker run -v $PWD/frontend/src/assets/:/app/assets -v $PWD/process_svg.py:/app/process_svg.py -w /app python:3.8-slim-bullseye python process_svg.py
}
# Run the command
cmd="$1"
"cmd_$cmd" "$@"

View file

@ -1,89 +0,0 @@
version: "3.8"
volumes:
righttree-postgres-data:
name: righttree-postgres-data
x-django: &django
image: right-tree
depends_on:
postgres:
condition: service_healthy
env_file: .env
user: "$UID:$GID"
restart: always
services:
backend:
<<: *django
container_name: backend
expose:
- "8000"
command:
- gunicorn
- --bind=0.0.0.0:8000
- right_tree.wsgi
nginx:
container_name: nginx
restart: always
image: nginx
depends_on:
- backend
volumes:
- ./nginx.production.conf:/etc/nginx/nginx.conf
- ./backend/right_tree/staticfiles:/etc/nginx/html/staticfiles
- ./frontend/build:/etc/nginx/html/build
- /etc/letsencrypt:/etc/letsencrypt
ports:
- "80:80"
- "443:443"
postgres:
image: postgis/postgis:13-3.1
restart: always
container_name: postgres
volumes:
- righttree-postgres-data:/var/lib/postgresql/data
- ./create_database.sql:/docker-entrypoint-initdb.d/create_database.sql
ports:
- "5432:5432"
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD", "pg_isready", "--dbname", "righttree", "--username", "righttree"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7.0.8
restart: always
container_name: redis
expose:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
celery:
<<: *django
container_name: celery
command:
- celery
- -A
- right_tree.api
- worker
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: '1'

View file

@ -1,21 +1,35 @@
version: "3.8" #version: "3.8"
volumes: volumes:
righttree-static:
name: righttree-static
external: true
righttree-media:
name: righttree-media
external: true
righttree-postgres-data: righttree-postgres-data:
name: righttree-postgres-data name: righttree-postgres-data
external: true
x-django: &django x-django: &django
image: right-tree image: right-tree
depends_on:
postgres:
condition: service_healthy
volumes:
- ./backend:/app
env_file: .env env_file: .env
user: "$UID:$GID" restart: always
restart: unless-stopped volumes:
- righttree-static:/app/right_tree/staticfiles
- ./backend/right_tree/media:/app/right_tree/media
services: services:
collectstatic:
<<: *django
container_name: collectstatic
command:
- python
- manage.py
- collectstatic
- --noinput
restart: on-failure
backend: backend:
<<: *django <<: *django
container_name: backend container_name: backend
@ -23,58 +37,54 @@ services:
- "8000" - "8000"
command: command:
- gunicorn - gunicorn
- --reload
- --bind=0.0.0.0:8000 - --bind=0.0.0.0:8000
- --timeout=300
- right_tree.wsgi - right_tree.wsgi
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
celery:
condition: service_healthy
collectstatic:
condition: service_completed_successfully
frontend: nginx:
image: node:16-bullseye container_name: nginx
restart: unless-stopped restart: always
container_name: frontend image: nginx
depends_on:
- backend
volumes: volumes:
- ./frontend:/app - ./nginx.production.conf:/etc/nginx/nginx.conf:ro
working_dir: /app - ./backend/right_tree/staticfiles:/etc/nginx/html/staticfiles:ro
user: "$UID:$GID" - ./frontend/build:/etc/nginx/html/build:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
ports: ports:
- 3000:3000 - "80:80"
command: - "443:443"
- npm
- start
postgres: postgres:
image: postgis/postgis:13-3.1 image: postgis/postgis:13-3.1
restart: unless-stopped restart: always
container_name: postgres container_name: postgres
volumes: volumes:
- righttree-postgres-data:/var/lib/postgresql/data - righttree-postgres-data:/var/lib/postgresql/data
- ./create_database.sql:/docker-entrypoint-initdb.d/create_database.sql expose:
ports: - "5432"
- 5432:5432
environment: environment:
POSTGRES_PASSWORD: postgres - POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck: healthcheck:
test: ["CMD", "pg_isready", "--dbname", "righttree", "--username", "righttree"] test: ["CMD", "pg_isready", "--dbname", "$DATABASE_NAME", "--username", "$DATABASE_USER"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
nginx:
image: nginx
restart: unless-stopped
container_name: nginx
depends_on:
- backend
- frontend
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./backend/right_tree/staticfiles:/etc/nginx/html/staticfiles:ro
ports:
- 80:80
redis: redis:
image: redis:7.0.8 image: redis:7.0.10
restart: unless-stopped restart: always
container_name: redis container_name: redis
volumes: volumes:
- ./redis.conf:/usr/local/etc/redis/redis.conf:ro - ./redis.conf:/usr/local/etc/redis/redis.conf:ro
@ -98,8 +108,15 @@ services:
- right_tree.api - right_tree.api
- worker - worker
depends_on: depends_on:
postgres:
condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
healthcheck:
test: ["CMD", "celery", "-A", "right_tree.api", "inspect", "ping"]
interval: 10s
timeout: 5s
retries: 5
deploy: deploy:
resources: resources:
limits: limits:

View file

@ -29,7 +29,7 @@ const StepperWizard = ({children}) => {
<Box sx={{ width: '100%', height: '100%', display: "flex", flexDirection: "column", overflow: "hidden" }}> <Box sx={{ width: '100%', height: '100%', display: "flex", flexDirection: "column", overflow: "hidden" }}>
<Stepper activeStep={step} sx={{ paddingRight: '3vw', paddingLeft: '3vw', marginBottom: '2vw' }}> <Stepper activeStep={step} sx={{ paddingRight: '3vw', paddingLeft: '3vw', marginBottom: '2vw' }}>
{children.map(child => ( {children.map(child => (
<Tooltip title={child.props.tooltip}> <Tooltip title={child.props.tooltip} key={child.props.label}>
<Step key={child.props.label}> <Step key={child.props.label}>
<StepLabel>{child.props.label}</StepLabel> <StepLabel>{child.props.label}</StepLabel>
</Step> </Step>

View file

@ -28,9 +28,5 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
} }
location ~* \.(eot|otf|ttf|woff|woff2)$ {
add_header Access-Control-Allow-Origin *;
}
} }
} }

View file

@ -4,22 +4,25 @@ http {
server_name _; server_name _;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
server { server {
listen 443 ssl; listen 443 ssl;
index index.html; index index.html;
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
ssl_certificate /etc/letsencrypt/live/rightplant.biospherecapital.com/fullchain.pem; ssl_certificate /etc/letsencrypt/live/rightplant.nz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rightplant.biospherecapital.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/rightplant.nz/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/rightplant.biospherecapital.com/chain.pem; ssl_trusted_certificate /etc/letsencrypt/live/rightplant.nz/chain.pem;
location / { location / {
root /etc/nginx/html/build; root /etc/nginx/html/build;
index index.html;
try_files $uri /index.html;
} }
location /staticfiles { location /staticfiles {
root /etc/nginx/html/; root /etc/nginx/html/;
} }
location ~* ^/(api|admin) { location ~* ^/(api|admin) {

View file

@ -1 +1 @@
requirepass "redis" requirepass "9zWMuCgbPaLNZhu8"