Skip to content

Commit 0a40f93

Browse files
authored
Merge pull request #251 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 bf46942 + d50cca0 commit 0a40f93

File tree

8 files changed

+137
-73
lines changed

8 files changed

+137
-73
lines changed

README.md

Lines changed: 14 additions & 8 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

@@ -184,13 +189,13 @@ If you're using a base.html template, you must include the following block in yo
184189
{% endblock %}
185190
```
186191

187-
At least the block for css, body and js are required; otherwise the application will not work properly.
192+
At least the block for css, body and js are required; otherwise the application will not work properly.
188193
Jquery is also required to be included in the template.
189194

190195
The title and description are optional but recommended for SEO purposes.
191196

192-
See an example of a base.html template [here](https://github.com/adamspd/django-appointment/blob/main/appointment/templates/base_templates/base.html).
193-
197+
See an example of a base.html
198+
template [here](https://github.com/adamspd/django-appointment/blob/main/appointment/templates/base_templates/base.html).
194199

195200
## Customization 🔧
196201

@@ -200,7 +205,6 @@ See an example of a base.html template [here](https://github.com/adamspd/django-
200205
2. Modify these values as needed for your application, and the app will adapt to the new settings.
201206
3. For further customization, you can extend the provided models, views, and templates or create your own.
202207

203-
204208
## Docker Support 🐳
205209

206210
Django-Appointment now supports Docker, making it easier to set up, develop, and test.
@@ -297,7 +301,6 @@ Here's how you can set it up:
297301
> **Note:** I used the default database settings for the Docker container.
298302
> If you want to use a different database, you can modify the Dockerfile and docker-compose.yml files to use your
299303
> preferred database.
300-
301304
302305
## Compatibility Matrix 📊
303306
@@ -333,9 +336,12 @@ information.
333336
334337
## Notes 📝⚠️
335338
336-
I'm working on a testing website for the application that is not fully functional yet, no hard feelings. Before using it,
337-
it's important to me that you read the terms of use, only then you can use it if you agree to them. The demo website is located
338-
at [https://django-appt.adamspierredavid.com/](https://django-appt.adamspierredavid.com/terms-and-conditions/). Ideas are welcome.
339+
I'm working on a testing website for the application that is not fully functional yet, no hard feelings. Before using
340+
it,
341+
it's important to me that you read the terms of use, only then you can use it if you agree to them. The demo website is
342+
located
343+
at [https://django-appt.adamspierredavid.com/](https://django-appt.adamspierredavid.com/terms-and-conditions/). Ideas
344+
are welcome.
339345
340346
## About the Author
341347

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: 73 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,25 @@
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
38+
try:
39+
from django_q.models import Schedule
40+
from django_q.tasks import schedule
41+
42+
DJANGO_Q_AVAILABLE = True
43+
except ImportError:
44+
DJANGO_Q_AVAILABLE = False
45+
Schedule = None
46+
schedule = None
47+
48+
49+
@skip("Django-Q is not available")
50+
class DjangoQUnavailableTest(TestCase):
51+
def test_placeholder(self):
52+
self.skipTest("Django-Q is not available")
53+
3454

3555
class TestCalculateSlots(TestCase):
3656
def setUp(self):
@@ -165,18 +185,12 @@ def test_another_staff_member_no_day_off(self):
165185
self.assertFalse(check_day_off_for_staff(self.staff_member2, "2023-10-06"))
166186

167187

168-
class TestCreateAndSaveAppointment(BaseTest):
169-
188+
class TestCreateAndSaveAppointment(BaseTest, TestCase):
170189
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()
190+
super().setUp()
174191
self.factory = RequestFactory()
175192
self.request = self.factory.get('/')
176-
177-
def tearDown(self):
178-
Appointment.objects.all().delete()
179-
AppointmentRequest.objects.all().delete()
193+
self.ar = self.create_appt_request_for_sm1()
180194

181195
def test_create_and_save_appointment(self):
182196
client_data = {
@@ -190,7 +204,8 @@ def test_create_and_save_appointment(self):
190204
'additional_info': 'Please bring a Zat gun.'
191205
}
192206

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

195210
self.assertIsNotNone(appointment)
196211
self.assertEqual(appointment.client.email, client_data['email'])
@@ -199,6 +214,32 @@ def test_create_and_save_appointment(self):
199214
self.assertEqual(appointment.address, appointment_data['address'])
200215
self.assertEqual(appointment.additional_info, appointment_data['additional_info'])
201216

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

203244
def get_mock_reverse(url_name, **kwargs):
204245
"""A mocked version of the reverse function."""
@@ -208,6 +249,13 @@ def get_mock_reverse(url_name, **kwargs):
208249

209250

210251
class ScheduleEmailReminderTest(BaseTest):
252+
@classmethod
253+
def setUpClass(cls):
254+
if not DJANGO_Q_AVAILABLE:
255+
import unittest
256+
raise unittest.SkipTest("Django-Q is not available")
257+
super().setUpClass()
258+
211259
def setUp(self):
212260
super().setUp()
213261
self.factory = RequestFactory()
@@ -217,6 +265,7 @@ def setUp(self):
217265
def tearDown(self):
218266
Appointment.objects.all().delete()
219267
AppointmentRequest.objects.all().delete()
268+
super().tearDown()
220269

221270
def test_schedule_email_reminder_cluster_running(self):
222271
with patch('appointment.settings.check_q_cluster', return_value=True), \
@@ -233,7 +282,14 @@ def test_schedule_email_reminder_cluster_not_running(self):
233282
"Django-Q cluster is not running. Email reminder will not be scheduled.")
234283

235284

236-
class UpdateAppointmentReminderTest(BaseTest):
285+
class UpdateAppointmentReminderTest(BaseTest, TestCase):
286+
@classmethod
287+
def setUpClass(cls):
288+
if not DJANGO_Q_AVAILABLE:
289+
import unittest
290+
raise unittest.SkipTest("Django-Q is not available")
291+
super().setUpClass()
292+
237293
def setUp(self):
238294
super().setUp()
239295
self.factory = RequestFactory()
@@ -243,6 +299,7 @@ def setUp(self):
243299
def tearDown(self):
244300
Appointment.objects.all().delete()
245301
AppointmentRequest.objects.all().delete()
302+
super().tearDown()
246303

247304
def test_update_appointment_reminder_date_time_changed(self):
248305
appointment = self.create_appt_for_sm1()
@@ -267,7 +324,7 @@ def test_update_appointment_reminder_no_change(self):
267324
mock_cancel_existing_reminder.assert_not_called()
268325
mock_schedule_email_reminder.assert_not_called()
269326

270-
@patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary
327+
@patch('appointment.utils.db_helpers.logger')
271328
def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger):
272329
# Scenario where user does not want a reminder
273330
want_reminder = False
@@ -281,7 +338,7 @@ def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger):
281338
f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime."
282339
)
283340

284-
@patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary
341+
@patch('appointment.utils.db_helpers.logger')
285342
def test_reminder_not_scheduled_due_to_past_datetime(self, mock_logger):
286343
# Scenario where the new datetime is in the past
287344
want_reminder = True
@@ -371,6 +428,8 @@ def test_staff_change_not_allowed(self, mock_config_first):
371428

372429
class CancelExistingReminderTest(BaseTest):
373430
def test_cancel_existing_reminder(self):
431+
if not DJANGO_Q_AVAILABLE:
432+
return
374433
appointment = self.create_appt_for_sm1()
375434
Schedule.objects.create(func='appointment.tasks.send_email_reminder', name=f"reminder_{appointment.id_request}")
376435

0 commit comments

Comments
 (0)