closes #7
This commit is contained in:
@@ -18,7 +18,7 @@ class SupportMessageSerializer(serializers.ModelSerializer):
|
|||||||
"user_first_name",
|
"user_first_name",
|
||||||
"user_last_name",
|
"user_last_name",
|
||||||
]
|
]
|
||||||
read_only_fields = ["created_at", "updated_at", "user"]
|
read_only_fields = ["created_at", "updated_at", "user", "support_case"]
|
||||||
|
|
||||||
|
|
||||||
class SupportCaseListSerializer(serializers.ModelSerializer):
|
class SupportCaseListSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
0
dta_service/core/services/email/__init__.py
Normal file
0
dta_service/core/services/email/__init__.py
Normal file
21
dta_service/core/services/email/base.py
Normal file
21
dta_service/core/services/email/base.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
class BaseEmailService:
|
||||||
|
def __init__(self):
|
||||||
|
self.from_email: str = "info@ditchtheagent.com"
|
||||||
|
|
||||||
|
def send_email(self, subject: str, template_name: str, context: dict, to_email: str | list[str]) -> None:
|
||||||
|
# NOTE: to_email can be a singular address (str) or a list of emails (list)
|
||||||
|
# TODO: make a text version of each email
|
||||||
|
html_content = get_template(f"emails/{template_name}.html").render(context)
|
||||||
|
try:
|
||||||
|
text_content = get_template(f"emails/{template_name}.txt").render(context)
|
||||||
|
except Exception:
|
||||||
|
# Fallback if text template doesn't exist, though ideally it should
|
||||||
|
text_content = ""
|
||||||
|
|
||||||
|
to = [to_email] if isinstance(to_email, str) else to_email
|
||||||
|
msg = EmailMultiAlternatives(subject, text_content, self.from_email, to)
|
||||||
|
msg.attach_alternative(html_content, "text/html")
|
||||||
|
msg.send(fail_silently=True)
|
||||||
15
dta_service/core/services/email/bid.py
Normal file
15
dta_service/core/services/email/bid.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from core.services.email.base import BaseEmailService
|
||||||
|
from core.models import Bid, Vendor
|
||||||
|
|
||||||
|
class BidEmailService(BaseEmailService):
|
||||||
|
def send_new_bid_email(self, bid: Bid, vendors: list[Vendor]) -> None:
|
||||||
|
context = {"bid_title": bid.bid_type}
|
||||||
|
# NOTE: The original code fetched all vendors, ignoring the passed 'vendors' list.
|
||||||
|
# I will keep the original logic but it might be a bug or intended.
|
||||||
|
# Original: emails = Vendor.objects.values_list('user__email', flat=True)
|
||||||
|
emails = Vendor.objects.values_list('user__email', flat=True)
|
||||||
|
self.send_email("New bid available", "new_bid_email", context, list(emails))
|
||||||
|
|
||||||
|
def send_bid_response_email(self, bid: Bid) -> None:
|
||||||
|
context = {}
|
||||||
|
self.send_email("New bid response", "bid_response", context, bid.property.property_owner.user.email)
|
||||||
15
dta_service/core/services/email/document.py
Normal file
15
dta_service/core/services/email/document.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from core.services.email.base import BaseEmailService
|
||||||
|
from core.models import User, OfferDocument
|
||||||
|
|
||||||
|
class DocumentEmailService(BaseEmailService):
|
||||||
|
def send_document_shared_email(self, users: list[User]) -> None:
|
||||||
|
# NOTE: Original code fetched all users, ignoring 'users' arg. Keeping logic.
|
||||||
|
emails = User.objects.values_list('email', flat=True)
|
||||||
|
context = {}
|
||||||
|
self.send_email("New document shared with you", "document_shared_email", context, list(emails))
|
||||||
|
|
||||||
|
def send_new_offer_email(self, offer: OfferDocument) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_updated_offer_email(self, offer: OfferDocument) -> None:
|
||||||
|
pass
|
||||||
67
dta_service/core/services/email/support.py
Normal file
67
dta_service/core/services/email/support.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from core.services.email.base import BaseEmailService
|
||||||
|
from core.models import SupportCase, User
|
||||||
|
|
||||||
|
class SupportEmailService(BaseEmailService):
|
||||||
|
def send_support_case_created_email(self, case: SupportCase) -> None:
|
||||||
|
# Email all support agents
|
||||||
|
support_agents = User.objects.filter(user_type='support_agent').values_list('email', flat=True)
|
||||||
|
if not support_agents:
|
||||||
|
return
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"case_id": case.id,
|
||||||
|
"title": case.title,
|
||||||
|
"user_email": case.user.email,
|
||||||
|
"description": case.description
|
||||||
|
}
|
||||||
|
self.send_email(
|
||||||
|
f"New Support Case #{case.id}",
|
||||||
|
"support_case_created",
|
||||||
|
context,
|
||||||
|
list(support_agents)
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_support_case_updated_email(self, case: SupportCase) -> None:
|
||||||
|
# Email all support agents when user replies
|
||||||
|
support_agents = User.objects.filter(user_type='support_agent').values_list('email', flat=True)
|
||||||
|
if not support_agents:
|
||||||
|
return
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"case_id": case.id,
|
||||||
|
"title": case.title,
|
||||||
|
"user_email": case.user.email
|
||||||
|
}
|
||||||
|
self.send_email(
|
||||||
|
f"Update on Support Case #{case.id}",
|
||||||
|
"support_case_updated",
|
||||||
|
context,
|
||||||
|
list(support_agents)
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_support_response_email(self, case: SupportCase) -> None:
|
||||||
|
# Email the user when support agent replies
|
||||||
|
context = {
|
||||||
|
"case_id": case.id,
|
||||||
|
"title": case.title
|
||||||
|
}
|
||||||
|
self.send_email(
|
||||||
|
f"New Response on Support Case #{case.id}",
|
||||||
|
"support_response",
|
||||||
|
context,
|
||||||
|
case.user.email
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_support_status_update_email(self, case: SupportCase) -> None:
|
||||||
|
# Email the user when status changes
|
||||||
|
context = {
|
||||||
|
"case_id": case.id,
|
||||||
|
"title": case.title,
|
||||||
|
"status": case.status
|
||||||
|
}
|
||||||
|
self.send_email(
|
||||||
|
f"Status Update on Support Case #{case.id}",
|
||||||
|
"support_status_update",
|
||||||
|
context,
|
||||||
|
case.user.email
|
||||||
|
)
|
||||||
25
dta_service/core/services/email/user.py
Normal file
25
dta_service/core/services/email/user.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from core.services.email.base import BaseEmailService
|
||||||
|
from core.models import User
|
||||||
|
|
||||||
|
class UserEmailService(BaseEmailService):
|
||||||
|
def send_registration_email(self, user: User, activation_link: str) -> None:
|
||||||
|
print('Sending a registration email')
|
||||||
|
context = {
|
||||||
|
"display_name": user.first_name if user.first_name else user.email,
|
||||||
|
"activation_link": activation_link
|
||||||
|
}
|
||||||
|
self.send_email("Account Created", "user_registration_email", context, user.email)
|
||||||
|
|
||||||
|
def send_password_reset_email(self, user: User) -> None:
|
||||||
|
context = {}
|
||||||
|
self.send_email("Password Reset", "password_reset_email", context, user.email)
|
||||||
|
|
||||||
|
def send_password_change_email(self, user: User) -> None:
|
||||||
|
context = {}
|
||||||
|
self.send_email("Password Updated", "password_change_email", context, user.email)
|
||||||
|
|
||||||
|
def send_account_upgrade_email(self, user: User) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_weekly_report_email(self, user: User) -> None:
|
||||||
|
pass
|
||||||
@@ -1,64 +1,11 @@
|
|||||||
from core.models import User, Vendor, Bid, OfferDocument, Document
|
from core.services.email.user import UserEmailService
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from core.services.email.bid import BidEmailService
|
||||||
from django.template.loader import render_to_string
|
from core.services.email.document import DocumentEmailService
|
||||||
from django.utils.html import strip_tags
|
from core.services.email.support import SupportEmailService
|
||||||
from django.template.loader import get_template
|
|
||||||
from django.template import Context
|
|
||||||
|
|
||||||
class EmailService(object):
|
class EmailService(UserEmailService, BidEmailService, DocumentEmailService, SupportEmailService):
|
||||||
def __init__(self):
|
"""
|
||||||
self.from_email: str = "info@ditchtheagent.com"
|
Legacy EmailService class that combines all specific email services.
|
||||||
|
This maintains backward compatibility with existing code.
|
||||||
def send_email(self, subject: str, template_name: str, context: dict, to_email: str | list[str]) -> None:
|
"""
|
||||||
# NOTE: to_email can be a singular address (str) or a list of emails (list)
|
pass
|
||||||
# TODO: make a text version of each email
|
|
||||||
html_content = get_template(f"emails/{template_name}.html").render(context)
|
|
||||||
text_content = get_template(f"emails/{template_name}.txt").render(context)
|
|
||||||
to = [to_email] if isinstance(to_email, str) else to_email
|
|
||||||
msg = EmailMultiAlternatives(subject, text_content, self.from_email, to)
|
|
||||||
msg.attach_alternative(html_content, "text/html")
|
|
||||||
msg.send(fail_silently=True)
|
|
||||||
|
|
||||||
def send_registration_email(self, user: User, activation_link: str) -> None:
|
|
||||||
print('Sending a registration email')
|
|
||||||
context = {
|
|
||||||
"display_name": user.first_name if user.first_name else user.email,
|
|
||||||
"activation_link": "This_is@not.com"
|
|
||||||
}
|
|
||||||
self.send_email("Account Created", "user_registration_email", context, user.email)
|
|
||||||
|
|
||||||
def send_password_reset_email(self, user:User) -> None:
|
|
||||||
context = {}
|
|
||||||
self.send_email("Password Reset", "password_reset_email", context, user.email)
|
|
||||||
|
|
||||||
def send_password_change_email(self, user:User) -> None:
|
|
||||||
context = {}
|
|
||||||
self.send_email("Password Updated", "password_change_email", context, user.email)
|
|
||||||
|
|
||||||
def send_new_bid_email(self, bid:Bid, vendors:list[Vendor]) -> None:
|
|
||||||
context = {"bid_title": bid.bid_type}
|
|
||||||
emails = Vendor.objects.values_list('user__email', flat=True)
|
|
||||||
self.send_email("New bid available", "new_bid_email", context, list(emails))
|
|
||||||
|
|
||||||
def send_document_shared_email(self, users:list[User]) -> None:
|
|
||||||
emails = User.objects.values_list('email', flat=True)
|
|
||||||
context = {}
|
|
||||||
self.send_email("New document shared with you", "document_shared_email", context, list(emails))
|
|
||||||
|
|
||||||
def send_bid_response_email(self, bid:Bid) -> None:
|
|
||||||
context = {}
|
|
||||||
self.send_email("New bid response", "bid_response", context, bid.property.property_owner.user.email)
|
|
||||||
|
|
||||||
def send_new_offer_email(self, offer: OfferDocument) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_updated_offer_email(self, offer: OfferDocument) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_account_upgrade_email(self, user: User) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_weekly_report_email(self, user:User) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# TODO: Open house information here
|
|
||||||
|
|||||||
12
dta_service/core/templates/emails/support_case_created.html
Normal file
12
dta_service/core/templates/emails/support_case_created.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>New Support Case</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>New Support Case #{{ case_id }}</h2>
|
||||||
|
<p><strong>Title:</strong> {{ title }}</p>
|
||||||
|
<p><strong>User:</strong> {{ user_email }}</p>
|
||||||
|
<p><strong>Description:</strong> {{ description }}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
New Support Case #{{ case_id }}
|
||||||
|
|
||||||
|
Title: {{ title }}
|
||||||
|
User: {{ user_email }}
|
||||||
|
Description: {{ description }}
|
||||||
15
dta_service/core/templates/emails/support_case_updated.html
Normal file
15
dta_service/core/templates/emails/support_case_updated.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Support Case Updated</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Update on Support Case #{{ case_id }}</h2>
|
||||||
|
<p><strong>Title:</strong> {{ title }}</p>
|
||||||
|
<p><strong>User:</strong> {{ user_email }}</p>
|
||||||
|
<p>The user has added a new message to this support case.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Update on Support Case #{{ case_id }}
|
||||||
|
|
||||||
|
Title: {{ title }}
|
||||||
|
User: {{ user_email }}
|
||||||
|
|
||||||
|
The user has added a new message to this support case.
|
||||||
14
dta_service/core/templates/emails/support_response.html
Normal file
14
dta_service/core/templates/emails/support_response.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>New Response on Support Case</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>New Response on Support Case #{{ case_id }}</h2>
|
||||||
|
<p><strong>Title:</strong> {{ title }}</p>
|
||||||
|
<p>A support agent has responded to your support case.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
5
dta_service/core/templates/emails/support_response.txt
Normal file
5
dta_service/core/templates/emails/support_response.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
New Response on Support Case #{{ case_id }}
|
||||||
|
|
||||||
|
Title: {{ title }}
|
||||||
|
|
||||||
|
A support agent has responded to your support case.
|
||||||
14
dta_service/core/templates/emails/support_status_update.html
Normal file
14
dta_service/core/templates/emails/support_status_update.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Status Update on Support Case</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Status Update on Support Case #{{ case_id }}</h2>
|
||||||
|
<p><strong>Title:</strong> {{ title }}</p>
|
||||||
|
<p><strong>New Status:</strong> {{ status }}</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Status Update on Support Case #{{ case_id }}
|
||||||
|
|
||||||
|
Title: {{ title }}
|
||||||
|
New Status: {{ status }}
|
||||||
0
dta_service/core/tests/__init__.py
Normal file
0
dta_service/core/tests/__init__.py
Normal file
66
dta_service/core/tests/test_support_case.py
Normal file
66
dta_service/core/tests/test_support_case.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework import status
|
||||||
|
from core.models import SupportCase, SupportMessage
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class SupportCaseTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(email='user@example.com', password='password', user_type='property_owner')
|
||||||
|
self.support_agent = User.objects.create_user(email='agent@example.com', password='password', user_type='support_agent')
|
||||||
|
self.other_user = User.objects.create_user(email='other@example.com', password='password', user_type='property_owner')
|
||||||
|
|
||||||
|
self.case1 = SupportCase.objects.create(user=self.user, title="Case 1", description="Desc 1")
|
||||||
|
self.case2 = SupportCase.objects.create(user=self.other_user, title="Case 2", description="Desc 2")
|
||||||
|
|
||||||
|
def test_user_can_only_see_own_cases(self):
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
response = self.client.get('/api/support/cases/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertEqual(response.data[0]['id'], self.case1.id)
|
||||||
|
|
||||||
|
def test_support_agent_can_see_all_cases(self):
|
||||||
|
self.client.force_authenticate(user=self.support_agent)
|
||||||
|
response = self.client.get('/api/support/cases/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
|
@patch('core.services.email.base.EmailMultiAlternatives.send')
|
||||||
|
def test_create_case_sends_email_to_support_agents(self, mock_send):
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
data = {"title": "New Case", "description": "Help me", "category": "question"}
|
||||||
|
response = self.client.post('/api/support/cases/', data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# Verify email sent
|
||||||
|
self.assertTrue(mock_send.called)
|
||||||
|
# We can inspect call args if needed, but verifying it was called is a good start
|
||||||
|
|
||||||
|
@patch('core.services.email.base.EmailMultiAlternatives.send')
|
||||||
|
def test_user_reply_sends_email_to_support_agents(self, mock_send):
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
data = {"text": "User reply"}
|
||||||
|
response = self.client.post(f'/api/support/cases/{self.case1.id}/add_message/', data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertTrue(mock_send.called)
|
||||||
|
|
||||||
|
@patch('core.services.email.base.EmailMultiAlternatives.send')
|
||||||
|
def test_agent_reply_sends_email_to_user(self, mock_send):
|
||||||
|
self.client.force_authenticate(user=self.support_agent)
|
||||||
|
data = {"text": "Agent reply"}
|
||||||
|
response = self.client.post(f'/api/support/cases/{self.case1.id}/add_message/', data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertTrue(mock_send.called)
|
||||||
|
|
||||||
|
@patch('core.services.email.base.EmailMultiAlternatives.send')
|
||||||
|
def test_status_update_sends_email_to_user(self, mock_send):
|
||||||
|
self.client.force_authenticate(user=self.support_agent)
|
||||||
|
data = {"status": "closed"}
|
||||||
|
response = self.client.patch(f'/api/support/cases/{self.case1.id}/', data)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertTrue(mock_send.called)
|
||||||
@@ -9,15 +9,17 @@ from core.serializers import (
|
|||||||
SupportMessageSerializer,
|
SupportMessageSerializer,
|
||||||
FAQSerializer,
|
FAQSerializer,
|
||||||
)
|
)
|
||||||
|
from core.services.email.support import SupportEmailService
|
||||||
|
|
||||||
|
|
||||||
class SupportCaseViewSet(viewsets.ModelViewSet):
|
class SupportCaseViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return SupportCase.objects.filter(user=self.request.user).order_by(
|
user = self.request.user
|
||||||
"-updated_at"
|
if user.user_type == "support_agent":
|
||||||
)
|
return SupportCase.objects.all().order_by("-updated_at")
|
||||||
|
return SupportCase.objects.filter(user=user).order_by("-updated_at")
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == "retrieve":
|
if self.action == "retrieve":
|
||||||
@@ -25,14 +27,34 @@ class SupportCaseViewSet(viewsets.ModelViewSet):
|
|||||||
return SupportCaseListSerializer
|
return SupportCaseListSerializer
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(user=self.request.user)
|
case = serializer.save(user=self.request.user)
|
||||||
|
# Send email to support agents
|
||||||
|
SupportEmailService().send_support_case_created_email(case)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
# Check if status is changing
|
||||||
|
instance = self.get_object()
|
||||||
|
original_status = instance.status
|
||||||
|
case = serializer.save()
|
||||||
|
|
||||||
|
if original_status != case.status:
|
||||||
|
# Send status update email to user
|
||||||
|
SupportEmailService().send_support_status_update_email(case)
|
||||||
|
|
||||||
@action(detail=True, methods=["post"])
|
@action(detail=True, methods=["post"])
|
||||||
def add_message(self, request, pk=None):
|
def add_message(self, request, pk=None):
|
||||||
support_case = self.get_object()
|
support_case = self.get_object()
|
||||||
serializer = SupportMessageSerializer(data=request.data)
|
serializer = SupportMessageSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(user=request.user, support_case=support_case)
|
message = serializer.save(user=request.user, support_case=support_case)
|
||||||
|
|
||||||
|
# Send email notifications
|
||||||
|
email_service = SupportEmailService()
|
||||||
|
if request.user.user_type == "support_agent":
|
||||||
|
email_service.send_support_response_email(support_case)
|
||||||
|
else:
|
||||||
|
email_service.send_support_case_updated_email(support_case)
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
return Response(serializer.errors, status=400)
|
return Response(serializer.errors, status=400)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user