Compare commits
85 Commits
596e3fcfca
...
pre-prod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
255cb9bc2f | ||
|
|
d8800daba3 | ||
|
|
5fd943db85 | ||
|
|
00db9182d9 | ||
|
|
3f52db35c2 | ||
|
|
16c1913e69 | ||
|
|
5490593b94 | ||
| c4785455eb | |||
| b463e5b18b | |||
|
|
76c0cf3e74 | ||
|
|
68689ea885 | ||
| 74ee2f1965 | |||
| 048828f2d2 | |||
| f6293b7a0c | |||
| e0675c8684 | |||
| 4b18769137 | |||
| 90b7bb0dee | |||
| 8cfe5c6f9b | |||
| 9397c2d4f2 | |||
| 313702f9fe | |||
|
|
49199b7184 | ||
|
|
c17e3e85f2 | ||
|
|
be54afa093 | ||
|
|
fd5414bd5a | ||
|
|
6b635955eb | ||
|
|
0610053a3e | ||
| 79560c5835 | |||
| c1aa69dd8b | |||
| d17b2f0a2a | |||
| 41a61fb318 | |||
| cba945375c | |||
|
|
1c545638b9 | ||
|
|
5fa195a0dc | ||
|
|
f8dc8fc684 | ||
| df587931b5 | |||
|
|
01ea42b64a | ||
|
|
fe6c0df123 | ||
| 357c81ff08 | |||
|
|
26c6f46531 | ||
|
|
13a317d3df | ||
| 026bce1f16 | |||
| b1f44f2ce0 | |||
| 761227e05e | |||
| c165c03579 | |||
| badf0dde7a | |||
| b1f25ebcc3 | |||
| 01c8640d38 | |||
| f511599273 | |||
| bac4c56485 | |||
| e1a6e4f21f | |||
| 2f1102bebc | |||
| f24bf52136 | |||
| 0f98959d37 | |||
| e99f44d8ad | |||
| 5b9d70db22 | |||
| ac4e92aa07 | |||
| b94c208798 | |||
| f293a34923 | |||
| 56c54c973f | |||
| 3984dfb058 | |||
| bcb72b42e1 | |||
| 86c3cfe426 | |||
|
|
7ecd65b9c2 | ||
|
|
8e4e2b8434 | ||
|
|
dda1b29626 | ||
|
|
d999fece6f | ||
|
|
cbf5e4b503 | ||
|
|
9959272051 | ||
|
|
3fca18e000 | ||
|
|
0f57d9f32b | ||
|
|
518364faa9 | ||
|
|
b680c1ab5f | ||
| 27257a2977 | |||
|
|
8049cb2e62 | ||
|
|
600baaee80 | ||
| 14f2665070 | |||
|
|
acc116a6c1 | ||
|
|
c9eb6d52be | ||
|
|
6511120965 | ||
| b25196545f | |||
|
|
9e8d43e210 | ||
|
|
5160a06aa2 | ||
|
|
9b57180e20 | ||
|
|
d359cb9780 | ||
|
|
860b0ef033 |
14
.docker-compose-files/compose.check.yaml
Normal file
14
.docker-compose-files/compose.check.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
web:
|
||||
command: python manage.py check --deploy
|
||||
restart: "no"
|
||||
env_file:
|
||||
- path: .env.template
|
||||
required: true
|
||||
|
||||
|
||||
db:
|
||||
env_file:
|
||||
- path: .env.template
|
||||
required: true
|
||||
|
||||
18
.docker-compose-files/compose.dev.yaml
Normal file
18
.docker-compose-files/compose.dev.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
web:
|
||||
command: gunicorn --capture-output --enable-stdio-inheritance -b 0.0.0.0:8000 website.wsgi:application
|
||||
volumes:
|
||||
- ./src:/src
|
||||
env_file:
|
||||
- path: .env.template
|
||||
required: true
|
||||
- path: .env
|
||||
required: false
|
||||
|
||||
db:
|
||||
env_file:
|
||||
- path: .env.template
|
||||
required: true
|
||||
- path: .env
|
||||
required: false
|
||||
|
||||
10
.docker-compose-files/compose.prod.yaml
Normal file
10
.docker-compose-files/compose.prod.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
web:
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
|
||||
db:
|
||||
env_file:
|
||||
- path: .env
|
||||
required: true
|
||||
14
.docker-compose-files/compose.test.yaml
Normal file
14
.docker-compose-files/compose.test.yaml
Normal 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
|
||||
|
||||
20
.env.template
Normal file
20
.env.template
Normal file
@@ -0,0 +1,20 @@
|
||||
#NGINX
|
||||
NGINX_HOSTNAME=localhost
|
||||
|
||||
# Django
|
||||
DJANGO_SETTINGS_MODULE=website.settings
|
||||
DJANGO_SECRET_KEY=CWHZCAZBNV57tDkwGHJwTUu3PCSnGG45
|
||||
DEBUG=TRUE
|
||||
#ALLOWED_HOSTS=localhost
|
||||
|
||||
# Database (PostgreSQL)
|
||||
POSTGRES_USER=test_user
|
||||
POSTGRES_PASSWORD=test_password
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DBNAME=test_dbname # == POSTGRES_DB
|
||||
POSTGRES_DB=test_db
|
||||
|
||||
# Gunicorn
|
||||
GUNICORN_WORKERS=3
|
||||
GUNICORN_TIMEOUT=120
|
||||
38
.gitea/workflows/deploy.yml
Normal file
38
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Deploy to production
|
||||
run-name: deploy-${{ gitea.actor }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_rsa ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF'
|
||||
cd ~/Quatsh-Website
|
||||
|
||||
echo "Pulling latest code..."
|
||||
git pull origin main
|
||||
|
||||
echo "Building containers..."
|
||||
make prod
|
||||
|
||||
echo "Running Django deploy check"
|
||||
docker compose run --rm web python manage.py check --deploy
|
||||
|
||||
echo "Deployment complete."
|
||||
EOF
|
||||
18
.gitea/workflows/test.yaml
Normal file
18
.gitea/workflows/test.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Gitea Test.
|
||||
run-name: ${{ gitea.actor }}
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- pre-prod
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: test
|
||||
run: make test
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -136,6 +136,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
!.env.template
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@@ -174,3 +175,7 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# gunicon webserver
|
||||
gunicorn.ctl
|
||||
|
||||
|
||||
|
||||
3
.nginx/.conf
Normal file
3
.nginx/.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
events {}
|
||||
|
||||
http {}
|
||||
49
.nginx/.templates/nginx.conf.template
Normal file
49
.nginx/.templates/nginx.conf.template
Normal 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;
|
||||
}
|
||||
}
|
||||
19
Dockerfile
19
Dockerfile
@@ -1,18 +1,15 @@
|
||||
FROM python:3.14-alpine
|
||||
|
||||
RUN mkdir /app
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /src
|
||||
|
||||
RUN pip install --upgrade pip
|
||||
COPY requirements.txt /src/
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY requirements.txt /app/
|
||||
COPY gunicorn.conf.py /app/
|
||||
COPY ./app/ /app/
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
COPY gunicorn.conf.py /src/
|
||||
COPY ./src/ /src/
|
||||
|
||||
CMD ["gunicorn", "website.wsgi:application"]
|
||||
|
||||
|
||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
||||
.SHELLFLAGS := -ec
|
||||
|
||||
prod:
|
||||
docker compose down
|
||||
docker compose --env-file .env -f ./compose.yaml -f ./.docker-compose-files/compose.prod.yaml up -d --build
|
||||
docker compose exec web python manage.py collectstatic --noinput
|
||||
docker compose exec web python manage.py check --deploy
|
||||
docker compose exec web python manage.py migrate
|
||||
|
||||
dev:
|
||||
docker compose down
|
||||
docker compose -f ./compose.yaml -f ./.docker-compose-files/compose.dev.yaml up --build -d
|
||||
docker compose exec web python manage.py collectstatic --noinput
|
||||
docker compose exec -it web sh
|
||||
|
||||
dev_restart:
|
||||
docker compose down
|
||||
docker compose -f ./compose.yaml -f ./.docker-compose-files/compose.dev.yaml up -d
|
||||
docker compose exec -it web 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 ./.docker-compose-files/compose.test.yaml up --build --abort-on-container-exit --exit-code-from web
|
||||
|
||||
68
README.md
68
README.md
@@ -1,2 +1,70 @@
|
||||
# Quatsh-Website
|
||||
|
||||
## 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~
|
||||
|
||||
```shell
|
||||
choco install make
|
||||
```
|
||||
|
||||
EDIT: I've swapped over to using [[scoop](https://github.com/ScoopInstaller/Scoop#readme)] for these kinds of installations.
|
||||
|
||||
```shell
|
||||
scoop 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 changes to the .env file.
|
||||
|
||||
### Running the website
|
||||
|
||||
To run the website 3 options have been provided
|
||||
|
||||
#### 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
|
||||
|
||||
```shell
|
||||
make
|
||||
```
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
Django settings for website project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-c$q7wdq+u@ow74wp!&zzkxdylkueu)(+34e%!e0du&bjwoqz9z'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'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',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'website.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'website.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
74
compose.yaml
74
compose.yaml
@@ -1,9 +1,75 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- 8000:8000
|
||||
command: gunicorn website.wsgi:application
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
environment:
|
||||
- PYTHONDONTWRITEBYTECODE=1
|
||||
- PYTHONUNBUFFERED=1
|
||||
ALLOWED_HOSTS: ${NGINX_HOSTNAME}
|
||||
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE}
|
||||
VIRTUAL_HOST: localhost
|
||||
#VIRTUAL_PORT: 8000
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- static_volume:/app/static
|
||||
- media_volume:/app/media
|
||||
networks:
|
||||
- frontend
|
||||
- backend
|
||||
|
||||
|
||||
db:
|
||||
image: postgres:18
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DBNAME}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DBNAME}"]
|
||||
interval: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- backend
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
depends_on:
|
||||
- db
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8080:8080
|
||||
networks:
|
||||
- backend
|
||||
|
||||
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
|
||||
networks:
|
||||
- frontend
|
||||
|
||||
networks:
|
||||
frontend:
|
||||
backend:
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
static_volume:
|
||||
media_volume:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
services:
|
||||
web:
|
||||
volumes:
|
||||
- ./app:/app
|
||||
|
||||
24
docs/ROADMAP.md
Normal file
24
docs/ROADMAP.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Goals
|
||||
|
||||
This document contains the wanted feature list and their current state.
|
||||
|
||||
## Basic Pages
|
||||
|
||||
-[] Homepage
|
||||
-[] Boards
|
||||
-[] Committee page
|
||||
-[] Contact page
|
||||
-[] Donator page/Friends of Quatsh
|
||||
-[] Membership page
|
||||
|
||||
## Main Features
|
||||
|
||||
-[] Signup form with email SMTP link.
|
||||
-[] Gallery
|
||||
-[] Login page
|
||||
|
||||
## Extra's
|
||||
|
||||
-[] Allow google-workspace login with SSO?
|
||||
-[] Add Redis for Caching
|
||||
-[] Setup SMTP with automatic switching to in-mem email saving OR print to stdout when in DEBUG mode.
|
||||
177
docs/architecture/decisions.md
Normal file
177
docs/architecture/decisions.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Architectural Decision Records
|
||||
|
||||
This document records the major architectural decisions made during the development of the Quatsh website, including the context and reasoning behind each choice. Future maintainers should add to this document when making significant architectural decisions.
|
||||
|
||||
---
|
||||
|
||||
## ADR-001 — Move away from WordPress
|
||||
|
||||
**Date:** 2026
|
||||
**Status:** Decided
|
||||
|
||||
### Context
|
||||
The previous Quatsh website was built on WordPress, hosted on a shared webhosting service (cloud86). Over time, development had ground to a halt due to several compounding problems:
|
||||
|
||||
- WordPress stores page content in a serialised format inside a database, making it difficult to version control, diff, or reason about changes.
|
||||
- The site relied heavily on third-party plugins, creating a fragile dependency chain where updating one plugin risked breaking others.
|
||||
- PHP and the WordPress ecosystem were unfamiliar to the association's technically-minded members, narrowing the pool of people who could contribute.
|
||||
- The shared webhosting service was more expensive and less flexible than a VPS for a project of this nature.
|
||||
- The page-stucture made DRY development difficult leading to a lot of code duplication across different pages.
|
||||
- Direct database access using Raw-SQL made it easy to make mistakes.
|
||||
|
||||
### Decision
|
||||
Replace the WordPress site with a purpose-built web application on a VPS.
|
||||
|
||||
### Consequences
|
||||
- Full control over the codebase, dependencies, and infrastructure.
|
||||
- Performance issues can actually be debugged and fixed.
|
||||
- Higher initial setup cost in developer time, offset by lower long-term maintenance friction and cheaper hosting.
|
||||
- The association is no longer dependent on a third-party hosting provider's ecosystem.
|
||||
|
||||
---
|
||||
|
||||
## ADR-002 — Django as the web framework
|
||||
|
||||
**Date:** 2026
|
||||
**Status:** Decided
|
||||
|
||||
### Context
|
||||
Having decided to build a custom web application, a framework and language needed to be chosen. The primary constraints were:
|
||||
|
||||
- **Maintainability over time** — the project will be handed off to future developers. The current maintainer will only be a member for a limited number of years.
|
||||
- **Familiarity within the target contributor pool** — a student sports association at a university. New contributors are likely to be students.
|
||||
- **Batteries included** — the association does not have the capacity to assemble and maintain a bespoke stack of micro-libraries.
|
||||
|
||||
### Decision
|
||||
Use Django with Python as the primary framework and language.
|
||||
|
||||
### Reasoning
|
||||
- Python is the most commonly taught language at Dutch universities, maximising the pool of potential future contributors.
|
||||
- Django is "batteries included" — authentication, admin interface, ORM, migrations, form handling, and email are all built in. This reduces the number of third-party dependencies and the surface area a new maintainer needs to learn.
|
||||
- A single Django repository centralises the entire codebase (backend, templates, static files, configuration) making handoff to future maintainers straightforward.
|
||||
- The Django admin interface provides board members with basic data management capabilities without requiring a custom-built admin panel from scratch.
|
||||
|
||||
### Alternatives considered
|
||||
- **Flask / FastAPI** — too minimal; would require assembling many additional libraries, increasing long-term maintenance burden.
|
||||
- **Node.js** — less common in the university curriculum than Python; splits frontend and backend concerns in a way that adds complexity for a small team.
|
||||
- **Laravel (PHP)** — would have kept the PHP ecosystem from WordPress, but PHP is less familiar to the target contributor pool and does not solve the maintainability problem.
|
||||
|
||||
### Consequences
|
||||
- New contributors with basic Python knowledge can get up to speed quickly.
|
||||
- The project is well-positioned for handoff to future maintainers.
|
||||
- Django's monolithic nature means the entire application is in one place, which is appropriate for a project of this scale.
|
||||
|
||||
---
|
||||
|
||||
## ADR-003 — Gunicorn + nginx as the serving stack
|
||||
|
||||
**Date:** 2026
|
||||
**Status:** Decided
|
||||
|
||||
### Context
|
||||
Django's built-in development server is not suitable for production. A production-grade serving stack was needed.
|
||||
|
||||
### Decision
|
||||
Use Gunicorn as the WSGI application server behind nginx acting as a reverse proxy and static file server.
|
||||
|
||||
### Reasoning
|
||||
- Gunicorn is the standard WSGI server for Django in production and is well documented alongside Django.
|
||||
- nginx is significantly more efficient than Django/Gunicorn at serving static files and handling slow clients.
|
||||
- nginx handles SSL termination, keeping TLS configuration out of the application layer.
|
||||
- This is the most common Django deployment pattern, meaning future maintainers are likely to be familiar with it or find documentation easily.
|
||||
|
||||
### Consequences
|
||||
- Static files must be collected via `manage.py collectstatic` before deployment (handled in the Makefile).
|
||||
- nginx and the Django container share a read-only/read-write volume respectively for static and media files.
|
||||
|
||||
---
|
||||
|
||||
## ADR-004 — Docker Compose for container orchestration
|
||||
|
||||
**Date:** 2026
|
||||
**Status:** Decided
|
||||
|
||||
### Context
|
||||
The application consists of multiple services (Django, nginx, Postgres) that need to run together consistently across development and production environments.
|
||||
|
||||
### Decision
|
||||
Use Docker with Docker Compose for local development and production deployment, with separate compose override files per environment.
|
||||
|
||||
### Reasoning
|
||||
- Docker ensures the application runs identically regardless of the host machine, reducing "works on my machine" issues during handoff.
|
||||
- Docker is an easier way of getting the full stack operational on a new contributors machine compared to making them do multiple different installs.
|
||||
- Docker Compose is simple enough for a non-specialist maintainer to understand and operate.
|
||||
- Separate override files (`compose.dev.yaml`, `compose.prod.yaml`, `compose.test.yaml`) allow environment-specific configuration without duplicating the base service definitions.
|
||||
- A VPS with Docker is significantly cheaper than a managed hosting service while providing more control.
|
||||
|
||||
### Consequences
|
||||
- New maintainers need minimal Docker literacy, but this is a reasonable baseline expectation.
|
||||
- The Makefile wraps common Docker Compose commands to reduce the surface area a maintainer needs to know day-to-day.
|
||||
- Data persistence is handled via named Docker volumes.
|
||||
|
||||
---
|
||||
|
||||
## ADR-005 — PostgreSQL as the database
|
||||
|
||||
**Date:** 2026
|
||||
**Status:** Decided
|
||||
|
||||
### Context
|
||||
Django supports multiple database backends. A production database needed to be chosen.
|
||||
|
||||
### Decision
|
||||
Use PostgreSQL 18.
|
||||
|
||||
### Reasoning
|
||||
- PostgreSQL is the recommended database for Django in production.
|
||||
- It is robust, well-documented, and widely understood.
|
||||
|
||||
### Consequences
|
||||
- The Postgres Docker volume must be mounted at `/var/lib/postgresql` (not `/data`) when using the official `postgres:18` image. See the note in `compose.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## ADR-006 — PEP8 as style guide
|
||||
|
||||
**Date:** 2026/04/08
|
||||
**Status:** Decided
|
||||
|
||||
### Context
|
||||
Styleguides are used to ensure the long-term maintainability and readability of a codebase.
|
||||
|
||||
### Decision
|
||||
Use PEP8 as the style guide with enforced type hinting. Enforced using 'ruff' and 'MyPy'.
|
||||
|
||||
### Reasoning
|
||||
- PEP8 is the standard style guide for python development. It has great tooling to ensure enforcement of the style guide.
|
||||
- Type hinting greatly improves the readability of code as you are better able to reason. It also helps keep variable names short as "picture_array" is instead written as "pictures: Sequence".
|
||||
|
||||
### Consequences
|
||||
Linting checks have to integrated into the CI/CD pipeline.
|
||||
'Contributing.md' needs to be created to make it clear how to contribute.
|
||||
|
||||
---
|
||||
|
||||
*When making a new architectural decision, copy the template below and append it to this file.*
|
||||
|
||||
---
|
||||
|
||||
## ADR-00X — [Title]
|
||||
|
||||
**Date:**
|
||||
**Status:** Proposed / Decided / Superseded by ADR-00X
|
||||
|
||||
### Context
|
||||
<!-- What is the situation that requires a decision? What constraints exist? -->
|
||||
|
||||
### Decision
|
||||
<!-- What was decided? -->
|
||||
|
||||
### Reasoning
|
||||
<!-- Why was this the right choice given the context? -->
|
||||
|
||||
### Alternatives considered
|
||||
<!-- What else was evaluated and why was it not chosen? -->
|
||||
|
||||
### Consequences
|
||||
<!-- What are the implications of this decision, positive and negative? -->
|
||||
31
docs/environment.md
Normal file
31
docs/environment.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Details on environment variables
|
||||
|
||||
## Nginx
|
||||
|
||||
### NGINX_HOSTNAME
|
||||
|
||||
## Django
|
||||
|
||||
### DEBUG
|
||||
|
||||
### DJANGO_SETTINGS_MODULE
|
||||
|
||||
### DJANGO_SECRET_KEY
|
||||
|
||||
## Database (PostgreSQL)
|
||||
|
||||
### POSTGRES_HOST
|
||||
|
||||
### POSTGRES_PORT
|
||||
|
||||
### POSTGRES_USER
|
||||
|
||||
### POSTGRES_PASSWORD
|
||||
|
||||
### POSTGRES_DBNAME
|
||||
|
||||
## Gunicorn
|
||||
|
||||
### GUNICORN_WORKERS
|
||||
|
||||
### GUNICORN_TIMEOUT
|
||||
22
docs/installation_faq.md
Normal file
22
docs/installation_faq.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Common Issues & Questions
|
||||
|
||||
## Environment
|
||||
|
||||
### Postgres Database
|
||||
|
||||
The database login-details are set upon first-launch.
|
||||
Therefore trying to change them later, after first-launch, will require a change of these details.
|
||||
A way of sidestepping manually changing this in the DB-shell is by deleting the postgres data volume.
|
||||
|
||||
> [!WARNING]
|
||||
> Do not run this command in prod, it will delete ALL data in our database.
|
||||
|
||||
```shell
|
||||
docker volume rm quatsh-website_postgres-data
|
||||
```
|
||||
|
||||
After running this command a Django Migration has to be run to remake the tables.
|
||||
|
||||
``` shell
|
||||
docker exec quatsh-website-web-1 python manage.py migrate
|
||||
```
|
||||
72
docs/templates/template.md
vendored
Normal file
72
docs/templates/template.md
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# Feature Name
|
||||
|
||||
> One sentence description of what this feature does and who it is for.
|
||||
|
||||
## Status
|
||||
<!-- Delete all that do not apply -->
|
||||
`Planned` `In Progress` `In Review` `Complete` `Deferred`
|
||||
|
||||
## Background
|
||||
<!-- Why are we building this? What problem does it solve? Link to any relevant roadmap phase. -->
|
||||
|
||||
## Scope
|
||||
<!-- What is included in this feature. Be explicit about what is out of scope too. -->
|
||||
|
||||
**In scope:**
|
||||
-
|
||||
|
||||
**Out of scope:**
|
||||
-
|
||||
|
||||
## User Stories
|
||||
<!-- Who interacts with this feature and what do they want to achieve? -->
|
||||
- As a **[member / board member / anonymous visitor]**, I want to **[action]** so that **[outcome]**.
|
||||
|
||||
## Data Model
|
||||
<!-- What models are created or modified? List fields and their types. -->
|
||||
|
||||
```python
|
||||
class ExampleModel(models.Model):
|
||||
pass
|
||||
```
|
||||
|
||||
**Related models:**
|
||||
- `User` —
|
||||
|
||||
## GDPR Considerations
|
||||
<!-- See docs/gdpr/ for shared policies. Only document what is specific to this feature. -->
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Personal data collected | |
|
||||
| Legal basis | Legitimate interest / Consent / Legal obligation |
|
||||
| Retention period | See `docs/gdpr/retention-policy.md` / [custom policy] |
|
||||
| Erasure behaviour | Cascade delete / Anonymise / Transfer ownership |
|
||||
| Visible to non-members | Yes / No |
|
||||
|
||||
## Open Questions
|
||||
<!-- Unresolved decisions that are blocking or will affect implementation. -->
|
||||
- [ ]
|
||||
|
||||
## Decisions Log
|
||||
<!-- Record decisions made during development so future contributors understand why things are the way they are. -->
|
||||
|
||||
| Date | Decision | Reasoning |
|
||||
|---|---|---|
|
||||
| YYYY-MM-DD | | |
|
||||
|
||||
## Tasks
|
||||
<!-- Break the feature down into concrete implementation steps. -->
|
||||
|
||||
### Backend
|
||||
- [ ]
|
||||
|
||||
### Frontend
|
||||
- [ ]
|
||||
|
||||
### Tests
|
||||
- [ ]
|
||||
|
||||
## Related
|
||||
<!-- Links to related feature files, external docs, or GitHub issues. -->
|
||||
-
|
||||
@@ -1,2 +1,9 @@
|
||||
Django==6.0.3
|
||||
gunicorn==25.1.0
|
||||
psycopg [binary] ==3.3.3
|
||||
django-browser-reload
|
||||
django-watchfiles
|
||||
docutils==0.22.4
|
||||
ruff==0.15.9
|
||||
Mypy==1.20
|
||||
django-stubs==5.0.2
|
||||
|
||||
3
src/gallery/admin.py
Normal file
3
src/gallery/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
src/gallery/apps.py
Normal file
5
src/gallery/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GalleryConfig(AppConfig):
|
||||
name = 'gallery'
|
||||
0
src/gallery/migrations/__init__.py
Normal file
0
src/gallery/migrations/__init__.py
Normal file
3
src/gallery/models.py
Normal file
3
src/gallery/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
src/gallery/tests.py
Normal file
3
src/gallery/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
src/gallery/views.py
Normal file
3
src/gallery/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
src/polls/__init__.py
Normal file
0
src/polls/__init__.py
Normal file
7
src/polls/admin.py
Normal file
7
src/polls/admin.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
||||
from .models import Question
|
||||
|
||||
admin.site.register(Question)
|
||||
5
src/polls/apps.py
Normal file
5
src/polls/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PollsConfig(AppConfig):
|
||||
name = 'polls'
|
||||
32
src/polls/migrations/0001_initial.py
Normal file
32
src/polls/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
src/polls/migrations/__init__.py
Normal file
0
src/polls/migrations/__init__.py
Normal file
20
src/polls/models.py
Normal file
20
src/polls/models.py
Normal file
@@ -0,0 +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
|
||||
11
src/polls/templates/polls/detail.html
Normal file
11
src/polls/templates/polls/detail.html
Normal 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>
|
||||
19
src/polls/templates/polls/index.html
Normal file
19
src/polls/templates/polls/index.html
Normal 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>
|
||||
|
||||
3
src/polls/tests.py
Normal file
3
src/polls/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
src/polls/urls.py
Normal file
14
src/polls/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# ex: /polls/
|
||||
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"),
|
||||
]
|
||||
24
src/polls/views.py
Normal file
24
src/polls/views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from .models import Question
|
||||
|
||||
|
||||
def index(request):
|
||||
latest_question_list = Question.objects.order_by("-pub_date")[:5]
|
||||
context = {"latest_question_list": latest_question_list}
|
||||
return render(request, "polls/index.html", context)
|
||||
|
||||
|
||||
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)
|
||||
0
src/website/__init__.py
Normal file
0
src/website/__init__.py
Normal file
129
src/website/settings.py
Normal file
129
src/website/settings.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Django settings for website project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.getenv("DEBUG") == "TRUE"
|
||||
|
||||
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.contrib.admindocs",
|
||||
"django_browser_reload",
|
||||
"django_watchfiles",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"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",
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "website.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "website.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.getenv("POSTGRES_DBNAME"),
|
||||
"USER": os.getenv("POSTGRES_USER"),
|
||||
"PASSWORD": os.getenv("POSTGRES_PASSWORD"),
|
||||
"HOST": os.getenv("POSTGRES_HOST"),
|
||||
"PORT": os.getenv("POSTGRES_PORT"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "Europe/Amsterdam"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
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/"
|
||||
@@ -14,9 +14,13 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("polls/", include("polls.urls")),
|
||||
path("admin/doc/", include("django.contrib.admindocs.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
]
|
||||
Reference in New Issue
Block a user