Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ repos:

# Run the Ruff linter.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.8
rev: v0.15.6
hooks:
# Linter
- id: ruff
types_or: [python, pyi, jupyter, toml]
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, pyi, jupyter, pyproject]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also update the version here...

- repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.6
    hooks:
      # Linter
      - id: ruff
        types_or: [python, pyi, jupyter, pyproject]
        args: [--fix]
      # Formatter
      - id: ruff-format
        types_or: [python, pyi, jupyter, pyproject]

args: [--fix]
# Formatter
- id: ruff-format
types_or: [python, pyi, jupyter, toml]
types_or: [python, pyi, jupyter, pyproject]

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.398
Expand Down
26 changes: 26 additions & 0 deletions apps/common/management/commands/run_celery_dev.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import shlex
import subprocess
import typing
from pathlib import Path

from django.core.management.base import BaseCommand
from django.utils import autoreload

WORKER_STATE_DIR = Path("/var/run/celery")

CMD = "celery -A main worker -E --concurrency=2 -l info"


def restart_celery(*args: typing.Any, **kwargs: typing.Any):
kill_worker_cmd = "pkill -9 celery"
subprocess.call(shlex.split(kill_worker_cmd)) # noqa: S603
subprocess.call(shlex.split(CMD)) # noqa: S603


class Command(BaseCommand):
@typing.override
def handle(self, *args: typing.Any, **options: typing.Any):
self.stdout.write("Starting celery worker with autoreload...")
if not Path.exists(WORKER_STATE_DIR):
Path.mkdir(WORKER_STATE_DIR, parents=True)
autoreload.run_with_reloader(restart_celery, args=None, kwargs=None)
27 changes: 27 additions & 0 deletions apps/common/management/commands/wait_for_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

import requests
from django.conf import settings
from django.core.cache import cache
from django.core.management.base import BaseCommand, CommandParser
from django.db import connections
from django.db.utils import OperationalError
from redis.exceptions import ConnectionError as RedisConnectionError


class TimeoutException(Exception): ...
Expand Down Expand Up @@ -37,6 +39,25 @@ def wait_for_db(self):

self.stdout.write(self.style.SUCCESS(f"DB is available after {time.time() - start_time} seconds"))

def wait_for_redis(self):
self.stdout.write("Waiting for Redis...")
redis_conn = None
start_time = time.time()
while True:
try:
cache.set("wait-for-it-ping", "pong", timeout=1) # Set a key to check Redis availability
redis_conn = cache.get("wait-for-it-ping") # Try to get the value back from Redis
if redis_conn != "pong":
raise TypeError
break
except (RedisConnectionError, TypeError):
...
# Try again
self.stdout.write(self.style.WARNING("Redis not available, waiting..."))
time.sleep(1)

self.stdout.write(self.style.SUCCESS(f"Redis is available after {time.time() - start_time} seconds"))

def wait_for_minio(self):
self.stdout.write("Waiting for Minio...")
AWS_S3_CONFIG_OPTIONS = getattr(settings, "AWS_S3_CONFIG_OPTIONS", None) or {}
Expand Down Expand Up @@ -69,6 +90,8 @@ def add_arguments(self, parser: CommandParser):
help="The maximum time (in seconds) the command is allowed to run before timing out. Default is 10 min.",
)
parser.add_argument("--db", action="store_true", help="Wait for DB to be available")
parser.add_argument("--celery-queue", action="store_true", help="Wait for Celery queue to be available")
parser.add_argument("--redis", action="store_true", help="Wait for Redis to be available")
parser.add_argument("--minio", action="store_true", help="Wait for MinIO (S3) storage to be available")
parser.add_argument("--all", action="store_true", help="Wait for all to be available")

Expand All @@ -86,6 +109,10 @@ def handle(self, **kwargs: typing.Any):
self.wait_for_db()
if _all or kwargs["minio"]:
self.wait_for_minio()
if _all or kwargs["redis"]:
self.wait_for_redis()
if _all or kwargs["celery_queue"]:
self.wait_for_redis()
except TimeoutException:
...
finally:
Expand Down
Empty file.
15 changes: 15 additions & 0 deletions apps/common/templatetags/custom_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django import template
from django.conf import settings
from django.templatetags.static import static

register = template.Library()


@register.filter(is_safe=True)
def static_full_path(path):
static_path = static(path)
# Domain from URL object
domain = f"{settings.APP_DOMAIN.scheme}://{settings.APP_DOMAIN.hostname}"
if settings.APP_DOMAIN.port:
domain += f":{settings.APP_DOMAIN.port}"
return f"{domain}{static_path}"
2 changes: 1 addition & 1 deletion apps/resources/graphql/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ class RequestDemoInput:
email: str
content: str
national_society: str
tool: int
tool: strawberry.ID
captcha_hashkey: str
captcha_code: str
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Generated by Django 5.2.9 on 2026-04-03 04:51

import django.db.models.deletion
from django.db import migrations, models

Expand Down
15 changes: 13 additions & 2 deletions apps/resources/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import typing

from captcha.serializers import CaptchaModelSerializer
from django.db import transaction
from rest_framework import serializers

from apps.resources.models import ContactRequest, RequestDemo
from apps.tool_picker.models import Tool

from .tasks import send_contact_request_email, send_demo_request_email


class ContactRequestSerializer(CaptchaModelSerializer):
captcha_code = serializers.CharField(write_only=True)
Expand All @@ -26,7 +29,11 @@ class Meta:
def create(self, validated_data: dict[str, typing.Any]):
validated_data.pop("captcha_code", None)
validated_data.pop("captcha_hashkey", None)
return super().create(validated_data)
instance = super().create(validated_data)
transaction.on_commit(
lambda: send_contact_request_email.delay(instance.id), # type: ignore[reportIncompatibleVariableOverride]
)
return instance


class RequestDemoSerializer(CaptchaModelSerializer):
Expand All @@ -53,4 +60,8 @@ class Meta:
def create(self, validated_data: dict[str, typing.Any]):
validated_data.pop("captcha_code", None)
validated_data.pop("captcha_hashkey", None)
return super().create(validated_data)
instance = super().create(validated_data)
transaction.on_commit(
lambda: send_demo_request_email.delay(instance.id), # type: ignore[reportIncompatibleVariableOverride]
)
return instance
57 changes: 57 additions & 0 deletions apps/resources/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import logging

from celery import shared_task
from django.conf import settings
from django.template.loader import render_to_string

from apps.resources.models import ContactRequest, RequestDemo
from apps.resources.utils import get_contact_request_email_context, get_demo_request_email_context
from utils.email.service import send_email

logger = logging.getLogger(__name__)


@shared_task
def send_contact_request_email(contact_id: int):
instance = ContactRequest.objects.filter(id=contact_id).first()
if not instance:
return None

recipient = settings.EMAIL_TO
email_subject = "You got a new feedback!"
email_context = get_contact_request_email_context(instance)
html_body = render_to_string("email/contact_request.html", email_context)

send_email(
subject=email_subject,
to_email=[recipient],
html=html_body,
)
return True


@shared_task
def send_demo_request_email(request_demo_id: int):
instance = (
RequestDemo.objects.select_related("tool").prefetch_related("tool__tool_owners").filter(id=request_demo_id).first()
)
if not instance:
return None

tool_owners = instance.tool.tool_owners.all()

if not tool_owners.exists():
logger.info("skipping cause there are no tool owners associated with this tool")
return False

recipients = list(tool_owners.values_list("email", flat=True))
email_subject = "You got a new request for demo!"
email_context = get_demo_request_email_context(instance)
html = render_to_string("email/request_demo.html", email_context)
send_email(
subject=email_subject,
to_email=recipients,
html=html,
cc_email=[settings.EMAIL_TO],
)
return True
42 changes: 26 additions & 16 deletions apps/resources/tests/mutation_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing
from unittest.mock import call, patch

from captcha.models import CaptchaStore

Expand Down Expand Up @@ -44,14 +45,16 @@ def setUpClass(cls):
cls.user = UserFactory.create(email="test@gmail.com")

def _create_contact_request_mutation(self, data: dict[str, str], **kwargs: typing.Any):
return self.query_check(
query=self.Mutation.CREATE_CONTACT_REQUEST,
variables={
"data": data,
},
)

def test_create_contact_request(self):
with self.captureOnCommitCallbacks(execute=True):
return self.query_check(
query=self.Mutation.CREATE_CONTACT_REQUEST,
variables={
"data": data,
},
)

@patch("apps.resources.serializers.send_contact_request_email.delay")
def test_create_contact_request(self, mock_requests): # type: ignore[reportMissingParameterType]
# Generates CAPTCHA key and value
captcha_key = CaptchaStore.generate_key()
captcha_obj = CaptchaStore.objects.get(hashkey=captcha_key)
Expand Down Expand Up @@ -81,6 +84,9 @@ def test_create_contact_request(self):
"nationalSociety": contact_request.national_society,
}

mock_requests.assert_called_once()
mock_requests.assert_has_calls([call(contact_request.pk)])

def test_create_contact_request_without_captcha(self):
contact_request_data = {
"name": "John",
Expand Down Expand Up @@ -157,14 +163,16 @@ def setUpClass(cls):
cls.tool = ToolFactory.create()

def _create_tool_demo_request_mutation(self, data: dict[str, str | int], **kwargs: typing.Any):
return self.query_check(
query=self.Mutation.CREATE_DEMO_REQUEST,
variables={
"data": data,
},
)

def test_create_tool_demo_request(self):
with self.captureOnCommitCallbacks(execute=True):
return self.query_check(
query=self.Mutation.CREATE_DEMO_REQUEST,
variables={
"data": data,
},
)

@patch("apps.resources.serializers.send_demo_request_email.delay")
def test_create_tool_demo_request(self, mock_requests): # type: ignore[reportMissingParameterType]
captcha_key = CaptchaStore.generate_key()
captcha_obj = CaptchaStore.objects.get(hashkey=captcha_key)
captcha_code = captcha_obj.response
Expand Down Expand Up @@ -196,6 +204,8 @@ def test_create_tool_demo_request(self):
"pk": self.gID(demo_request.tool.pk),
},
}
mock_requests.assert_called_once()
mock_requests.assert_has_calls([call(demo_request.pk)])

def test_create_tool_demo_request_without_captcha(self):
tool_demo_request_data = {
Expand Down
28 changes: 28 additions & 0 deletions apps/resources/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from apps.resources.models import ContactRequest, RequestDemo


def get_contact_request_email_context(instance: ContactRequest):
from .serializers import ContactRequestSerializer

data = dict(ContactRequestSerializer(instance).data)

return {
"name": data["name"],
"email": data["email"],
"national_society": data["national_society"],
"content": data["content"],
}


def get_demo_request_email_context(instance: RequestDemo):
from .serializers import RequestDemoSerializer

data = dict(RequestDemoSerializer(instance).data)
tool_name = instance.tool.name
return {
"name": data["name"],
"email": data["email"],
"national_society": data["national_society"],
"content": data["content"],
"tool": tool_name,
}
2 changes: 1 addition & 1 deletion apps/tool_picker/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ class ToolAnswer(UserResource):

# typing
question_id: typing.ClassVar[int]
ordinal_value: int | None
ordinal_value: int | None # noqa:PIE794

class Meta(UserResource.Meta):
verbose_name = "Tool Answer"
Expand Down
Loading
Loading