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
|
/build
|
||||||
*/uploads
|
*/mediafiles
|
||||||
|
*/staticfiles
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
.venv
|
.venv
|
||||||
|
.env*
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|
||||||
|
|
45
README.md
45
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Web backend for hosting a wedding website.
|
Web backend for hosting a wedding website.
|
||||||
|
|
||||||
## Run development server:
|
# Run Local server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
|
@ -10,10 +10,49 @@ cd wedding_site
|
||||||
python manage.py runserver
|
python manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
## Do Migrations
|
# Do Migrations
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd wedding_site
|
cd wedding_site
|
||||||
python manage.py makemigrations primary
|
python manage.py makemigrations primary
|
||||||
python manage.py migrate
|
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
|
podman-compose
|
||||||
markdown
|
|
||||||
pillow
|
|
||||||
beautifulsoup4
|
|
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="")
|
pretty_name = models.CharField(max_length=200, default="")
|
||||||
published = models.BooleanField(default=False)
|
published = models.BooleanField(default=False)
|
||||||
navigable = models.BooleanField(default=False)
|
navigable = models.BooleanField(default=False)
|
||||||
|
priority = models.IntegerField(default=0)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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 {
|
img {
|
||||||
margin:0 auto;
|
margin:0 auto;
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
max-width:100%;
|
||||||
max-height:300px;
|
max-height:300px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 8px 0;
|
box-shadow: 0 4px 8px 0;
|
||||||
|
@ -107,6 +107,10 @@ ul {
|
||||||
list-style-position: inside;
|
list-style-position: inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
padding: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.login-input{
|
.login-input{
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -143,7 +147,7 @@ input[type=submit] {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 50%;
|
width: 75%;
|
||||||
margin:0 auto;
|
margin:0 auto;
|
||||||
font-family: GlacialIndifference;
|
font-family: GlacialIndifference;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,5 @@
|
||||||
content="width=device-width, initial-scale=1">
|
content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
<link rel="shortcut icon" type="image/png" href="{% static 'primary/favicon.ico' %}"/>
|
||||||
<link rel="stylesheet" href="{% static 'primary/style.css' %}">
|
<link rel="stylesheet" href="{% static 'primary/style.css' %}">
|
|
@ -7,6 +7,7 @@ urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
path('login', views.login_view, name="login"),
|
path('login', views.login_view, name="login"),
|
||||||
path("home", views.home, name="home"),
|
path("home", views.home, name="home"),
|
||||||
|
path("afters", views.afters, name="afters"),
|
||||||
path("schedule", views.schedule, name="schedule"),
|
path("schedule", views.schedule, name="schedule"),
|
||||||
path("thingstodo", views.things_to_do, name="thingstodo"),
|
path("thingstodo", views.things_to_do, name="thingstodo"),
|
||||||
path("travel", views.travel, name="travel"),
|
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 import authenticate, login
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
from django.views.defaults import page_not_found
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
@ -24,6 +26,17 @@ _TEMPLATE = """
|
||||||
</html>
|
</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):
|
def get_site_header(site):
|
||||||
template = Template(site.header)
|
template = Template(site.header)
|
||||||
context = Context({"site": site})
|
context = Context({"site": site})
|
||||||
|
@ -46,8 +59,11 @@ def index(request):
|
||||||
return HttpResponse(soup.prettify())
|
return HttpResponse(soup.prettify())
|
||||||
|
|
||||||
def login_view(request):
|
def login_view(request):
|
||||||
token = request.POST["token"]
|
if request.method == "GET":
|
||||||
user = authenticate(request, username="guest", password=token)
|
token = request.GET["token"]
|
||||||
|
else:
|
||||||
|
token = request.POST["token"]
|
||||||
|
user = authenticate(request, username="guest", password=token.lower())
|
||||||
if user is not None:
|
if user is not None:
|
||||||
login(request, user)
|
login(request, user)
|
||||||
return redirect("home")
|
return redirect("home")
|
||||||
|
@ -58,9 +74,13 @@ def login_view(request):
|
||||||
def home(request):
|
def home(request):
|
||||||
return get_page("Home")
|
return get_page("Home")
|
||||||
|
|
||||||
|
@login_required(login_url="/")
|
||||||
|
def afters(request):
|
||||||
|
return get_page("Afters")
|
||||||
|
|
||||||
def get_page_header(site: Site):
|
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)
|
template = Template(site.page_header)
|
||||||
context = Context({"pages" : pages})
|
context = Context({"pages" : pages})
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
@ -87,7 +107,8 @@ def get_page(name:str):
|
||||||
img_name = img["src"]
|
img_name = img["src"]
|
||||||
db_images = Image.objects.filter(name=img_name)
|
db_images = Image.objects.filter(name=img_name)
|
||||||
if db_images:
|
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())
|
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
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
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
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||||
SECRET_KEY = 'django-insecure-bj9fez3qztt5e2lrzpgh%%nat@w^kn!k@l92l=+#%wm)4)p^5m'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
if "DJANGO_DEBUG" in os.environ:
|
||||||
DEBUG = True
|
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
|
# Application definition
|
||||||
|
@ -112,12 +115,29 @@ USE_I18N = True
|
||||||
|
|
||||||
USE_TZ = 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)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
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
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
# 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 import settings
|
||||||
from django.conf.urls.static import static
|
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 = [
|
urlpatterns = [
|
||||||
path("", include('primary.urls')),
|
path("", include('primary.urls')),
|
||||||
path('admin/', admin.site.urls),
|
path(ADMIN_URL, admin.site.urls),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
handler404 = 'primary.views.handler404'
|
||||||
|
|
Loading…
Reference in a new issue