Compare commits

...

8 Commits

29 changed files with 907 additions and 61 deletions

View File

@@ -1,18 +1,36 @@
---
services: services:
web: web:
command: gunicorn --capture-output --enable-stdio-inheritance -b 0.0.0.0:8000 website.wsgi:application command: python manage.py runserver 0.0.0.0:8000
volumes: volumes: [./src:/src]
- ./src:/src
env_file: env_file:
- path: .env.template - path: .env.template
required: true required: true
- path: .env - path: .env
required: false required: false
environment:
DJANGO_RELOAD: true
DEBUG: true
ALLOWED_HOSTS: localhost,127.0.0.1
ports: [127.0.0.1:8000:8000]
db: db:
env_file: env_file:
- path: .env.template - path: .env.template
required: true required: true
- path: .env - path: .env
required: false 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: services:
web: web:
command: gunicorn website.wsgi:application
restart: unless-stopped
environment:
ALLOWED_HOSTS: ${NGINX_HOSTNAME}
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE}
env_file: env_file:
- path: .env - path: .env
required: true required: true
db: db:
restart: unless-stopped
env_file: env_file:
- path: .env - path: .env
required: true 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: services:
web: web:
command: python manage.py check --deploy; python -Wa manage.py test --noinput --parallel command: python manage.py check --deploy; python -Wa manage.py test --noinput
restart: "no" --parallel
environment:
DEBUG: false
DJANGO_SETTINGS_MODULE: website.settings.test
env_file: env_file:
- path: .env.template - path: .env.template
required: true required: true
db: db:
# Tmpfs keeps tests fast and isolated — no persistent volume needed
tmpfs: [/var/lib/postgresql/data]
env_file: env_file:
- path: .env.template - path: .env.template
required: true required: true

View File

@@ -4,8 +4,6 @@ NGINX_HOSTNAME=localhost
# Django # Django
DJANGO_SETTINGS_MODULE=website.settings DJANGO_SETTINGS_MODULE=website.settings
DJANGO_SECRET_KEY=CWHZCAZBNV57tDkwGHJwTUu3PCSnGG45 DJANGO_SECRET_KEY=CWHZCAZBNV57tDkwGHJwTUu3PCSnGG45
DEBUG=TRUE
#ALLOWED_HOSTS=localhost
# Database (PostgreSQL) # Database (PostgreSQL)
POSTGRES_USER=test_user POSTGRES_USER=test_user

View File

@@ -1,38 +1,30 @@
---
name: Deploy to production name: Deploy to production
run-name: deploy-${{ gitea.actor }} run-name: deploy-${{ gitea.actor }}
on: on:
push: push:
branches: branches: [main]
- main
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup SSH - name: Setup SSH
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
- name: Deploy - name: Deploy
run: | run: |-
ssh -i ~/.ssh/id_rsa ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' ssh -i ~/.ssh/id_rsa ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF'
cd ~/Quatsh-Website cd ~/Quatsh-Website
echo "Pulling latest code..." echo "Pulling latest code..."
git pull origin main git pull origin main
echo "Building containers..." echo "Building containers..."
make prod make prod
echo "Running Django deploy check" echo "Running Django deploy check"
docker compose run --rm web python manage.py check --deploy docker compose run --rm web python manage.py check --deploy
echo "Deployment complete." echo "Deployment complete."
EOF EOF

View File

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

3
.gitignore vendored
View File

@@ -178,4 +178,5 @@ cython_debug/
# gunicon webserver # gunicon webserver
gunicorn.ctl 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 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1
@@ -12,4 +12,11 @@ RUN pip install --upgrade pip && \
COPY gunicorn.conf.py /src/ COPY gunicorn.conf.py /src/
COPY ./src/ /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"] CMD ["gunicorn", "website.wsgi:application"]

View File

@@ -2,16 +2,9 @@
services: services:
web: web:
build: . build: .
command: gunicorn website.wsgi:application
depends_on: depends_on:
db: db:
condition: service_healthy 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] volumes: [static_volume:/app/static, media_volume:/app/media]
networks: [frontend, backend] networks: [frontend, backend]
db: db:
@@ -21,32 +14,24 @@ services:
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: [postgres_data:/var/lib/postgresql] volumes: [postgres_data:/var/lib/postgresql]
restart: unless-stopped
healthcheck: healthcheck:
test: [CMD-SHELL, 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DBNAME}'] test: [CMD-SHELL, 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DBNAME}']
interval: 5s interval: 5s
retries: 5 retries: 5
networks: [backend] networks: [backend]
adminer:
image: adminer
depends_on: [db]
restart: always
ports: [127.0.0.1:8080:8080]
networks: [backend]
proxy: proxy:
image: nginx:stable image: nginx:stable
volumes: volumes:
- ./.nginx/.templates:/etc/nginx/templates - ./.nginx/.templates:/etc/nginx/templates
- static_volume:/app/static:ro - static_volume:/app/static:ro
- media_volume:/app/media: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] depends_on: [web]
networks: [frontend] networks: [frontend]
adminer:
image: adminer
depends_on: [db]
ports: [127.0.0.1:8080:8080]
networks: [backend]
networks: networks:
frontend: frontend:
backend: 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/ https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
from pathlib import Path
import os import os
from pathlib import Path
# 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
@@ -24,7 +24,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
# SECURITY WARNING: don't run with debug turned on in production! # 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(",") 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 = [ INSTALLED_APPS = [
"polls.apps.PollsConfig", "polls.apps.PollsConfig",
"core.apps.CoreConfig",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@@ -53,7 +54,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "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_browser_reload")
INSTALLED_APPS.append("django_watchfiles") INSTALLED_APPS.append("django_watchfiles")
MIDDLEWARE.append("django_browser_reload.middleware.BrowserReloadMiddleware") MIDDLEWARE.append("django_browser_reload.middleware.BrowserReloadMiddleware")
@@ -63,7 +64,7 @@ ROOT_URLCONF = "website.urls"
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [], "DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
"context_processors": [ "context_processors": [
@@ -129,3 +130,7 @@ USE_TZ = True
STATIC_ROOT = "/app/static/" STATIC_ROOT = "/app/static/"
STATIC_URL = "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 from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", include("core.urls")),
path("polls/", include("polls.urls")), path("polls/", include("polls.urls")),
path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/doc/", include("django.contrib.admindocs.urls")),
path("admin/", admin.site.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"))) urlpatterns.append(path("__reload__/", include("django_browser_reload.urls")))