Compare commits

..

8 Commits

29 changed files with 907 additions and 61 deletions

View File

@@ -1,18 +1,36 @@
---
services:
web:
command: gunicorn --capture-output --enable-stdio-inheritance -b 0.0.0.0:8000 website.wsgi:application
volumes:
- ./src:/src
command: python manage.py runserver 0.0.0.0:8000
volumes: [./src:/src]
env_file:
- path: .env.template
required: true
- path: .env
required: false
environment:
DJANGO_RELOAD: true
DEBUG: true
ALLOWED_HOSTS: localhost,127.0.0.1
ports: [127.0.0.1:8000:8000]
db:
env_file:
- path: .env.template
required: true
- path: .env
required: false
proxy:
restart: unless-stopped
ports: [127.0.0.1:80:80]
environment: [NGINX_HOSTNAME=localhost, NGINX_PORT=80]
tailwind:
image: python:3.14-slim
working_dir: /src
command: >
sh -c "apt-get update && apt-get install -y curl &&
curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
-o /usr/local/bin/tailwindcss &&
chmod +x /usr/local/bin/tailwindcss &&
tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch"
volumes: [.:/src]
networks: [frontend]

View File

@@ -1,10 +1,25 @@
---
services:
web:
command: gunicorn website.wsgi:application
restart: unless-stopped
environment:
ALLOWED_HOSTS: ${NGINX_HOSTNAME}
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE}
env_file:
- path: .env
required: true
db:
restart: unless-stopped
env_file:
- path: .env
required: true
adminer:
restart: unless-stopped
proxy:
restart: unless-stopped
ports: [80:80, 443:443]
environment:
- NGINX_HOSTNAME=${NGINX_HOSTNAME}
- NGINX_PORT=80
- NGINX_SSL_PORT=443

View File

@@ -1,14 +1,17 @@
---
services:
web:
command: python manage.py check --deploy; python -Wa manage.py test --noinput --parallel
restart: "no"
command: python manage.py check --deploy; python -Wa manage.py test --noinput
--parallel
environment:
DEBUG: false
DJANGO_SETTINGS_MODULE: website.settings.test
env_file:
- path: .env.template
required: true
db:
# Tmpfs keeps tests fast and isolated — no persistent volume needed
tmpfs: [/var/lib/postgresql/data]
env_file:
- path: .env.template
required: true

View File

@@ -4,8 +4,6 @@ NGINX_HOSTNAME=localhost
# Django
DJANGO_SETTINGS_MODULE=website.settings
DJANGO_SECRET_KEY=CWHZCAZBNV57tDkwGHJwTUu3PCSnGG45
DEBUG=TRUE
#ALLOWED_HOSTS=localhost
# Database (PostgreSQL)
POSTGRES_USER=test_user
@@ -17,4 +15,4 @@ POSTGRES_DB=test_db
# Gunicorn
GUNICORN_WORKERS=3
GUNICORN_TIMEOUT=120
GUNICORN_TIMEOUT=120

View File

@@ -1,38 +1,30 @@
---
name: Deploy to production
run-name: deploy-${{ gitea.actor }}
on:
push:
branches:
- main
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: |
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
EOF

View File

@@ -1,18 +1,16 @@
---
name: Gitea Test.
run-name: ${{ gitea.actor }}
on:
push:
branches:
- main
- pre-prod
branches: [main, pre-prod]
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: lint
run: make lint
- name: test
run: make test
run: make test

3
.gitignore vendored
View File

@@ -178,4 +178,5 @@ cython_debug/
# gunicon webserver
gunicorn.ctl
# tailwind stuff
output.css

View File

@@ -1,4 +1,4 @@
FROM python:3.14-alpine
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
@@ -12,4 +12,11 @@ RUN pip install --upgrade pip && \
COPY gunicorn.conf.py /src/
COPY ./src/ /src/
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o /usr/local/bin/tailwindcss && \
chmod +x /usr/local/bin/tailwindcss && \
apt-get remove -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
RUN tailwindcss build -i ./static/css/input.css -o ./static/css/output.css --minify
CMD ["gunicorn", "website.wsgi:application"]

View File

@@ -2,16 +2,9 @@
services:
web:
build: .
command: gunicorn website.wsgi:application
depends_on:
db:
condition: service_healthy
environment:
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:
@@ -21,32 +14,24 @@ services:
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]
adminer:
image: adminer
depends_on: [db]
ports: [127.0.0.1:8080:8080]
networks: [backend]
networks:
frontend:
backend:

0
src/core/__init__.py Normal file
View File

3
src/core/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
src/core/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = 'core'

View File

3
src/core/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome</h1>
<div>
{% include "components/carousel.html" with carousel_id="hero" carousel_images=carousel_images %}
</div>
{% endblock %}

3
src/core/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
src/core/urls.py Normal file
View File

@@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = "core"
urlpatterns = [
path("", views.index, name="index"),
]

10
src/core/views.py Normal file
View File

@@ -0,0 +1,10 @@
from django.shortcuts import render
from django.templatetags.static import static
def index(request):
return render(
request,
"core/index.html",
{"carousel_images": [static("/core/img/5x5-2023-11-16-at-14.56.56-1.jpeg")]},
)

File diff suppressed because one or more lines are too long

1
src/static/css/input.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss"

68
src/static/js/carousel.js Normal file
View File

@@ -0,0 +1,68 @@
function initCarousel(id) {
document.addEventListener("DOMContentLoaded", function () {
const track = document.getElementById(id);
const originalSlides = Array.from(track.children);
const total = originalSlides.length;
if (total <= 1) {
return;
}
const firstClone = originalSlides[0].cloneNode(true);
const lastClone = originalSlides[total - 1].cloneNode(true);
track.appendChild(firstClone);
track.insertBefore(lastClone, originalSlides[0]);
let current = 1;
let isTransitioning = false;
track.style.transform = `translateX(-${current * 100}%)`;
const dots = document.querySelectorAll(`.${id}_dot`);
function updateDots(index) {
const realIndex = (index - 1 + total) % total;
dots.forEach((d, i) => {
d.classList.toggle("opacity-100", i === realIndex);
d.classList.toggle("opacity-50", i !== realIndex);
});
}
function goTo(index) {
if (isTransitioning) return;
isTransitioning = true;
track.style.transition = "transform 500ms ease-in-out";
current = index;
track.style.transform = `translateX(-${current * 100}%)`;
updateDots(current);
}
function next() {
goTo(current + 1);
}
function prev() {
goTo(current - 1);
}
track.addEventListener("transitionend", function () {
if (current === 0) {
track.style.transition = "none";
current = total;
track.style.transform = `translateX(-${current * 100}%)`;
}
if (current === total + 1) {
track.style.transition = "none";
current = 1;
track.style.transform = `translateX(-${current * 100}%)`;
}
isTransitioning = false;
});
setInterval(next, 5000);
updateDots(current);
window[`${id}_goTo`] = (i) => goTo(i + 1);
window[`${id}_next`] = next;
window[`${id}_prev`] = prev;
});
}

18
src/templates/base.html Normal file
View File

@@ -0,0 +1,18 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% block title %}My Site{% endblock %}
</title>
<link rel="stylesheet" href="{% static 'css/output.css' %}">
</head>
<body>
{% include "components/topbar.html" %}
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,28 @@
{% load static %}
<div class="relative w-full overflow-hidden">
<div id="{{ carousel_id }}" class="flex w-full">
{% for image in carousel_images %}
<div class="w-full flex-shrink-0">
<img src="{{ image }}" class="w-full object-cover">
</div>
{% endfor %}
</div>
{% if carousel_images|length > 1 %}
<button onclick="{{ carousel_id }}_prev()"
class="absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 text-white px-3 py-1 rounded-full">
&#8592;
</button>
<button onclick="{{ carousel_id }}_next()"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 text-white px-3 py-1 rounded-full">
&#8594;
</button>
<div class="absolute bottom-2 w-full flex justify-center gap-2">
{% for image in carousel_images %}
<span class="{{ carousel_id }}_dot w-2 h-2 rounded-full bg-white opacity-50 cursor-pointer"
onclick="{{ carousel_id }}_goTo({{ forloop.counter0 }})"></span>
{% endfor %}
</div>
{% endif %}
</div>
<script src="{% static 'js/carousel.js' %}"></script>
<script>initCarousel('{{ carousel_id }}');</script>

View File

@@ -0,0 +1,4 @@
<nav>
<a href="{% url 'core:index' %}">Home</a>
<!-- rest of your topbar -->
</nav>

View File

@@ -10,8 +10,8 @@ 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
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -24,7 +24,7 @@ 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 = os.getenv("DEBUG") == "TRUE"
DEBUG = os.getenv("DEBUG", "false") == "true"
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1").split(",")
@@ -34,6 +34,7 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
INSTALLED_APPS = [
"polls.apps.PollsConfig",
"core.apps.CoreConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@@ -53,7 +54,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
if os.getenv("DJANGO_RELOAD", "False") == "True":
if os.getenv("DJANGO_RELOAD", "false") == "true":
INSTALLED_APPS.append("django_browser_reload")
INSTALLED_APPS.append("django_watchfiles")
MIDDLEWARE.append("django_browser_reload.middleware.BrowserReloadMiddleware")
@@ -63,7 +64,7 @@ ROOT_URLCONF = "website.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
@@ -129,3 +130,7 @@ USE_TZ = True
STATIC_ROOT = "/app/static/"
STATIC_URL = "static/"
STATICFILES_DIRS = [
BASE_DIR / "static",
]

View File

@@ -21,10 +21,11 @@ from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("core.urls")),
path("polls/", include("polls.urls")),
path("admin/doc/", include("django.contrib.admindocs.urls")),
path("admin/", admin.site.urls),
]
if os.getenv("DJANGO_RELOAD", "False") == "True":
if os.getenv("DJANGO_RELOAD", "false") == "true":
urlpatterns.append(path("__reload__/", include("django_browser_reload.urls")))