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
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@@ -6,11 +6,15 @@ services:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|
||||||
|
- name: redis
|
||||||
|
image: redis:7
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test-UTs-n-ITs
|
- name: test-UTs-n-ITs
|
||||||
image: python:3.13-slim
|
image: python:3.13-slim
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
|
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
|
||||||
|
CELERY_BROKER_URL: redis://redis:6379/0
|
||||||
commands:
|
commands:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- cd ./src
|
- cd ./src
|
||||||
@@ -22,6 +26,7 @@ steps:
|
|||||||
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
|
||||||
environment:
|
environment:
|
||||||
HEADLESS: 1
|
HEADLESS: 1
|
||||||
|
CELERY_BROKER_URL: redis://redis:6379/0
|
||||||
commands:
|
commands:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- cd ./src
|
- cd ./src
|
||||||
|
|||||||
@@ -114,6 +114,15 @@
|
|||||||
POSTGRES_USER: gamearray
|
POSTGRES_USER: gamearray
|
||||||
POSTGRES_PASSWORD: "{{ postgres_password }}"
|
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
|
- name: Run container
|
||||||
community.docker.docker_container:
|
community.docker.docker_container:
|
||||||
name: gamearray
|
name: gamearray
|
||||||
@@ -126,11 +135,30 @@
|
|||||||
DJANGO_ALLOWED_HOST: "{{ django_allowed_host }}"
|
DJANGO_ALLOWED_HOST: "{{ django_allowed_host }}"
|
||||||
DATABASE_URL: "postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray"
|
DATABASE_URL: "postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray"
|
||||||
MAILGUN_API_KEY: "{{ mailgun_api_key }}"
|
MAILGUN_API_KEY: "{{ mailgun_api_key }}"
|
||||||
|
CELERY_BROKER_URL: "redis://gamearray_redis:6379/0"
|
||||||
networks:
|
networks:
|
||||||
- name: gamearray_net
|
- name: gamearray_net
|
||||||
ports:
|
ports:
|
||||||
127.0.0.1:8888:8888
|
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
|
- name: Create static files directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /var/www/gamearray/static
|
path: /var/www/gamearray/static
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ docker run -d --name gamearray \
|
|||||||
-p 127.0.0.1:8888:8888 \
|
-p 127.0.0.1:8888:8888 \
|
||||||
"$IMAGE"
|
"$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..."
|
echo "==> Running migrations..."
|
||||||
docker exec gamearray python ./manage.py migrate
|
docker exec gamearray python ./manage.py migrate
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ DJANGO_SECRET_KEY={{ secret_key.content | b64decode }}
|
|||||||
DJANGO_ALLOWED_HOST={{ django_allowed_host }}
|
DJANGO_ALLOWED_HOST={{ django_allowed_host }}
|
||||||
DATABASE_URL=postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray
|
DATABASE_URL=postgresql://gamearray:{{ postgres_password }}@gamearray_postgres/gamearray
|
||||||
MAILGUN_API_KEY={{ mailgun_api_key }}
|
MAILGUN_API_KEY={{ mailgun_api_key }}
|
||||||
|
CELERY_BROKER_URL=redis://gamearray_redis:6379/0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
celery
|
||||||
cssselect==1.3.0
|
cssselect==1.3.0
|
||||||
Django==6.0
|
Django==6.0
|
||||||
dj-database-url
|
dj-database-url
|
||||||
@@ -7,5 +8,6 @@ djangorestframework
|
|||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
lxml==6.0.2
|
lxml==6.0.2
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
|
redis
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
whitenoise==6.11.0
|
whitenoise==6.11.0
|
||||||
|
|||||||
24
src/apps/lyric/tasks.py
Normal file
24
src/apps/lyric/tasks.py
Normal 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}")
|
||||||
@@ -5,27 +5,23 @@ from unittest import mock
|
|||||||
from apps.lyric.models import Token
|
from apps.lyric.models import Token
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("apps.lyric.views.send_login_email_task.delay")
|
||||||
class SendLoginEmailViewTest(TestCase):
|
class SendLoginEmailViewTest(TestCase):
|
||||||
def test_redirects_to_home_page(self):
|
def test_redirects_to_home_page(self, mock_delay):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, "/")
|
self.assertRedirects(response, "/")
|
||||||
|
|
||||||
@mock.patch("apps.lyric.views.requests.post")
|
def test_sends_mail_to_address_from_post(self, mock_delay):
|
||||||
def test_sends_mail_to_address_from_post(self, mock_post):
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(mock_post.called, True)
|
self.assertEqual(mock_delay.called, True)
|
||||||
data = mock_post.call_args.kwargs["data"]
|
self.assertEqual(mock_delay.call_args.args[0], "discoman@example.com")
|
||||||
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")
|
|
||||||
|
|
||||||
def test_adds_success_message(self):
|
def test_adds_success_message(self, mock_delay):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/apps/lyric/send_login_email",
|
"/apps/lyric/send_login_email",
|
||||||
data={"email": "discoman@example.com"},
|
data={"email": "discoman@example.com"},
|
||||||
@@ -39,24 +35,21 @@ class SendLoginEmailViewTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(message.tags, "success")
|
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(
|
self.client.post(
|
||||||
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
||||||
)
|
)
|
||||||
token = Token.objects.get()
|
token = Token.objects.get()
|
||||||
self.assertEqual(token.email, "discoman@example.com")
|
self.assertEqual(token.email, "discoman@example.com")
|
||||||
|
|
||||||
|
def test_sends_link_to_login_using_token_uid(self, mock_delay):
|
||||||
@mock.patch("apps.lyric.views.requests.post")
|
|
||||||
def test_sends_link_to_login_using_token_uid(self, mock_post):
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
"/apps/lyric/send_login_email", data={"email": "discoman@example.com"}
|
||||||
)
|
)
|
||||||
|
|
||||||
token = Token.objects.get()
|
token = Token.objects.get()
|
||||||
expected_url = f"http://testserver/apps/lyric/login?token={token.uid}"
|
expected_url = f"http://testserver/apps/lyric/login?token={token.uid}"
|
||||||
data = mock_post.call_args.kwargs["data"]
|
self.assertEqual(mock_delay.call_args.args[1], expected_url)
|
||||||
self.assertIn(expected_url, data["text"])
|
|
||||||
|
|
||||||
class LoginViewTest(TestCase):
|
class LoginViewTest(TestCase):
|
||||||
def test_redirects_to_home_page(self):
|
def test_redirects_to_home_page(self):
|
||||||
|
|||||||
16
src/apps/lyric/tests/unit/test_tasks.py
Normal file
16
src/apps/lyric/tests/unit/test_tasks.py
Normal 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"])
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import requests
|
|
||||||
from django.contrib import auth, messages
|
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.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import Token
|
from .models import Token
|
||||||
|
from .tasks import send_login_email_task
|
||||||
|
|
||||||
|
|
||||||
def send_login_email(request):
|
def send_login_email(request):
|
||||||
email = request.POST["email"]
|
email = request.POST["email"]
|
||||||
@@ -12,26 +12,13 @@ def send_login_email(request):
|
|||||||
url = request.build_absolute_uri(
|
url = request.build_absolute_uri(
|
||||||
reverse("login") + "?token=" + str(token.uid),
|
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(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
"Check your email!—there you'll find a magic login link. But hurry… it's only temporary!",
|
"Check your email!—there you'll find a magic login link. But hurry… it's only temporary!",
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
def login(request):
|
def login(request):
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ("celery_app",)
|
||||||
10
src/core/celery.py
Normal file
10
src/core/celery.py
Normal 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()
|
||||||
@@ -109,6 +109,8 @@ else:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|||||||
Reference in New Issue
Block a user