closes #7
This commit is contained in:
@@ -18,7 +18,7 @@ class SupportMessageSerializer(serializers.ModelSerializer):
|
||||
"user_first_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):
|
||||
|
||||
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 django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.template.loader import get_template
|
||||
from django.template import Context
|
||||
from core.services.email.user import UserEmailService
|
||||
from core.services.email.bid import BidEmailService
|
||||
from core.services.email.document import DocumentEmailService
|
||||
from core.services.email.support import SupportEmailService
|
||||
|
||||
class EmailService(object):
|
||||
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)
|
||||
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
|
||||
class EmailService(UserEmailService, BidEmailService, DocumentEmailService, SupportEmailService):
|
||||
"""
|
||||
Legacy EmailService class that combines all specific email services.
|
||||
This maintains backward compatibility with existing code.
|
||||
"""
|
||||
pass
|
||||
|
||||
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,
|
||||
FAQSerializer,
|
||||
)
|
||||
from core.services.email.support import SupportEmailService
|
||||
|
||||
|
||||
class SupportCaseViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return SupportCase.objects.filter(user=self.request.user).order_by(
|
||||
"-updated_at"
|
||||
)
|
||||
user = self.request.user
|
||||
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):
|
||||
if self.action == "retrieve":
|
||||
@@ -25,14 +27,34 @@ class SupportCaseViewSet(viewsets.ModelViewSet):
|
||||
return SupportCaseListSerializer
|
||||
|
||||
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"])
|
||||
def add_message(self, request, pk=None):
|
||||
support_case = self.get_object()
|
||||
serializer = SupportMessageSerializer(data=request.data)
|
||||
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.errors, status=400)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user