diff --git a/.dockerignore b/.dockerignore index 04fa458..04e9753 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,7 @@ src/django_project/*.sqlite3 src/django_project/*.log src/django_project/local_test src/django_project/tests + +src/django_project/core/static/node_modules +src/django_project/core/static/package.json +src/django_project/core/static/package-lock.json diff --git a/pyproject.toml b/pyproject.toml index 117b450..b5943eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,17 +2,16 @@ dependencies = [ "django~=5.2", "django-braces", + "django-debug-toolbar", "django-environ", "django-extensions", "django-filter", - "django-handyhelpers", + "django-handyhelpers==0.3.41", "djangorestframework", "djangorestframework-filters==1.0.0.dev0", "drf-flex-fields", "drf-spectacular", "drf-renderer-xlsx", - "psycopg2-binary", - "whitenoise", ] description = "Spokane Python Community" dynamic = ["version"] @@ -27,7 +26,6 @@ requires-python = ">=3.10" dev = [ "bandit", "coveralls", - "django-debug-toolbar", "faker", "isort", "model-bakery", @@ -43,6 +41,11 @@ dev = [ "types-requests", "typing_extensions", ] +docker = [ + "gunicorn", + "psycopg2-binary", + "whitenoise", +] [tool.bandit] diff --git a/src/django_project/core/settings.py b/src/django_project/core/settings.py index 558107a..34856d4 100644 --- a/src/django_project/core/settings.py +++ b/src/django_project/core/settings.py @@ -38,6 +38,8 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env.bool("DEBUG", True) +DEPLOYMENT_ENV = env.str("DEPLOYMENT_ENV", "local") + ALLOWED_HOSTS = env.str("ALLOWED_HOSTS", "127.0.0.1").split(",") INTERNAL_IPS = env.str("INTERNAL_IPS", "127.0.0.1").split(",") @@ -52,7 +54,6 @@ "django.contrib.messages", "django.contrib.staticfiles", # third party apps - "debug_toolbar", "django_extensions", "django_filters", "drf_spectacular", @@ -66,14 +67,22 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "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", - "debug_toolbar.middleware.DebugToolbarMiddleware", ] +if DEPLOYMENT_ENV in ["local", "dev"]: + INSTALLED_APPS += [ + "debug_toolbar", # pragma: no cover + ] + MIDDLEWARE += [ + "debug_toolbar.middleware.DebugToolbarMiddleware", # pragma: no cover + ] + ROOT_URLCONF = "core.urls" diff --git a/src/django_project/core/static/package-lock.json b/src/django_project/core/static/package-lock.json deleted file mode 100644 index 5ff4674..0000000 --- a/src/django_project/core/static/package-lock.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "static", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "bootstrap": "^5.3.7" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/bootstrap": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", - "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - } - } -} diff --git a/src/django_project/core/urls/urls.py b/src/django_project/core/urls/urls.py index 6adc42b..4510b43 100644 --- a/src/django_project/core/urls/urls.py +++ b/src/django_project/core/urls/urls.py @@ -41,7 +41,7 @@ path("", include("web.urls", namespace="web")), ] -if settings.DEBUG: +if settings.DEBUG and settings.DEPLOYMENT_ENV in ["local", "dev"]: urlpatterns.append( path("__debug__/", include("debug_toolbar.urls")), ) # pragma: no cover diff --git a/src/django_project/entrypoint.sh b/src/django_project/entrypoint.sh new file mode 100755 index 0000000..ee256d5 --- /dev/null +++ b/src/django_project/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# Exit immediately if a command exits with a non-zero status +set -e + +# Function to wait for the database to be ready +wait_for_db() { + echo "Waiting for database to be ready..." + until nc -z "$DB_HOST" "$DB_PORT"; do + echo "Database is unavailable - sleeping" + sleep 1 + done + echo "Database is up - continuing" +} + +# Wait for the database to be ready +wait_for_db + +# Run migrations +echo "Running migrations..." +python manage.py migrate + +# Create superuser +echo "Creating superuser..." +python manage.py add_superuser --username $DJANGO_ADMIN_USERNAME --group admin --password $DJANGO_ADMIN_PASSWORD + +# Collect static files +echo "Collecting static files..." +python manage.py collectstatic --noinput + +# Launch gunicorn +gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/src/django_project/web/migrations/0001_initial.py b/src/django_project/web/migrations/0001_initial.py index eb97ee0..c4ce6ec 100644 --- a/src/django_project/web/migrations/0001_initial.py +++ b/src/django_project/web/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.4 on 2025-07-27 20:48 +# Generated by Django 5.2.4 on 2025-07-29 22:16 import django.db.models.deletion from django.db import migrations, models @@ -21,6 +21,7 @@ class Migration(migrations.Migration): ("end_date_time", models.DateTimeField()), ("location", models.CharField(blank=True, max_length=256, null=True)), ("description", models.TextField(blank=True, null=True)), + ("url", models.URLField(blank=True, null=True)), ], options={ "ordering": ["-start_date_time"], @@ -37,7 +38,7 @@ class Migration(migrations.Migration): ("title", models.CharField(max_length=128)), ("description", models.TextField()), ( - "target_audience", + "skill_level", models.CharField( choices=[ ("all", "All Levels"), diff --git a/src/django_project/web/migrations/0002_event_url.py b/src/django_project/web/migrations/0002_event_url.py deleted file mode 100644 index 9ef1cee..0000000 --- a/src/django_project/web/migrations/0002_event_url.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-28 00:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("web", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="event", - name="url", - field=models.URLField(blank=True, null=True), - ), - ] diff --git a/src/django_project/web/migrations/0003_rename_target_audience_presentationrequest_skill_level.py b/src/django_project/web/migrations/0003_rename_target_audience_presentationrequest_skill_level.py deleted file mode 100644 index 93a5b99..0000000 --- a/src/django_project/web/migrations/0003_rename_target_audience_presentationrequest_skill_level.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.4 on 2025-07-29 16:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("web", "0002_event_url"), - ] - - operations = [ - migrations.RenameField( - model_name="presentationrequest", - old_name="target_audience", - new_name="skill_level", - ), - ] diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile new file mode 100644 index 0000000..d7e3c67 --- /dev/null +++ b/src/docker/Dockerfile @@ -0,0 +1,45 @@ +# === STAGE 1: Build dependencies in a virtual environment === +FROM python:3.12-alpine AS builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache \ + gcc \ + musl-dev \ + libffi-dev \ + openssl-dev + +COPY pyproject.toml /app/ + +# Install pip-tools and create a virtual environment +RUN pip install --upgrade pip +RUN python -m venv /venv +ENV PATH="/venv/bin:$PATH" +RUN pip install .[docker] + + +# === STAGE 2: Runtime with only necessary files === +FROM python:3.12-alpine + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Install minimal runtime dependencies +RUN apk add --no-cache libffi openssl libstdc++ + +# Copy the virtual environment from the builder stage +COPY --from=builder /venv /venv + +# Add the virtual environment's Python to the PATH +ENV PATH="/venv/bin:$PATH" + +# Copy Django project code +COPY src/django_project /app/ +RUN chmod +x /app/entrypoint.sh + +EXPOSE 8000 + +CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"] \ No newline at end of file diff --git a/src/docker/docker-compose.yaml b/src/docker/docker-compose.yaml new file mode 100644 index 0000000..21d4496 --- /dev/null +++ b/src/docker/docker-compose.yaml @@ -0,0 +1,31 @@ +version: '3.9' + +services: + django: + image: spokanepython-django:latest + container_name: django + build: + context: ../.. + dockerfile: src/docker/Dockerfile + env_file: + - ../envs/.env.docker-compose + command: ./entrypoint.sh + ports: + - "8000:8000" + depends_on: + - db + restart: unless-stopped + + db: + image: postgres:17 + container_name: postgres + volumes: + - spokanepython_postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + env_file: + - ../envs/.env.docker-compose + restart: unless-stopped + +volumes: + spokanepython_postgres: diff --git a/src/envs/.env.template b/src/envs/.env.template index 380972d..353dcbb 100644 --- a/src/envs/.env.template +++ b/src/envs/.env.template @@ -10,4 +10,3 @@ PROJECT_NAME="myproject" PROJECT_DESCRIPTION="my super awesome project powered, in part, by amazing code provided by DjangoAddicts." PROJECT_VERSION="0.0.1" PROJECT_SOURCE="https://github.com/djangoaddicts" -PYGWALKER_THEME="light"