diff --git a/compose.test.yaml b/.docker-compose-files/compose.check.yaml
similarity index 74%
rename from compose.test.yaml
rename to .docker-compose-files/compose.check.yaml
index cd66188..99a70d9 100644
--- a/compose.test.yaml
+++ b/.docker-compose-files/compose.check.yaml
@@ -1,11 +1,12 @@
services:
web:
- command: python -Wa manage.py test --noinput --parallel
+ command: python manage.py check --deploy
restart: "no"
env_file:
- path: .env.template
required: true
+
db:
env_file:
- path: .env.template
diff --git a/compose.dev.yaml b/.docker-compose-files/compose.dev.yaml
similarity index 71%
rename from compose.dev.yaml
rename to .docker-compose-files/compose.dev.yaml
index f5920c2..a47c7c6 100644
--- a/compose.dev.yaml
+++ b/.docker-compose-files/compose.dev.yaml
@@ -1,6 +1,6 @@
services:
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:
- ./src:/src
env_file:
diff --git a/compose.prod.yaml b/.docker-compose-files/compose.prod.yaml
similarity index 100%
rename from compose.prod.yaml
rename to .docker-compose-files/compose.prod.yaml
diff --git a/.docker-compose-files/compose.test.yaml b/.docker-compose-files/compose.test.yaml
new file mode 100644
index 0000000..6609ef5
--- /dev/null
+++ b/.docker-compose-files/compose.test.yaml
@@ -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
+
diff --git a/.gitea/workflows/build_test.yaml b/.gitea/workflows/build_test.yaml
index f71de77..9434cd8 100644
--- a/.gitea/workflows/build_test.yaml
+++ b/.gitea/workflows/build_test.yaml
@@ -3,7 +3,6 @@ run-name: ${{ gitea.actor }}
on:
push:
branches:
- - pre-prod
- main
pull_request:
@@ -16,3 +15,6 @@ jobs:
- name: Build and test
run: make test
+
+ - name: Build
+ run: make prod
\ No newline at end of file
diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml
new file mode 100644
index 0000000..8e2eb60
--- /dev/null
+++ b/.gitea/workflows/test.yaml
@@ -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
\ No newline at end of file
diff --git a/.nginx/.conf b/.nginx/.conf
new file mode 100644
index 0000000..d3c3d9d
--- /dev/null
+++ b/.nginx/.conf
@@ -0,0 +1,3 @@
+events {}
+
+http {}
diff --git a/.nginx/.templates/nginx.conf.template b/.nginx/.templates/nginx.conf.template
new file mode 100644
index 0000000..a67310b
--- /dev/null
+++ b/.nginx/.templates/nginx.conf.template
@@ -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;
+ }
+}
diff --git a/Makefile b/Makefile
index c61f5ba..cfeaf72 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,26 @@
prod:
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:
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
+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:
- 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
+
diff --git a/README.md b/README.md
index abd0593..f5e917b 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,65 @@
# Quatsh-Website
-## Requirements:
+## Requirements
### Install make
+
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: \
+
```
choco install make
```
### Install docker
-https://docs.docker.com/get-started/get-docker/
+
+
## Running for the first time
+
### Env
+
Before running the container environment has to be setup first, so first run:
+
```
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
-#### 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.
-```
+Properly utilising this also requires the DEBUG environment variable to be set to TRUE.
+
+```shell
make dev
-```
+```
#### Testing
+
Runs the testing environment and exits.
-```
+
+```shell
make test
```
#### Production
+
Using `make prod` is identical but preferred here.
-```
+
+```shell
make prod
-# or
+```
+
+Or
+
+```shell
make
-```
\ No newline at end of file
+```
+
diff --git a/compose.yaml b/compose.yaml
index 4c14151..eda301a 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,16 +1,21 @@
services:
web:
build: .
- command: gunicorn --proxy-protocol auto --proxy-allow-from 10.10.50.11 website.wsgi:application
+ command: gunicorn website.wsgi:application
depends_on:
- db
- ports:
- - 8000:8000
environment:
+ ALLOWED_HOSTS: ${NGINX_HOSTNAME}
PYTHONDONTWRITEBYTECODE: 1
PYTHONUNBUFFERED: 1
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE}
+ VIRTUAL_HOST: localhost
+ #VIRTUAL_PORT: 8000
restart: unless-stopped
+ volumes:
+ - static_volume:/app/static
+ - media_volume:/app/media
+
db:
image: postgres:18
@@ -30,6 +35,23 @@ services:
ports:
- 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:
build: .
command: python manage.py test
@@ -41,3 +63,5 @@ services:
volumes:
postgres_data:
+ static_volume:
+ media_volume:
diff --git a/requirements.txt b/requirements.txt
index d340205..daa4177 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,5 @@
Django==6.0.3
gunicorn==25.1.0
psycopg [binary] ==3.3.3
+django-browser-reload
+django-watchfiles
diff --git a/src/polls/admin.py b/src/polls/admin.py
index 8c38f3f..d0e6a57 100644
--- a/src/polls/admin.py
+++ b/src/polls/admin.py
@@ -1,3 +1,7 @@
from django.contrib import admin
# Register your models here.
+
+from .models import Question
+
+admin.site.register(Question)
diff --git a/src/polls/migrations/0001_initial.py b/src/polls/migrations/0001_initial.py
new file mode 100644
index 0000000..65683f4
--- /dev/null
+++ b/src/polls/migrations/0001_initial.py
@@ -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')),
+ ],
+ ),
+ ]
diff --git a/src/polls/models.py b/src/polls/models.py
index 71a8362..76e8101 100644
--- a/src/polls/models.py
+++ b/src/polls/models.py
@@ -1,3 +1,20 @@
from django.db import models
# 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
diff --git a/src/polls/templates/polls/detail.html b/src/polls/templates/polls/detail.html
new file mode 100644
index 0000000..d657719
--- /dev/null
+++ b/src/polls/templates/polls/detail.html
@@ -0,0 +1,11 @@
+
+
+
+
+ My test page
+
+
+ Hey This is a test of watchfiles + browser reload!
+ {{ question }}
+
+
diff --git a/src/polls/templates/polls/index.html b/src/polls/templates/polls/index.html
new file mode 100644
index 0000000..ccd78c2
--- /dev/null
+++ b/src/polls/templates/polls/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ My test page
+
+
+ {% if latest_question_list %}
+
+ {% else %}
+ No polls are available.
+ {% endif %}
+
+
+
diff --git a/src/polls/urls.py b/src/polls/urls.py
index 5119061..fc78e3e 100644
--- a/src/polls/urls.py
+++ b/src/polls/urls.py
@@ -3,5 +3,12 @@ from django.urls import path
from . import views
urlpatterns = [
+ # ex: /polls/
path("", views.index, name="index"),
+ # ex: /polls/5/
+ path("/", views.detail, name="detail"),
+ # ex: /polls/5/results/
+ path("/results/", views.results, name="results"),
+ # ex: /polls/5/vote/
+ path("/vote/", views.vote, name="vote"),
]
diff --git a/src/polls/views.py b/src/polls/views.py
index 933bf0d..5e8ef07 100644
--- a/src/polls/views.py
+++ b/src/polls/views.py
@@ -1,9 +1,24 @@
-from django.shortcuts import render
-from django.http import HttpResponse
+from django.http import HttpResponse, Http404
+from django.shortcuts import get_object_or_404, render
+
+from .models import Question
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)
diff --git a/src/website/settings.py b/src/website/settings.py
index 0264fb5..b12274a 100644
--- a/src/website/settings.py
+++ b/src/website/settings.py
@@ -24,21 +24,24 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
# 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")
# Application definition
INSTALLED_APPS = [
+ "polls.apps.PollsConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
+ "django_browser_reload",
+ "django_watchfiles",
]
MIDDLEWARE = [
@@ -49,6 +52,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "django_browser_reload.middleware.BrowserReloadMiddleware",
]
ROOT_URLCONF = "website.urls"
@@ -120,4 +124,5 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
+STATIC_ROOT = "/app/static/"
STATIC_URL = "static/"
diff --git a/src/website/urls.py b/src/website/urls.py
index 37f07e2..08481ca 100644
--- a/src/website/urls.py
+++ b/src/website/urls.py
@@ -19,6 +19,7 @@ from django.contrib import admin
from django.urls import path, include
urlpatterns = [
+ path("__reload__/", include("django_browser_reload.urls")),
path("polls/", include("polls.urls")),
path("admin/", admin.site.urls),
]