Skip to content

Commit d961970

Browse files
committed
Configured project to use django-q to send email reminder
1 parent 6a9557b commit d961970

File tree

6 files changed

+220
-7
lines changed

6 files changed

+220
-7
lines changed

appointment/services.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
day_off_exists_for_date_range, exclude_booked_slots, get_all_appointments, get_all_staff_members,
2727
get_appointment_by_id, get_appointments_for_date_and_time, get_staff_member_appointment_list,
2828
get_staff_member_from_user_id_or_logged_in, get_times_from_config, get_user_by_email,
29-
get_working_hours_for_staff_and_day, parse_name, working_hours_exist)
29+
get_working_hours_for_staff_and_day, parse_name, update_appointment_reminder, working_hours_exist)
3030
from appointment.utils.error_codes import ErrorCode
3131
from appointment.utils.json_context import convert_appointment_to_json, get_generic_context, json_response
3232
from appointment.utils.permissions import check_entity_ownership
@@ -326,6 +326,11 @@ def save_appointment(appt, client_name, client_email, start_time, phone_number,
326326

327327
# Modify and save appointment request details
328328
appt_request = appt.appointment_request
329+
330+
# Update reminder here
331+
update_appointment_reminder(appointment=appt, new_date=appt_request.date, new_start_time=start_time,
332+
want_reminder=want_reminder)
333+
329334
appt_request.service = service
330335
appt_request.start_time = start_time
331336
appt_request.end_time = end_time
@@ -367,6 +372,9 @@ def save_appt_date_time(appt_start_time, appt_date, appt_id):
367372
else:
368373
appt_date_obj = appt_date
369374

375+
# Update reminder here
376+
update_appointment_reminder(appointment=appt, new_date=appt_date_obj, new_start_time=appt_start_time_obj)
377+
370378
# Modify and save appointment request details
371379
appt_request = appt.appointment_request
372380
appt_request.date = appt_date_obj

appointment/settings.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from django.conf import settings
1010
from django.conf.global_settings import DEFAULT_FROM_EMAIL
1111

12+
from appointment.logger_config import logger
13+
1214
APPOINTMENT_BASE_TEMPLATE = getattr(settings, 'APPOINTMENT_BASE_TEMPLATE', 'base_templates/base.html')
1315
APPOINTMENT_ADMIN_BASE_TEMPLATE = getattr(settings, 'APPOINTMENT_ADMIN_BASE_TEMPLATE', 'base_templates/base.html')
1416
APPOINTMENT_WEBSITE_NAME = getattr(settings, 'APPOINTMENT_WEBSITE_NAME', 'Website')
@@ -20,3 +22,49 @@
2022
APPOINTMENT_FINISH_TIME = getattr(settings, 'APPOINTMENT_FINISH_TIME', (18, 30))
2123
APP_DEFAULT_FROM_EMAIL = getattr(settings, 'DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
2224
APP_TIME_ZONE = getattr(settings, 'TIME_ZONE', 'America/New_York')
25+
26+
27+
def check_q_cluster():
28+
"""
29+
Checks if Django Q is properly installed and configured in the Django settings.
30+
If 'django_q' is not in INSTALLED_APPS, it warns about both 'django_q' not being installed
31+
and 'Q_CLUSTER' likely not being configured.
32+
If 'django_q' is installed but 'Q_CLUSTER' is not configured, it only warns about 'Q_CLUSTER'.
33+
Returns True if configurations are correct, otherwise False.
34+
"""
35+
missing_conf = []
36+
37+
# Check if Django Q is installed
38+
if 'django_q' not in settings.INSTALLED_APPS:
39+
missing_conf.append("Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n"
40+
"Example: \n\n"
41+
"INSTALLED_APPS = [\n"
42+
" ...\n"
43+
" 'appointment',\n"
44+
" 'django_q',\n"
45+
"]\n")
46+
47+
# Check if Q_CLUSTER configuration is defined
48+
if not hasattr(settings, 'Q_CLUSTER'):
49+
missing_conf.append("Q_CLUSTER is not defined in settings. Please define it.\n"
50+
"Example: \n\n"
51+
"Q_CLUSTER = {\n"
52+
" 'name': 'DjangORM',\n"
53+
" 'workers': 4,\n"
54+
" 'timeout': 90,\n"
55+
" 'retry': 120,\n"
56+
" 'queue_limit': 50,\n"
57+
" 'bulk': 10,\n"
58+
" 'orm': 'default',\n"
59+
"}\n"
60+
"Then run 'python manage.py qcluster' to start the worker.\n"
61+
"See https://django-q.readthedocs.io/en/latest/configure.html for more information.")
62+
63+
# Log warnings if any configurations are missing
64+
if missing_conf:
65+
for warning in missing_conf:
66+
logger.warning(warning)
67+
return False
68+
69+
# Both 'django_q' is installed and 'Q_CLUSTER' is configured
70+
return True

appointment/tasks.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# tasks.py
2+
# Path: appointment/tasks.py
3+
4+
"""
5+
Author: Adams Pierre David
6+
Since: 3.1.0
7+
"""
8+
from django.utils.translation import gettext as _
9+
10+
from appointment.email_sender import notify_admin, send_email
11+
from appointment.models import Appointment
12+
from appointment.logger_config import logger
13+
14+
15+
def send_email_reminder(to_email, first_name, appointment_id):
16+
"""
17+
Send a reminder email to the client about the upcoming appointment.
18+
19+
:param to_email: The email address of the client.
20+
:param first_name: The first name of the client.
21+
:param appointment_id: The appointment ID.
22+
:return: None
23+
"""
24+
25+
# Fetch the appointment using appointment_id
26+
logger.info(f"Sending reminder to {to_email} for appointment {appointment_id}")
27+
appointment = Appointment.objects.get(id=appointment_id)
28+
email_context = {
29+
'first_name': first_name,
30+
'appointment': appointment,
31+
}
32+
send_email(
33+
recipient_list=[to_email], subject=_("Reminder: Upcoming Appointment"),
34+
template_url='email_sender/reminder_email.html', context=email_context
35+
)
36+
# Notify the admin
37+
notify_admin(
38+
subject=_("Admin Reminder: Upcoming Appointment"),
39+
template_url='email_sender/reminder_email.html', context=email_context
40+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% load i18n %}
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8">
6+
<title>{% translate 'Appointment Reminder' %}</title>
7+
</head>
8+
<body>
9+
<h1>{% translate 'Appointment Reminder' %}</h1>
10+
<p>{% translate 'Dear' %} {{ first_name }},</p>
11+
<p>{% translate 'This is a reminder for your upcoming appointment' %}.</p>
12+
<p>{% translate 'Service' %}: {{ appointment.get_service_name }}</p>
13+
<p>{% translate 'Date' %}: {{ appointment.appointment_request.date }}</p>
14+
<p>{% translate 'Time' %}: {{ appointment.appointment_request.start_time }}</p>
15+
<p>{% translate 'Location' %}: {{ appointment.address }}</p>
16+
<p>{% translate 'If you have any questions or need to reschedule, please contact us' %}.</p>
17+
<p>{% translate 'Thank you for choosing us' %}!</p>
18+
</body>
19+
</html>

appointment/utils/date_time.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@
1414
from appointment.settings import APP_TIME_ZONE
1515

1616

17+
def combine_date_and_time(date, time) -> datetime.datetime:
18+
"""Combine a date and a time into a datetime object.
19+
20+
:param date: The date.
21+
:param time: The time.
22+
:return: A datetime object.
23+
"""
24+
return datetime.datetime.combine(date, time)
25+
26+
1727
def convert_12_hour_time_to_24_hour_time(time_to_convert) -> str:
1828
"""Convert a 12-hour time to a 24-hour time.
1929
@@ -95,7 +105,7 @@ def convert_str_to_time(time_str: str) -> datetime.time:
95105
:param time_str: A string representation of time.
96106
:return: A Python `time` object.
97107
"""
98-
formats = ["%I:%M %p", "%H:%M:%S", "%H:%M"]
108+
formats = ["%I:%M %p", "%H:%M:%S", "%H:%M"]
99109

100110
for fmt in formats:
101111
try:

appointment/utils/db_helpers.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212

1313
from django.apps import apps
1414
from django.conf import settings
15-
from django.contrib.auth.hashers import make_password
1615
from django.core.cache import cache
1716
from django.core.exceptions import FieldDoesNotExist
1817
from django.urls import reverse
18+
from django.utils import timezone
19+
from django_q.models import Schedule
20+
from django_q.tasks import schedule
1921

2022
from appointment.logger_config import logger
21-
from appointment.settings import APPOINTMENT_SLOT_DURATION, APPOINTMENT_LEAD_TIME, APPOINTMENT_FINISH_TIME, \
22-
APPOINTMENT_BUFFER_TIME, APPOINTMENT_WEBSITE_NAME, APPOINTMENT_PAYMENT_URL
23-
from appointment.utils.date_time import get_weekday_num, get_current_year
23+
from appointment.settings import (APPOINTMENT_BUFFER_TIME, APPOINTMENT_FINISH_TIME, APPOINTMENT_LEAD_TIME,
24+
APPOINTMENT_PAYMENT_URL, APPOINTMENT_SLOT_DURATION, APPOINTMENT_WEBSITE_NAME)
25+
from appointment.utils.date_time import combine_date_and_time, get_current_year, get_weekday_num
2426

2527
Appointment = apps.get_model('appointment', 'Appointment')
2628
AppointmentRequest = apps.get_model('appointment', 'AppointmentRequest')
@@ -100,10 +102,96 @@ def create_and_save_appointment(ar, client_data: dict, appointment_data: dict):
100102
**appointment_data
101103
)
102104
appointment.save()
103-
logger.info(f"New appointment created: {appointment}")
105+
logger.info(f"New appointment created: {appointment.to_dict()}")
106+
schedule_email_reminder(appointment)
104107
return appointment
105108

106109

110+
def schedule_email_reminder(appointment, appointment_datetime=None):
111+
"""Schedule an email reminder for the given appointment."""
112+
# Check if the Django-Q cluster is running
113+
from appointment.settings import check_q_cluster
114+
if not check_q_cluster():
115+
logger.warning("Django-Q cluster is not running. Email reminder will not be scheduled.")
116+
return
117+
118+
# Calculate reminder datetime if not provided
119+
if appointment_datetime is None:
120+
appointment_datetime = combine_date_and_time(appointment.appointment_request.date,
121+
appointment.appointment_request.start_time)
122+
if timezone.is_naive(appointment_datetime):
123+
appointment_datetime = timezone.make_aware(appointment_datetime)
124+
125+
reminder_datetime = appointment_datetime - datetime.timedelta(days=1)
126+
127+
logger.info(f"Scheduling email reminder for appointment {appointment.id} at {reminder_datetime}")
128+
129+
# Schedule the email reminder task with Django-Q
130+
schedule('appointment.tasks.send_email_reminder',
131+
to_email=appointment.client.email,
132+
name=f"reminder_{appointment.id_request}",
133+
first_name=appointment.client.first_name,
134+
appointment_id=appointment.id,
135+
schedule_type=Schedule.ONCE, # Use Schedule.ONCE for a one-time task
136+
next_run=reminder_datetime)
137+
138+
139+
def update_appointment_reminder(appointment, new_date, new_start_time, want_reminder=None):
140+
"""
141+
Updates or cancels the appointment reminder based on changes to the start time or date,
142+
and the user's preference for receiving a reminder.
143+
144+
:param appointment: The Appointment instance being updated.
145+
:param new_date: The new date as a string (format: "YYYY-MM-DD").
146+
:param new_start_time: The new start time as a string (format: "HH:MM").
147+
:param want_reminder: Boolean indicating if a reminder is desired.
148+
"""
149+
# Convert new date and time strings to datetime objects for comparison
150+
new_datetime = combine_date_and_time(new_date, new_start_time)
151+
152+
existing_datetime = combine_date_and_time(appointment.appointment_request.date,
153+
appointment.appointment_request.start_time)
154+
155+
# Ensure new_datetime is timezone-aware
156+
if timezone.is_naive(new_datetime):
157+
new_datetime = timezone.make_aware(new_datetime)
158+
159+
# Ensure existing_datetime is timezone-aware
160+
if timezone.is_naive(existing_datetime):
161+
existing_datetime = timezone.make_aware(existing_datetime)
162+
163+
# Determine if there's been a change in the datetime or the reminder preference
164+
want_reminder = want_reminder if want_reminder is not None else appointment.want_reminder
165+
datetime_changed = new_datetime != existing_datetime
166+
reminder_preference_changed = appointment.want_reminder != want_reminder
167+
168+
if datetime_changed or reminder_preference_changed:
169+
# Cancel any existing reminder
170+
cancel_existing_reminder(appointment.id_request)
171+
172+
# If a reminder is still desired and the appointment is in the future, schedule a new one
173+
if want_reminder and new_datetime > timezone.now():
174+
schedule_email_reminder(appointment, new_datetime)
175+
else:
176+
logger.info(
177+
f"Reminder for appointment {appointment.id} is not scheduled per user's preference or past datetime.")
178+
179+
# Update the appointment's reminder preference
180+
appointment.want_reminder = want_reminder
181+
appointment.save()
182+
183+
184+
def cancel_existing_reminder(appointment_id_request):
185+
"""
186+
Cancels any existing reminder for the appointment.
187+
Placeholder function - implement based on your scheduling mechanism.
188+
"""
189+
# Example: Delete existing scheduled tasks for this appointment
190+
# This is highly dependent on how reminders are implemented and stored
191+
task_name = f"reminder_{appointment_id_request}"
192+
Schedule.objects.filter(name=task_name).delete()
193+
194+
107195
def generate_unique_username_from_email(email: str) -> str:
108196
username_base = email.split('@')[0]
109197
username = username_base

0 commit comments

Comments
 (0)