Compare commits
10 commits
6464703381
...
8ed93ebc12
Author | SHA1 | Date | |
---|---|---|---|
8ed93ebc12 | |||
|
06f53e8304 | ||
|
c4ed1e0792 | ||
|
7de5cd52d2 | ||
|
7ba077608c | ||
|
4be5e93c9c | ||
|
03778e9834 | ||
|
2b9dbcbb2d | ||
|
9576202fa1 | ||
|
288759146c |
22 changed files with 306 additions and 20 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,7 +1,9 @@
|
|||
/build
|
||||
*/uploads
|
||||
*/mediafiles
|
||||
*/staticfiles
|
||||
__pycache__
|
||||
*.pyc
|
||||
.venv
|
||||
.env*
|
||||
*.sqlite3
|
||||
|
||||
|
|
45
README.md
45
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
Web backend for hosting a wedding website.
|
||||
|
||||
## Run development server:
|
||||
# Run Local server:
|
||||
|
||||
```sh
|
||||
source .venv/bin/activate
|
||||
|
@ -10,10 +10,49 @@ cd wedding_site
|
|||
python manage.py runserver
|
||||
```
|
||||
|
||||
## Do Migrations
|
||||
# Do Migrations
|
||||
|
||||
```sh
|
||||
cd wedding_site
|
||||
python manage.py makemigrations primary
|
||||
python manage.py migrate
|
||||
```
|
||||
```
|
||||
|
||||
# Run Dev Server in container
|
||||
|
||||
```sh
|
||||
podman-compose -f compose.yaml up -d --build
|
||||
```
|
||||
|
||||
Bring it down with:
|
||||
|
||||
```sh
|
||||
podman-compose down -v
|
||||
```
|
||||
|
||||
# Run Prod Server
|
||||
|
||||
```sh
|
||||
podman-compose -f compose.prod.yaml up -d --build
|
||||
```
|
||||
|
||||
Sync static files:
|
||||
|
||||
```sh
|
||||
podman-compose -f compose.prod.yaml exec web python manage.py collectstatic --no-input --clear
|
||||
```
|
||||
|
||||
Check cert
|
||||
|
||||
```sh
|
||||
podman-compose -f compose.prod.yaml exec acme-companion /app/cert_status
|
||||
```
|
||||
|
||||
Force renew cert:
|
||||
|
||||
```sh
|
||||
podman-compose -f compose.prod.yaml exec acme-companion /app/force_renew
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
52
compose.prod.yaml
Normal file
52
compose.prod.yaml
Normal file
|
@ -0,0 +1,52 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: ./wedding_site
|
||||
command: gunicorn wedding_site.wsgi:application --bind 0.0.0.0:8000
|
||||
volumes:
|
||||
- ./wedding_site/:/usr/src/app/
|
||||
- static_volume:/staticfiles
|
||||
- media_volume:/mediafiles
|
||||
expose:
|
||||
- 8000
|
||||
env_file:
|
||||
- ./.env.prod
|
||||
|
||||
nginx-proxy:
|
||||
container_name: nginx-proxy
|
||||
build: ./nginx
|
||||
restart: always
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80
|
||||
volumes:
|
||||
- static_volume:/staticfiles
|
||||
- media_volume:/mediafiles
|
||||
- certs:/etc/nginx/certs
|
||||
- html:/usr/share/nginx/html
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
acme-companion:
|
||||
image: docker.io/nginxproxy/acme-companion
|
||||
env_file:
|
||||
- ./.env.prod.proxy-companion
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- certs:/etc/nginx/certs
|
||||
- html:/usr/share/nginx/html
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- acme:/etc/acme.sh
|
||||
depends_on:
|
||||
- nginx-proxy
|
||||
|
||||
volumes:
|
||||
static_volume:
|
||||
media_volume:
|
||||
certs:
|
||||
html:
|
||||
vhost:
|
||||
acme:
|
52
compose.staging.yaml
Normal file
52
compose.staging.yaml
Normal file
|
@ -0,0 +1,52 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: ./wedding_site
|
||||
command: gunicorn wedding_site.wsgi:application --bind 0.0.0.0:8000
|
||||
volumes:
|
||||
- ./wedding_site/:/usr/src/app/
|
||||
- static_volume:/staticfiles
|
||||
- media_volume:/mediafiles
|
||||
expose:
|
||||
- 8000
|
||||
env_file:
|
||||
- ./.env.staging
|
||||
|
||||
nginx-proxy:
|
||||
container_name: nginx-proxy
|
||||
build: ./nginx
|
||||
restart: always
|
||||
ports:
|
||||
- 443:443
|
||||
- 80:80
|
||||
volumes:
|
||||
- static_volume:/staticfiles
|
||||
- media_volume:/mediafiles
|
||||
- certs:/etc/nginx/certs
|
||||
- html:/usr/share/nginx/html
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
depends_on:
|
||||
- web
|
||||
|
||||
acme-companion:
|
||||
image: docker.io/nginxproxy/acme-companion
|
||||
env_file:
|
||||
- ./.env.staging.proxy-companion
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- certs:/etc/nginx/certs
|
||||
- html:/usr/share/nginx/html
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- acme:/etc/acme.sh
|
||||
depends_on:
|
||||
- nginx-proxy
|
||||
|
||||
volumes:
|
||||
static_volume:
|
||||
media_volume:
|
||||
certs:
|
||||
html:
|
||||
vhost:
|
||||
acme:
|
12
compose.yaml
Normal file
12
compose.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: ./wedding_site
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
volumes:
|
||||
- ./wedding_site/:/usr/src/app/
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file:
|
||||
- ./.env.dev
|
1
infra/install_arch_deps.sh
Normal file
1
infra/install_arch_deps.sh
Normal file
|
@ -0,0 +1 @@
|
|||
pacman -Sy podman aardvark-dns
|
4
nginx/Dockerfile
Normal file
4
nginx/Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
|||
FROM docker.io/nginxproxy/nginx-proxy
|
||||
|
||||
COPY vhost.d/default /etc/nginx/vhost.d/default
|
||||
COPY custom.conf /etc/nginx/conf.d/custom.conf
|
1
nginx/custom.conf
Normal file
1
nginx/custom.conf
Normal file
|
@ -0,0 +1 @@
|
|||
client_max_body_size 100M;
|
9
nginx/vhost.d/default
Normal file
9
nginx/vhost.d/default
Normal file
|
@ -0,0 +1,9 @@
|
|||
location /static/ {
|
||||
alias /staticfiles/;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
alias /mediafiles/;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
|
@ -1,4 +1 @@
|
|||
django
|
||||
markdown
|
||||
pillow
|
||||
beautifulsoup4
|
||||
podman-compose
|
19
wedding_site/Dockerfile
Normal file
19
wedding_site/Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
|||
# pull official base image
|
||||
FROM python:3.11.4-slim-buster
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# install dependencies
|
||||
RUN pip install --upgrade pip
|
||||
COPY ./requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
RUN mkdir staticfiles
|
||||
RUN mkdir mediafiles
|
||||
|
||||
ADD ./mediafiles /mediafiles
|
||||
|
||||
# copy project
|
||||
COPY . .
|
18
wedding_site/primary/migrations/0010_page_prority.py
Normal file
18
wedding_site/primary/migrations/0010_page_prority.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.2 on 2024-02-18 09:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('primary', '0009_page_pretty_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='page',
|
||||
name='prority',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.2 on 2024-02-18 10:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('primary', '0010_page_prority'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='page',
|
||||
old_name='prority',
|
||||
new_name='priority',
|
||||
),
|
||||
]
|
|
@ -18,6 +18,7 @@ class Page(models.Model):
|
|||
pretty_name = models.CharField(max_length=200, default="")
|
||||
published = models.BooleanField(default=False)
|
||||
navigable = models.BooleanField(default=False)
|
||||
priority = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
BIN
wedding_site/primary/static/primary/favicon.ico
Executable file
BIN
wedding_site/primary/static/primary/favicon.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -33,9 +33,9 @@ body {
|
|||
|
||||
img {
|
||||
margin:0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width:100%;
|
||||
max-height:300px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px 0;
|
||||
|
@ -107,6 +107,10 @@ ul {
|
|||
list-style-position: inside;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding: 5px 0px;
|
||||
}
|
||||
|
||||
.login-input{
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
|
@ -143,7 +147,7 @@ input[type=submit] {
|
|||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
width: 50%;
|
||||
width: 75%;
|
||||
margin:0 auto;
|
||||
font-family: GlacialIndifference;
|
||||
}
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
content="width=device-width, initial-scale=1">
|
||||
|
||||
{% load static %}
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'primary/favicon.ico' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'primary/style.css' %}">
|
|
@ -7,6 +7,7 @@ urlpatterns = [
|
|||
path("", views.index, name="index"),
|
||||
path('login', views.login_view, name="login"),
|
||||
path("home", views.home, name="home"),
|
||||
path("afters", views.afters, name="afters"),
|
||||
path("schedule", views.schedule, name="schedule"),
|
||||
path("thingstodo", views.things_to_do, name="thingstodo"),
|
||||
path("travel", views.travel, name="travel"),
|
||||
|
|
|
@ -6,6 +6,8 @@ from django.template import Context
|
|||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from django.views.defaults import page_not_found
|
||||
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
@ -24,6 +26,17 @@ _TEMPLATE = """
|
|||
</html>
|
||||
"""
|
||||
|
||||
def handler404(request, exception, template_name="404.html"):
|
||||
page_names = ["home", "schedule", "thingstodo", "afters"]
|
||||
working_path = request.path.lower()
|
||||
if working_path and working_path[0] == "/":
|
||||
working_path = working_path[1:]
|
||||
if working_path and working_path[-1] == "/":
|
||||
working_path = working_path[:-1]
|
||||
if working_path in page_names:
|
||||
return redirect(working_path)
|
||||
return page_not_found(request, exception, template_name)
|
||||
|
||||
def get_site_header(site):
|
||||
template = Template(site.header)
|
||||
context = Context({"site": site})
|
||||
|
@ -46,8 +59,11 @@ def index(request):
|
|||
return HttpResponse(soup.prettify())
|
||||
|
||||
def login_view(request):
|
||||
token = request.POST["token"]
|
||||
user = authenticate(request, username="guest", password=token)
|
||||
if request.method == "GET":
|
||||
token = request.GET["token"]
|
||||
else:
|
||||
token = request.POST["token"]
|
||||
user = authenticate(request, username="guest", password=token.lower())
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return redirect("home")
|
||||
|
@ -58,9 +74,13 @@ def login_view(request):
|
|||
def home(request):
|
||||
return get_page("Home")
|
||||
|
||||
@login_required(login_url="/")
|
||||
def afters(request):
|
||||
return get_page("Afters")
|
||||
|
||||
def get_page_header(site: Site):
|
||||
|
||||
pages = site.page_set.filter(navigable=True)
|
||||
pages = site.page_set.order_by("priority").filter(navigable=True)
|
||||
template = Template(site.page_header)
|
||||
context = Context({"pages" : pages})
|
||||
return template.render(context)
|
||||
|
@ -87,7 +107,8 @@ def get_page(name:str):
|
|||
img_name = img["src"]
|
||||
db_images = Image.objects.filter(name=img_name)
|
||||
if db_images:
|
||||
img["src"] = db_images[0].content.url
|
||||
url = db_images[0].content.url.replace("uploads/","")
|
||||
img["src"] = url
|
||||
|
||||
return HttpResponse(soup.prettify())
|
||||
|
||||
|
|
5
wedding_site/requirements.txt
Normal file
5
wedding_site/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
django
|
||||
markdown
|
||||
pillow
|
||||
beautifulsoup4
|
||||
gunicorn
|
|
@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.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
|
||||
|
@ -19,13 +20,15 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-bj9fez3qztt5e2lrzpgh%%nat@w^kn!k@l92l=+#%wm)4)p^5m'
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
if "DJANGO_DEBUG" in os.environ:
|
||||
DEBUG = os.environ.get("DJANGO_DEBUG") == 1
|
||||
else:
|
||||
DEBUG=False
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
if "DJANGO_ALLOWED_HOSTS" in os.environ:
|
||||
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
|
||||
|
||||
|
||||
# Application definition
|
||||
|
@ -112,12 +115,29 @@ USE_I18N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
if "CSRF_TRUSTED_ORIGINS" in os.environ:
|
||||
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS").split(" ")
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
if "DJANGO_STATIC_ROOT" in os.environ:
|
||||
STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT")
|
||||
else:
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
MEDIA_URL = 'media/'
|
||||
|
||||
if "DJANGO_MEDIA_ROOT" in os.environ:
|
||||
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT")
|
||||
else:
|
||||
MEDIA_ROOT = BASE_DIR / "mediafiles"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||
|
||||
|
|
|
@ -19,7 +19,16 @@ from django.urls import path, include
|
|||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
import os
|
||||
|
||||
if "DJANGO_ADMIN_PATH" in os.environ:
|
||||
ADMIN_URL = os.environ.get("DJANGO_ADMIN_PATH") + "/"
|
||||
else:
|
||||
ADMIN_URL = "admin/"
|
||||
|
||||
urlpatterns = [
|
||||
path("", include('primary.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
path(ADMIN_URL, admin.site.urls),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
handler404 = 'primary.views.handler404'
|
||||
|
|
Loading…
Reference in a new issue