Merge pull request 'pre-prod' (#17) from pre-prod into main
Some checks failed
Gitea build & Test. / tests (push) Failing after 6s
Deploy to production / deploy (push) Failing after 1m19s

Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2026-03-26 08:17:36 +00:00
21 changed files with 285 additions and 26 deletions

View File

@@ -1,11 +1,12 @@
services: services:
web: web:
command: python -Wa manage.py test --noinput --parallel command: python manage.py check --deploy
restart: "no" restart: "no"
env_file: env_file:
- path: .env.template - path: .env.template
required: true required: true
db: db:
env_file: env_file:
- path: .env.template - path: .env.template

View File

@@ -1,6 +1,6 @@
services: services:
web: web:
command: gunicorn -b 0.0.0.0:8000 website.wsgi:application command: gunicorn --capture-output --enable-stdio-inheritance -b 0.0.0.0:8000 website.wsgi:application
volumes: volumes:
- ./src:/src - ./src:/src
env_file: env_file:

View File

@@ -0,0 +1,14 @@
services:
web:
command: python manage.py check --deploy; python -Wa manage.py test --noinput --parallel
restart: "no"
env_file:
- path: .env.template
required: true
db:
env_file:
- path: .env.template
required: true

View File

@@ -3,7 +3,6 @@ run-name: ${{ gitea.actor }}
on: on:
push: push:
branches: branches:
- pre-prod
- main - main
pull_request: pull_request:
@@ -16,3 +15,6 @@ jobs:
- name: Build and test - name: Build and test
run: make test run: make test
- name: Build
run: make prod

View File

@@ -0,0 +1,17 @@
name: Gitea Test.
run-name: ${{ gitea.actor }}
on:
push:
branches:
- pre-prod
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: test
run: make test

3
.nginx/.conf Normal file
View File

@@ -0,0 +1,3 @@
events {}
http {}

View File

@@ -0,0 +1,49 @@
server {
listen ${NGINX_PORT};
server_name ${NGINX_HOSTNAME};
location /static/ {
alias /app/static/;
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
alias /app/media;
expires 30d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen ${NGINX_SSL_PORT};
server_name ${NGINX_HOSTNAME};
location /static/ {
alias /app/static/;
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
alias /app/media;
expires 30d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -1,11 +1,26 @@
prod: prod:
docker compose down docker compose down
docker compose -f ./compose.yaml -f ./compose.prod.yaml up -d --build docker compose --env-file .env -f ./compose.yaml -f ./.docker-compose-files/compose.prod.yaml up -d --build
docker exec quatsh-website-web-1 python manage.py collectstatic --noinput
docker exec quatsh-website-web-1 python manage.py check --deploy
docker exec quatsh-website-web-1 python manage.py migrate
dev: dev:
docker compose down docker compose down
docker compose -f ./compose.yaml -f ./compose.dev.yaml up --build -d docker compose -f ./compose.yaml -f ./.docker-compose-files/compose.dev.yaml up --build -d
docker exec quatsh-website-web-1 python manage.py collectstatic --noinput
docker exec -it quatsh-website-web-1 sh docker exec -it quatsh-website-web-1 sh
dev_restart:
docker compose down
docker compose -f ./compose.yaml -f ./.docker-compose-files/compose.dev.yaml up -d
docker exec -it quatsh-website-web-1 sh
dev_restart_with_logs:
docker compose down
docker compose -f ./compose.yaml -f ./.docker-compose-files/compose.dev.yaml up
test: test:
docker compose --env-file .env.template -f ./compose.yaml -f ./compose.test.yaml up --build --abort-on-container-exit --exit-code-from web docker compose --env-file .env.template -f ./compose.yaml -f ./.docker-compose-files/compose.test.yaml up --build --abort-on-container-exit --exit-code-from web

View File

@@ -1,44 +1,65 @@
# Quatsh-Website # Quatsh-Website
## Requirements: ## Requirements
### Install make ### Install make
Running this requires the use of "make" and docker. Running this requires the use of "make" and docker.
"make" tends to come pre-installed on linux but not on Windows, to install it on Windows I recommend using Chocolatey as follows: \ "make" tends to come pre-installed on linux but not on Windows, to install it on Windows I recommend using Chocolatey as follows: \
``` ```
choco install make choco install make
``` ```
### Install docker ### Install docker
https://docs.docker.com/get-started/get-docker/
<https://docs.docker.com/get-started/get-docker/>
## Running for the first time ## Running for the first time
### Env ### Env
Before running the container environment has to be setup first, so first run: Before running the container environment has to be setup first, so first run:
``` ```
cp .env.template .env cp .env.template .env
``` ```
Then make any nessecary adjustments to the .env file.
### Running the website: Then make any changes to the .env file.
### Running the website
To run the website 3 options have been provided To run the website 3 options have been provided
#### Development:
#### Development
For development purposes only as the src folder has been mounted in the container to allow for the making of changes without rebuilding the entire image. For development purposes only as the src folder has been mounted in the container to allow for the making of changes without rebuilding the entire image.
``` Properly utilising this also requires the DEBUG environment variable to be set to TRUE.
```shell
make dev make dev
``` ```
#### Testing #### Testing
Runs the testing environment and exits. Runs the testing environment and exits.
```
```shell
make test make test
``` ```
#### Production #### Production
Using `make prod` is identical but preferred here. Using `make prod` is identical but preferred here.
```
```shell
make prod make prod
# or ```
Or
```shell
make make
``` ```

View File

@@ -1,16 +1,21 @@
services: services:
web: web:
build: . build: .
command: gunicorn --proxy-protocol auto --proxy-allow-from 10.10.50.11 website.wsgi:application command: gunicorn website.wsgi:application
depends_on: depends_on:
- db - db
ports:
- 8000:8000
environment: environment:
ALLOWED_HOSTS: ${NGINX_HOSTNAME}
PYTHONDONTWRITEBYTECODE: 1 PYTHONDONTWRITEBYTECODE: 1
PYTHONUNBUFFERED: 1 PYTHONUNBUFFERED: 1
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE} DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE}
VIRTUAL_HOST: localhost
#VIRTUAL_PORT: 8000
restart: unless-stopped restart: unless-stopped
volumes:
- static_volume:/app/static
- media_volume:/app/media
db: db:
image: postgres:18 image: postgres:18
@@ -30,6 +35,23 @@ services:
ports: ports:
- 8080:8080 - 8080:8080
proxy:
image: nginx:stable
volumes:
- ./.nginx/.templates:/etc/nginx/templates
- static_volume:/app/static:ro
- media_volume:/app/media:ro
restart: unless-stopped
ports:
- 80:80
- 443:443
environment:
- NGINX_HOSTNAME=${NGINX_HOSTNAME}
- NGINX_PORT=80
- NGINX_SSL_PORT=443
depends_on:
- web
test: test:
build: . build: .
command: python manage.py test command: python manage.py test
@@ -41,3 +63,5 @@ services:
volumes: volumes:
postgres_data: postgres_data:
static_volume:
media_volume:

View File

@@ -1,3 +1,5 @@
Django==6.0.3 Django==6.0.3
gunicorn==25.1.0 gunicorn==25.1.0
psycopg [binary] ==3.3.3 psycopg [binary] ==3.3.3
django-browser-reload
django-watchfiles

View File

@@ -1,3 +1,7 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. # Register your models here.
from .models import Question
admin.site.register(Question)

View File

@@ -0,0 +1,32 @@
# Generated by Django 6.0.3 on 2026-03-25 14:26
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question_text', models.CharField(max_length=200)),
('pub_date', models.DateTimeField(verbose_name='date published')),
],
),
migrations.CreateModel(
name='Choice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=200)),
('votes', models.IntegerField(default=0)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')),
],
),
]

View File

@@ -1,3 +1,20 @@
from django.db import models from django.db import models
# Create your models here. # Create your models here.
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>My test page</title>
</head>
<body>
<h1>Hey This is a test of watchfiles + browser reload!</h1>
{{ question }}
</body>
</html>

View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>My test page</title>
</head>
<body>
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
</body>
</html>

View File

@@ -3,5 +3,12 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
# ex: /polls/
path("", views.index, name="index"), path("", views.index, name="index"),
# ex: /polls/5/
path("<int:question_id>/", views.detail, name="detail"),
# ex: /polls/5/results/
path("<int:question_id>/results/", views.results, name="results"),
# ex: /polls/5/vote/
path("<int:question_id>/vote/", views.vote, name="vote"),
] ]

View File

@@ -1,9 +1,24 @@
from django.shortcuts import render from django.http import HttpResponse, Http404
from django.http import HttpResponse from django.shortcuts import get_object_or_404, render
from .models import Question
def index(request): def index(request):
return HttpResponse("Hello, world. You're at the polls index.") latest_question_list = Question.objects.order_by("-pub_date")[:5]
context = {"latest_question_list": latest_question_list}
return render(request, "polls/index.html", context)
# Create your views here. def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, "polls/detail.html", {"question": question})
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)

View File

@@ -24,21 +24,24 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(os.getenv("DEBUG", False)) DEBUG = os.getenv("DEBUG") == "TRUE"
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1").split(",")
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"polls.apps.PollsConfig",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django_browser_reload",
"django_watchfiles",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -49,6 +52,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_browser_reload.middleware.BrowserReloadMiddleware",
] ]
ROOT_URLCONF = "website.urls" ROOT_URLCONF = "website.urls"
@@ -120,4 +124,5 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/ # https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_ROOT = "/app/static/"
STATIC_URL = "static/" STATIC_URL = "static/"

View File

@@ -19,6 +19,7 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
urlpatterns = [ urlpatterns = [
path("__reload__/", include("django_browser_reload.urls")),
path("polls/", include("polls.urls")), path("polls/", include("polls.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
] ]