Skip to content

Commit 40ed562

Browse files
authored
Merge pull request #253 from adamspd/django_q-no-longer-needs-to-be-installed-even-if-not-used
django_q2 no longer needs to be installed even if not used
2 parents 2e0add8 + d45edea commit 40ed562

File tree

8 files changed

+131
-65
lines changed

8 files changed

+131
-65
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ see their [release notes](https://github.com/adamspd/django-appointment/tree/mai
6868
```bash
6969
pip install django-appointment
7070
```
71+
Optionally installing django_q2 if you need email reminders:
72+
73+
```bash
74+
pip install django_q2
75+
```
7176

7277
2. Add "appointment" (& "django_q" if you want to enable email reminders) to your `INSTALLED_APPS` setting like so:
7378

appointment/email_sender/email_sender.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33

44
from django.core.mail import mail_admins, send_mail
55
from django.template import loader
6-
from django_q.tasks import async_task
76

87
from appointment.logger_config import get_logger
98
from appointment.settings import APP_DEFAULT_FROM_EMAIL, check_q_cluster
109

1110
logger = get_logger(__name__)
1211

12+
try:
13+
from django_q.tasks import async_task
14+
15+
DJANGO_Q_AVAILABLE = True
16+
except ImportError:
17+
async_task = None
18+
DJANGO_Q_AVAILABLE = False
19+
logger.warning("django-q is not installed. Email will be send synchronously.")
20+
1321

1422
def has_required_email_settings():
1523
"""Check if all required email settings are configured and warn if any are missing."""
@@ -45,7 +53,7 @@ def send_email(recipient_list, subject: str, template_url: str = None, context:
4553
from_email = from_email or APP_DEFAULT_FROM_EMAIL
4654
html_message = render_email_template(template_url, context)
4755

48-
if get_use_django_q_for_emails() and check_q_cluster():
56+
if get_use_django_q_for_emails() and check_q_cluster() and DJANGO_Q_AVAILABLE:
4957
# Asynchronously send the email using Django-Q
5058
async_task(
5159
"appointment.tasks.send_email_task", recipient_list=recipient_list, subject=subject,
@@ -69,7 +77,7 @@ def notify_admin(subject: str, template_url: str = None, context: dict = None, m
6977

7078
html_message = render_email_template(template_url, context)
7179

72-
if get_use_django_q_for_emails() and check_q_cluster():
80+
if get_use_django_q_for_emails() and check_q_cluster() and DJANGO_Q_AVAILABLE:
7381
# Enqueue the task to send admin email asynchronously
7482
async_task('appointment.tasks.notify_admin_task', subject=subject, message=message, html_message=html_message)
7583
else:

appointment/settings.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,15 @@ def check_q_cluster():
3737
logger.info("Checking missing configuration for django q cluster")
3838
# Check if Django Q is installed
3939
if 'django_q' not in settings.INSTALLED_APPS:
40-
missing_conf.append("Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n"
41-
"Example: \n\n"
42-
"INSTALLED_APPS = [\n"
43-
" ...\n"
44-
" 'appointment',\n"
45-
" 'django_q',\n"
46-
"]\n")
40+
missing_conf.append("Django Q is not in settings.INSTALLED_APPS. Please add it to the list. "
41+
"See https://django-appt-doc.adamspierredavid.com/getting-started/#installation "
42+
"for more information")
4743

4844
# Check if Q_CLUSTER configuration is defined
4945
if not hasattr(settings, 'Q_CLUSTER'):
50-
missing_conf.append("Q_CLUSTER is not defined in settings. Please define it.\n"
51-
"Example: \n\n"
52-
"Q_CLUSTER = {\n"
53-
" 'name': 'DjangORM',\n"
54-
" 'workers': 4,\n"
55-
" 'timeout': 90,\n"
56-
" 'retry': 120,\n"
57-
" 'queue_limit': 50,\n"
58-
" 'bulk': 10,\n"
59-
" 'orm': 'default',\n"
60-
"}\n"
61-
"Then run 'python manage.py qcluster' to start the worker.\n"
62-
"See https://django-q.readthedocs.io/en/latest/configure.html for more information.")
46+
missing_conf.append("Q_CLUSTER is not defined in settings. Please define it. "
47+
"See https://django-appt-doc.adamspierredavid.com/project-structure/#configuration "
48+
"for more information.")
6349

6450
# Log warnings if any configurations are missing
6551
if missing_conf:

appointment/tests/test_settings.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,9 @@ def test_check_q_cluster_with_django_q_missing(self, mock_logger, mock_settings)
2222
self.assertFalse(result)
2323
# Verify logger was called with the expected warning about 'django_q' not being installed
2424
mock_logger.warning.assert_called_with(
25-
"Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n"
26-
"Example: \n\n"
27-
"INSTALLED_APPS = [\n"
28-
" ...\n"
29-
" 'appointment',\n"
30-
" 'django_q',\n"
31-
"]\n")
25+
"Django Q is not in settings.INSTALLED_APPS. Please add it to the list. "
26+
"See https://django-appt-doc.adamspierredavid.com/getting-started/#installation "
27+
"for more information")
3228

3329
@patch('appointment.settings.settings')
3430
@patch('appointment.settings.logger')

appointment/tests/utils/test_db_helpers.py

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Path: appointment/tests/utils/test_db_helpers.py
33

44
import datetime
5+
from unittest import skip
56
from unittest.mock import MagicMock, PropertyMock, patch
67

78
from django.apps import apps
@@ -12,8 +13,8 @@
1213
from django.test.client import RequestFactory
1314
from django.urls import reverse
1415
from django.utils import timezone
15-
from django_q.models import Schedule
1616

17+
from appointment.logger_config import get_logger
1718
from appointment.models import Config, DayOff, PaymentInfo
1819
from appointment.tests.base.base_test import BaseTest
1920
from appointment.tests.mixins.base_mixin import ConfigMixin
@@ -31,6 +32,28 @@
3132
staff_change_allowed_on_reschedule, update_appointment_reminder, username_in_user_model, working_hours_exist
3233
)
3334

35+
logger = get_logger(__name__)
36+
37+
# Check if django-q is installed in settings
38+
DJANGO_Q_AVAILABLE = 'django_q' in settings.INSTALLED_APPS
39+
40+
# Check if django-q is installed as a dependency
41+
try:
42+
from django_q.models import Schedule
43+
from django_q.tasks import schedule
44+
45+
DJANGO_Q_AVAILABLE = True
46+
except ImportError:
47+
DJANGO_Q_AVAILABLE = False
48+
Schedule = None
49+
schedule = None
50+
51+
52+
@skip("Django-Q is not available")
53+
class DjangoQUnavailableTest(TestCase):
54+
def test_placeholder(self):
55+
self.skipTest("Django-Q is not available")
56+
3457

3558
class TestCalculateSlots(TestCase):
3659
def setUp(self):
@@ -165,18 +188,12 @@ def test_another_staff_member_no_day_off(self):
165188
self.assertFalse(check_day_off_for_staff(self.staff_member2, "2023-10-06"))
166189

167190

168-
class TestCreateAndSaveAppointment(BaseTest):
169-
191+
class TestCreateAndSaveAppointment(BaseTest, TestCase):
170192
def setUp(self):
171-
super().setUp() # Call the parent class setup
172-
# Specific setups for this test class
173-
self.ar = self.create_appt_request_for_sm1()
193+
super().setUp()
174194
self.factory = RequestFactory()
175195
self.request = self.factory.get('/')
176-
177-
def tearDown(self):
178-
Appointment.objects.all().delete()
179-
AppointmentRequest.objects.all().delete()
196+
self.ar = self.create_appt_request_for_sm1()
180197

181198
def test_create_and_save_appointment(self):
182199
client_data = {
@@ -190,7 +207,8 @@ def test_create_and_save_appointment(self):
190207
'additional_info': 'Please bring a Zat gun.'
191208
}
192209

193-
appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request)
210+
with patch('appointment.utils.db_helpers.schedule_email_reminder') as mock_schedule_reminder:
211+
appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request)
194212

195213
self.assertIsNotNone(appointment)
196214
self.assertEqual(appointment.client.email, client_data['email'])
@@ -199,6 +217,32 @@ def test_create_and_save_appointment(self):
199217
self.assertEqual(appointment.address, appointment_data['address'])
200218
self.assertEqual(appointment.additional_info, appointment_data['additional_info'])
201219

220+
if DJANGO_Q_AVAILABLE:
221+
mock_schedule_reminder.assert_called_once()
222+
else:
223+
mock_schedule_reminder.assert_not_called()
224+
225+
@patch('appointment.utils.db_helpers.DJANGO_Q_AVAILABLE', False)
226+
def test_create_and_save_appointment_without_django_q(self):
227+
client_data = {
228+
'email': '[email protected]',
229+
'name': 'samantha.carter',
230+
}
231+
appointment_data = {
232+
'phone': '987654321',
233+
'want_reminder': True,
234+
'address': '456, SGC, Colorado Springs, USA',
235+
'additional_info': 'Bring naquadah generator.'
236+
}
237+
238+
with patch('appointment.utils.db_helpers.logger.warning') as mock_logger_warning:
239+
appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request)
240+
241+
self.assertIsNotNone(appointment)
242+
self.assertEqual(appointment.client.email, client_data['email'])
243+
mock_logger_warning.assert_called_with(
244+
f"Email reminder requested for appointment {appointment.id}, but django-q is not available.")
245+
202246

203247
def get_mock_reverse(url_name, **kwargs):
204248
"""A mocked version of the reverse function."""
@@ -208,6 +252,13 @@ def get_mock_reverse(url_name, **kwargs):
208252

209253

210254
class ScheduleEmailReminderTest(BaseTest):
255+
@classmethod
256+
def setUpClass(cls):
257+
if not DJANGO_Q_AVAILABLE:
258+
import unittest
259+
raise unittest.SkipTest("Django-Q is not available")
260+
super().setUpClass()
261+
211262
def setUp(self):
212263
super().setUp()
213264
self.factory = RequestFactory()
@@ -217,6 +268,7 @@ def setUp(self):
217268
def tearDown(self):
218269
Appointment.objects.all().delete()
219270
AppointmentRequest.objects.all().delete()
271+
super().tearDown()
220272

221273
def test_schedule_email_reminder_cluster_running(self):
222274
with patch('appointment.settings.check_q_cluster', return_value=True), \
@@ -233,7 +285,14 @@ def test_schedule_email_reminder_cluster_not_running(self):
233285
"Django-Q cluster is not running. Email reminder will not be scheduled.")
234286

235287

236-
class UpdateAppointmentReminderTest(BaseTest):
288+
class UpdateAppointmentReminderTest(BaseTest, TestCase):
289+
@classmethod
290+
def setUpClass(cls):
291+
if not DJANGO_Q_AVAILABLE:
292+
import unittest
293+
raise unittest.SkipTest("Django-Q is not available")
294+
super().setUpClass()
295+
237296
def setUp(self):
238297
super().setUp()
239298
self.factory = RequestFactory()
@@ -243,6 +302,7 @@ def setUp(self):
243302
def tearDown(self):
244303
Appointment.objects.all().delete()
245304
AppointmentRequest.objects.all().delete()
305+
super().tearDown()
246306

247307
def test_update_appointment_reminder_date_time_changed(self):
248308
appointment = self.create_appt_for_sm1()
@@ -267,7 +327,7 @@ def test_update_appointment_reminder_no_change(self):
267327
mock_cancel_existing_reminder.assert_not_called()
268328
mock_schedule_email_reminder.assert_not_called()
269329

270-
@patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary
330+
@patch('appointment.utils.db_helpers.logger')
271331
def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger):
272332
# Scenario where user does not want a reminder
273333
want_reminder = False
@@ -281,7 +341,7 @@ def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger):
281341
f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime."
282342
)
283343

284-
@patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary
344+
@patch('appointment.utils.db_helpers.logger')
285345
def test_reminder_not_scheduled_due_to_past_datetime(self, mock_logger):
286346
# Scenario where the new datetime is in the past
287347
want_reminder = True
@@ -371,6 +431,8 @@ def test_staff_change_not_allowed(self, mock_config_first):
371431

372432
class CancelExistingReminderTest(BaseTest):
373433
def test_cancel_existing_reminder(self):
434+
if not DJANGO_Q_AVAILABLE:
435+
return
374436
appointment = self.create_appt_for_sm1()
375437
Schedule.objects.create(func='appointment.tasks.send_email_reminder', name=f"reminder_{appointment.id_request}")
376438

appointment/utils/db_helpers.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
from urllib.parse import urlparse
1212

1313
from django.apps import apps
14+
from django.conf import settings
1415
from django.contrib.auth import get_user_model
1516
from django.core.cache import cache
1617
from django.core.exceptions import FieldDoesNotExist
1718
from django.urls import reverse
1819
from django.utils import timezone
19-
from django_q.models import Schedule
20-
from django_q.tasks import schedule
2120

2221
from appointment.logger_config import get_logger
2322
from appointment.settings import (
@@ -26,6 +25,23 @@
2625
)
2726
from appointment.utils.date_time import combine_date_and_time, get_weekday_num
2827

28+
logger = get_logger(__name__)
29+
30+
# Check if django-q is installed in settings
31+
DJANGO_Q_AVAILABLE = 'django_q' in settings.INSTALLED_APPS
32+
33+
# Check if django-q is installed as a dependency
34+
try:
35+
from django_q.models import Schedule
36+
from django_q.tasks import schedule
37+
38+
DJANGO_Q_AVAILABLE = True
39+
except ImportError:
40+
DJANGO_Q_AVAILABLE = False
41+
Schedule = None
42+
schedule = None
43+
logger.warning("django-q is not installed. Email reminders will not be scheduled.")
44+
2945
Appointment = apps.get_model('appointment', 'Appointment')
3046
AppointmentRequest = apps.get_model('appointment', 'AppointmentRequest')
3147
WorkingHours = apps.get_model('appointment', 'WorkingHours')
@@ -37,8 +53,6 @@
3753
EmailVerificationCode = apps.get_model('appointment', 'EmailVerificationCode')
3854
AppointmentRescheduleHistory = apps.get_model('appointment', 'AppointmentRescheduleHistory')
3955

40-
logger = get_logger(__name__)
41-
4256

4357
def calculate_slots(start_time, end_time, buffer_time, slot_duration):
4458
"""Calculate the available slots between the given start and end times using the given buffer time and slot duration
@@ -112,16 +126,17 @@ def create_and_save_appointment(ar, client_data: dict, appointment_data: dict, r
112126
logger.info(f"New appointment created: {appointment.to_dict()}")
113127
if appointment.want_reminder:
114128
logger.info(f"User wants a reminder for appointment {appointment.id}, scheduling it...")
115-
schedule_email_reminder(appointment, request)
129+
if DJANGO_Q_AVAILABLE:
130+
schedule_email_reminder(appointment, request)
131+
else:
132+
logger.warning(f"Email reminder requested for appointment {appointment.id}, but django-q is not available.")
116133
return appointment
117134

118135

119136
def schedule_email_reminder(appointment, request, appointment_datetime=None):
120137
"""Schedule an email reminder for the given appointment."""
121-
# Check if the Django-Q cluster is running
122-
from appointment.settings import check_q_cluster
123-
if not check_q_cluster():
124-
logger.warning("Django-Q cluster is not running. Email reminder will not be scheduled.")
138+
if not DJANGO_Q_AVAILABLE:
139+
logger.warning("Django-Q is not available. Email reminder will not be scheduled.")
125140
return
126141

127142
# Calculate reminder datetime if not provided
@@ -155,6 +170,9 @@ def update_appointment_reminder(appointment, new_date, new_start_time, request,
155170
Updates or cancels the appointment reminder based on changes to the start time or date,
156171
and the user's preference for receiving a reminder.
157172
"""
173+
if not DJANGO_Q_AVAILABLE:
174+
logger.warning("Django-Q is not available. Appointment reminder cannot be updated.")
175+
return
158176
# Convert new date and time strings to datetime objects for comparison
159177
new_datetime = combine_date_and_time(new_date, new_start_time)
160178

@@ -195,6 +213,9 @@ def cancel_existing_reminder(appointment_id_request):
195213
"""
196214
Cancels any existing reminder for the appointment.
197215
"""
216+
if not DJANGO_Q_AVAILABLE:
217+
logger.warning("Django-Q is not available. Appointment reminder cannot be updated.")
218+
return
198219
task_name = f"reminder_{appointment_id_request}"
199220
Schedule.objects.filter(name=task_name).delete()
200221

0 commit comments

Comments
 (0)