offloaded some apps.lyric.views responsibilities to new Celery depend fn in .tasks; core.celery created for celery config; CELERY_BROKER_URL added to .settings & throughout project; some lyric view IT responsibility now accordingly covered by task UT domain
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Disco DeDisco
2026-02-21 21:35:15 -05:00
parent 880fcb5bcf
commit 04e28b96c8
12 changed files with 115 additions and 34 deletions

View File

@@ -6,11 +6,15 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
- name: redis
image: redis:7
steps:
- name: test-UTs-n-ITs
image: python:3.13-slim
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
CELERY_BROKER_URL: redis://redis:6379/0
commands:
- pip install -r requirements.txt
- cd ./src
@@ -22,6 +26,7 @@ steps:
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
environment:
HEADLESS: 1
CELERY_BROKER_URL: redis://redis:6379/0
commands:
- pip install -r requirements.txt
- cd ./src

View File

@@ -114,6 +114,15 @@
POSTGRES_USER: gamearray
POSTGRES_PASSWORD: "{{ postgres_password }}"
- name: Start Redis container
community.docker.docker_container:
name: gamearray_redis
image: redis:7
state: started
restart_policy: unless-stopped
networks:
- name: gamearray_net
- name: Run container
community.docker.docker_container:
name: gamearray
@@ -126,11 +135,30 @@
DJANGO_ALLOWED_HOST: "{{ django_allowed_host }}"
DATABASE_URL: "postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray"
MAILGUN_API_KEY: "{{ mailgun_api_key }}"
CELERY_BROKER_URL: "redis://gamearray_redis:6379/0"
networks:
- name: gamearray_net
ports:
127.0.0.1:8888:8888
- name: Start Celery worker container
community.docker.docker_container:
name: gamearray_celery
image: gitea.earthmanrpg.me/discoman/gamearray:latest
state: started
recreate: true
env:
DJANGO_DEBUG_FALSE: "1"
DJANGO_SECRET_KEY: "{{ secret_key.content | b64decode }}"
DJANGO_ALLOWED_HOST: "{{ django_allowed_host }}"
DATABASE_URL: "postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray"
MAILGUN_API_KEY: "{{ mailgun_api_key }}"
CELERY_BROKER_URL: "redis://gamearray_redis:6379/0"
networks:
- name: gamearray_net
command: "python -m celery -A core worker -l info"
- name: Create static files directory
ansible.builtin.file:
path: /var/www/gamearray/static

View File

@@ -17,6 +17,16 @@ docker run -d --name gamearray \
-p 127.0.0.1:8888:8888 \
"$IMAGE"
echo "==> Stopping old celery worker..."
docker stop gamearray_celery 2>/dev/null || true
docker rm gamearray_celery 2>/dev/null || true
echo "==> Starting new celery worker..."
docker run -d --name gamearray_celery \
--env-file /opt/gamearray/gamearray.env \
--network gamearray_net \
"$IMAGE" python -m celery -A core worker -l info
echo "==> Running migrations..."
docker exec gamearray python ./manage.py migrate

View File

@@ -3,3 +3,4 @@ DJANGO_SECRET_KEY={{ secret_key.content | b64decode }}
DJANGO_ALLOWED_HOST={{ django_allowed_host }}
DATABASE_URL=postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray
MAILGUN_API_KEY={{ mailgun_api_key }}
CELERY_BROKER_URL=redis://gamearray_redis:6379/0

View File

@@ -1,3 +1,4 @@
celery
cssselect==1.3.0
Django==6.0
dj-database-url
@@ -7,5 +8,6 @@ djangorestframework
gunicorn==23.0.0
lxml==6.0.2
psycopg2-binary
redis
requests==2.31.0
whitenoise==6.11.0

24
src/apps/lyric/tasks.py Normal file
View File

@@ -0,0 +1,24 @@
import requests
from celery import shared_task
from django.conf import settings
@shared_task
def send_login_email_task(email, url):
message_body = f"Use this magic link to login to your Dashboard:\n\n{url}"
# Send mail via Mailgun HTTP API
response = requests.post(
f"https://api.mailgun.net/v3/{settings.MAILGUN_DOMAIN}/messages",
auth=("api", settings.MAILGUN_API_KEY),
data={
"from": "adman@howdy.earthmanrpg.com",
"to": email,
"subject": "A magic login link to your Dashboard",
"text": message_body,
}
)
# Log any errors
if response.status_code != 200:
print(f"Mailgun API error: {response.status_code}: {response.text}")

View File

@@ -5,27 +5,23 @@ from unittest import mock
from apps.lyric.models import Token
@mock.patch("apps.lyric.views.send_login_email_task.delay")
class SendLoginEmailViewTest(TestCase):
def test_redirects_to_home_page(self):
def test_redirects_to_home_page(self, mock_delay):
response = self.client.post(
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
)
self.assertRedirects(response, "/")
@mock.patch("apps.lyric.views.requests.post")
def test_sends_mail_to_address_from_post(self, mock_post):
def test_sends_mail_to_address_from_post(self, mock_delay):
self.client.post(
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
)
self.assertEqual(mock_post.called, True)
data = mock_post.call_args.kwargs["data"]
self.assertEqual(data["subject"], "A magic login link to your Dashboard")
self.assertEqual(data["from"], "adman@howdy.earthmanrpg.com")
self.assertEqual(data["to"], "discoman@example.com")
self.assertEqual(mock_delay.called, True)
self.assertEqual(mock_delay.call_args.args[0], "discoman@example.com")
def test_adds_success_message(self):
def test_adds_success_message(self, mock_delay):
response = self.client.post(
"/apps/lyric/send_login_email",
data={"email": "discoman@example.com"},
@@ -39,24 +35,21 @@ class SendLoginEmailViewTest(TestCase):
)
self.assertEqual(message.tags, "success")
def test_creates_token_associated_with_email(self):
def test_creates_token_associated_with_email(self, mock_delay):
self.client.post(
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
)
token = Token.objects.get()
self.assertEqual(token.email, "discoman@example.com")
@mock.patch("apps.lyric.views.requests.post")
def test_sends_link_to_login_using_token_uid(self, mock_post):
def test_sends_link_to_login_using_token_uid(self, mock_delay):
self.client.post(
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
)
token = Token.objects.get()
expected_url = f"http://testserver/apps/lyric/login?token={token.uid}"
data = mock_post.call_args.kwargs["data"]
self.assertIn(expected_url, data["text"])
self.assertEqual(mock_delay.call_args.args[1], expected_url)
class LoginViewTest(TestCase):
def test_redirects_to_home_page(self):

View File

@@ -0,0 +1,16 @@
from django.test import SimpleTestCase
from unittest import mock
from apps.lyric.tasks import send_login_email_task
class SendLoginEmailTaskTest(SimpleTestCase):
@mock.patch("apps.lyric.tasks.requests.post")
def test_sends_mail_via_mailgun(self, mock_post):
send_login_email_task("discoman@example.com", "http://example.com/login?token=abc123")
self.assertEqual(mock_post.called, True)
data = mock_post.call_args.kwargs["data"]
self.assertEqual(data["subject"], "A magic login link to your Dashboard")
self.assertEqual(data["from"], "adman@howdy.earthmanrpg.com")
self.assertEqual(data["to"], "discoman@example.com")
self.assertIn("http://example.com/login?token=abc123", data["text"])

View File

@@ -1,10 +1,10 @@
import requests
from django.contrib import auth, messages
from django.conf import settings
# from django.core.mail import send_mail
from django.shortcuts import redirect
from django.urls import reverse
from .models import Token
from .tasks import send_login_email_task
def send_login_email(request):
email = request.POST["email"]
@@ -12,26 +12,13 @@ def send_login_email(request):
url = request.build_absolute_uri(
reverse("login") + "?token=" + str(token.uid),
)
message_body = f"Use this magic link to login to your Dashboard:\n\n{url}"
# Send mail via Mailgun HTTP API
response = requests.post(
f"https://api.mailgun.net/v3/{settings.MAILGUN_DOMAIN}/messages",
auth=("api", settings.MAILGUN_API_KEY),
data={
"from": "adman@howdy.earthmanrpg.com",
"to": email,
"subject": "A magic login link to your Dashboard",
"text": message_body,
},
)
# Log any errors
if response.status_code != 200:
print(f"Mailgun API error: {response.status_code}: {response.text}")
send_login_email_task.delay(email, url)
messages.success(
request,
"Check your email!—there you'll find a magic login link. But hurry… it's only temporary!",
)
return redirect("/")
def login(request):

View File

@@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ("celery_app",)

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

@@ -0,0 +1,10 @@
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
app = Celery('core')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

View File

@@ -109,6 +109,8 @@ else:
}
}
# Celery
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators