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), ]