Skip to content

Commit 025350f

Browse files
committed
sending ics file, one email is sent to user and admins
1 parent d9cadf8 commit 025350f

File tree

5 files changed

+276
-139
lines changed

5 files changed

+276
-139
lines changed

appointment/email_sender/email_sender.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
# email_sender.py
22
# Path: appointment/email_sender/email_sender.py
33
import os
4+
from datetime import datetime, timedelta
5+
from typing import Any, Optional, Tuple
46

57
from django.conf import settings
68
from django.core.mail import send_mail
79
from django.template import loader
10+
from django.utils import timezone
811

912
from appointment.logger_config import get_logger
1013
from appointment.settings import APP_DEFAULT_FROM_EMAIL, check_q_cluster
1114

1215
logger = get_logger(__name__)
1316

1417
try:
15-
from django_q.tasks import async_task
18+
from django_q.tasks import async_task, schedule
19+
from django_q.models import Schedule
20+
1621

1722
DJANGO_Q_AVAILABLE = True
1823
except ImportError:
1924
async_task = None
25+
schedule = None
26+
Schedule = None
27+
2028
DJANGO_Q_AVAILABLE = False
2129
logger.warning("django-q is not installed. Email will be sent synchronously.")
2230

@@ -87,6 +95,146 @@ def send_email(recipient_list, subject: str, template_url: str = None, context:
8795
logger.error(f"Error sending email: {e}")
8896

8997

98+
def validate_required_fields(recipient_list: list, subject: str) -> Tuple[bool, str]:
99+
if not recipient_list or not subject:
100+
return False, "Recipient list and subject are required."
101+
return True, ""
102+
103+
104+
def validate_and_process_datetime(dt: Optional[datetime], field_name: str) -> Tuple[bool, str, Optional[datetime]]:
105+
if not dt:
106+
return True, "", None
107+
108+
if not isinstance(dt, datetime):
109+
try:
110+
dt = datetime.fromisoformat(dt)
111+
except ValueError:
112+
return False, f"Invalid {field_name} format. Use ISO format or datetime object.", None
113+
114+
if dt.tzinfo is None:
115+
dt = timezone.make_aware(dt)
116+
117+
return True, "", dt
118+
119+
120+
def validate_send_at(send_at: Optional[datetime]) -> tuple[bool, str, None] | tuple[
121+
bool, str, datetime | None | timedelta | Any]:
122+
success, message, processed_send_at = validate_and_process_datetime(send_at, "send_at")
123+
if not success:
124+
return success, message, None
125+
126+
if processed_send_at and processed_send_at <= timezone.now():
127+
return False, "send_at must be in the future.", None
128+
129+
return True, "", processed_send_at or (timezone.now() + timedelta(minutes=1))
130+
131+
132+
def validate_repeat_until(repeat_until: Optional[datetime], send_at: datetime) -> Tuple[bool, str, Optional[datetime]]:
133+
success, message, processed_repeat_until = validate_and_process_datetime(repeat_until, "repeat_until")
134+
if not success:
135+
return success, message, None
136+
137+
if processed_repeat_until and processed_repeat_until <= send_at:
138+
return False, "repeat_until must be after send_at.", None
139+
140+
return True, "", processed_repeat_until
141+
142+
143+
def validate_repeat_option(repeat: Optional[str]) -> Tuple[bool, str, Optional[str]]:
144+
valid_options = ['HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'YEARLY']
145+
if repeat and repeat not in valid_options:
146+
return False, f"Invalid repeat option. Choose from {', '.join(valid_options)}.", None
147+
return True, "", repeat
148+
149+
150+
def schedule_email_task(
151+
recipient_list: list,
152+
subject: str,
153+
html_message: str,
154+
from_email: str,
155+
attachments: Optional[list],
156+
schedule_type: str,
157+
send_at: datetime,
158+
name: Optional[str],
159+
repeat_until: Optional[datetime]
160+
) -> Tuple[bool, str]:
161+
try:
162+
schedule(
163+
'appointment.tasks.send_email_task',
164+
recipient_list=recipient_list,
165+
subject=subject,
166+
message=None,
167+
html_message=html_message,
168+
from_email=from_email,
169+
attachments=attachments,
170+
schedule_type=schedule_type,
171+
next_run=send_at,
172+
name=name,
173+
repeats=-1 if schedule_type != Schedule.ONCE and not repeat_until else None,
174+
end_date=repeat_until
175+
)
176+
return True, "Email scheduled successfully."
177+
except Exception as e:
178+
logger.error(f"Error scheduling email: {e}")
179+
return False, f"Error scheduling email: {str(e)}"
180+
181+
182+
def schedule_email_sending(
183+
recipient_list: list,
184+
subject: str,
185+
template_url: Optional[str] = None,
186+
context: Optional[dict] = None,
187+
from_email: Optional[str] = None,
188+
attachments: Optional[list] = None,
189+
send_at: Optional[datetime] = None,
190+
name: Optional[str] = None,
191+
repeat: Optional[str] = None,
192+
repeat_until: Optional[datetime] = None
193+
) -> Tuple[bool, str]:
194+
if not has_required_email_settings():
195+
return False, "Email settings are not configured."
196+
197+
if not check_q_cluster() or not DJANGO_Q_AVAILABLE:
198+
return False, "Django-Q is not available."
199+
200+
# Validate required fields
201+
success, message = validate_required_fields(recipient_list, subject)
202+
if not success:
203+
return success, message
204+
205+
# Validate and process send_at
206+
success, message, processed_send_at = validate_send_at(send_at)
207+
if not success:
208+
return success, message
209+
210+
# Validate repeat option
211+
success, message, validated_repeat = validate_repeat_option(repeat)
212+
if not success:
213+
return success, message
214+
215+
# Validate repeat_until
216+
success, message, processed_repeat_until = validate_repeat_until(repeat_until, processed_send_at)
217+
if not success:
218+
return success, message
219+
220+
from_email = from_email or APP_DEFAULT_FROM_EMAIL
221+
html_message = render_email_template(template_url, context)
222+
223+
schedule_type = getattr(Schedule, validated_repeat or 'ONCE')
224+
225+
return schedule_email_task(
226+
recipient_list,
227+
subject,
228+
html_message,
229+
from_email,
230+
attachments,
231+
schedule_type,
232+
processed_send_at,
233+
name,
234+
processed_repeat_until
235+
)
236+
237+
90238
def notify_admin(subject: str, template_url: str = None, context: dict = None, message: str = None,
91239
recipient_email: str = None, attachments=None):
92240
if not has_required_email_settings():

appointment/templates/email_sender/reminder_email.html

Lines changed: 94 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,58 +7,105 @@
77
<title>{% translate 'Appointment Reminder' %}</title>
88
<style>
99
body {
10-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
1111
margin: 0;
12-
padding: 20px;
13-
background-color: #f0f0f0;
14-
color: #636363;
12+
padding: 0;
13+
background-color: #f4f4f4;
14+
color: #333333;
1515
}
1616

1717
.email-container {
1818
max-width: 600px;
19-
background: #ffffff;
2019
margin: 0 auto;
21-
padding: 25px;
22-
border-radius: 8px;
23-
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
20+
background: #ffffff;
2421
}
2522

2623
.email-header {
24+
background-color: #2c3e50;
2725
color: #ffffff;
28-
background-color: rgb(5, 100, 129);
29-
padding: 15px;
30-
border-top-left-radius: 8px;
31-
border-top-right-radius: 8px;
26+
padding: 20px 15px;
3227
text-align: center;
3328
}
3429

30+
.email-header h2 {
31+
margin: 0;
32+
font-size: 22px;
33+
font-weight: 600;
34+
}
35+
3536
.email-body {
36-
padding: 25px;
37-
line-height: 1.6;
37+
padding: 20px 15px;
38+
line-height: 1.5;
3839
}
3940

40-
.email-footer {
41-
padding-top: 20px;
41+
.email-body p {
42+
margin-bottom: 15px;
43+
}
44+
45+
.appointment-info {
46+
background-color: #f8f8f8;
47+
border: 1px solid #e0e0e0;
48+
border-radius: 8px;
49+
padding: 15px;
50+
margin-bottom: 20px;
51+
}
52+
53+
.appointment-info h3 {
54+
margin-top: 0;
55+
color: #2c3e50;
56+
border-bottom: 2px solid #3498db;
57+
padding-bottom: 10px;
58+
margin-bottom: 15px;
59+
font-size: 20px;
60+
}
61+
62+
.info-row {
63+
margin-bottom: 15px;
64+
}
65+
66+
.info-label {
67+
font-weight: 600;
68+
color: #7f8c8d;
69+
display: block;
70+
margin-bottom: 5px;
4271
font-size: 14px;
43-
text-align: left;
44-
color: #aaaaaa;
72+
}
73+
74+
.info-value {
75+
display: block;
76+
font-size: 16px;
77+
color: #2c3e50;
78+
}
79+
80+
.email-footer {
81+
background-color: #f8f8f8;
82+
padding: 15px;
83+
font-size: 12px;
84+
color: #7f8c8d;
85+
text-align: center;
86+
border-top: 1px solid #e0e0e0;
4587
}
4688

4789
.button {
48-
background-color: rgb(5, 100, 129);
90+
display: block;
91+
background-color: #3498db;
4992
color: #ffffff;
50-
padding: 10px 20px;
93+
padding: 12px 20px;
5194
border-radius: 5px;
5295
text-decoration: none;
53-
display: inline-block;
96+
font-weight: 600;
5497
margin-top: 20px;
55-
text-decoration-color: white;
98+
text-align: center;
5699
}
57100

58101
.button:hover {
59-
background-color: #37aee9;
60-
color: #ffffff;
61-
text-decoration: none;
102+
background-color: #2980b9;
103+
}
104+
105+
@media only screen and (max-width: 480px) {
106+
.email-container {
107+
width: 100%;
108+
}
62109
}
63110
</style>
64111
</head>
@@ -76,11 +123,27 @@ <h2>{% translate 'Appointment Reminder' %}</h2>
76123
{% endif %}
77124
</p>
78125
<p>{% translate 'This is a reminder for your upcoming appointment.' %}</p>
79-
<p><strong>{% translate 'Service' %}:</strong> {{ appointment.get_service_name }}</p>
80-
<p><strong>{% translate 'Date' %}:</strong> {{ appointment.appointment_request.date }}</p>
81-
<p><strong>{% translate 'Time' %}:</strong> {{ appointment.appointment_request.start_time }}
82-
- {{ appointment.appointment_request.end_time }}</p>
83-
<p><strong>{% translate 'Location' %}:</strong> {{ appointment.address }}</p>
126+
127+
<div class="appointment-info">
128+
<h3>{% translate 'Appointment Details' %}</h3>
129+
<div class="info-row">
130+
<span class="info-label">{% translate 'Service' %}</span>
131+
<span class="info-value">{{ appointment.get_service_name }}</span>
132+
</div>
133+
<div class="info-row">
134+
<span class="info-label">{% translate 'Date' %}</span>
135+
<span class="info-value">{{ appointment.appointment_request.date }}</span>
136+
</div>
137+
<div class="info-row">
138+
<span class="info-label">{% translate 'Time' %}</span>
139+
<span class="info-value">{{ appointment.appointment_request.start_time }} - {{ appointment.appointment_request.end_time }}</span>
140+
</div>
141+
<div class="info-row">
142+
<span class="info-label">{% translate 'Location' %}</span>
143+
<span class="info-value">{{ appointment.address }}</span>
144+
</div>
145+
</div>
146+
84147
{% if recipient_type == 'client' %}
85148
<p>{% translate 'If you need to reschedule, please click the button below or contact us for further assistance.' %}</p>
86149
<a href="{{ reschedule_link }}" class="button">{% translate 'Reschedule Appointment' %}</a>
@@ -94,4 +157,4 @@ <h2>{% translate 'Appointment Reminder' %}</h2>
94157
</div>
95158
</div>
96159
</body>
97-
</html>
160+
</html>

0 commit comments

Comments
 (0)