Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ see their [release notes](https://github.com/adamspd/django-appointment/tree/mai
```bash
pip install django-appointment
```
Optionally installing django_q2 if you need email reminders:

```bash
pip install django_q2
```

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

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

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

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

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

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

## Customization 🔧

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


## Docker Support 🐳

Django-Appointment now supports Docker, making it easier to set up, develop, and test.
Expand Down Expand Up @@ -297,7 +301,6 @@ Here's how you can set it up:
> **Note:** I used the default database settings for the Docker container.
> If you want to use a different database, you can modify the Dockerfile and docker-compose.yml files to use your
> preferred database.


## Compatibility Matrix 📊

Expand Down Expand Up @@ -333,9 +336,12 @@ information.

## Notes 📝⚠️

I'm working on a testing website for the application that is not fully functional yet, no hard feelings. Before using it,
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
at [https://django-appt.adamspierredavid.com/](https://django-appt.adamspierredavid.com/terms-and-conditions/). Ideas are welcome.
I'm working on a testing website for the application that is not fully functional yet, no hard feelings. Before using
it,
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
at [https://django-appt.adamspierredavid.com/](https://django-appt.adamspierredavid.com/terms-and-conditions/). Ideas
are welcome.

## About the Author

Expand Down
14 changes: 11 additions & 3 deletions appointment/email_sender/email_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@

from django.core.mail import mail_admins, send_mail
from django.template import loader
from django_q.tasks import async_task

from appointment.logger_config import get_logger
from appointment.settings import APP_DEFAULT_FROM_EMAIL, check_q_cluster

logger = get_logger(__name__)

try:
from django_q.tasks import async_task

DJANGO_Q_AVAILABLE = True
except ImportError:
async_task = None
DJANGO_Q_AVAILABLE = False
logger.warning("django-q is not installed. Email will be send synchronously.")


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

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

html_message = render_email_template(template_url, context)

if get_use_django_q_for_emails() and check_q_cluster():
if get_use_django_q_for_emails() and check_q_cluster() and DJANGO_Q_AVAILABLE:
# Enqueue the task to send admin email asynchronously
async_task('appointment.tasks.notify_admin_task', subject=subject, message=message, html_message=html_message)
else:
Expand Down
26 changes: 6 additions & 20 deletions appointment/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,15 @@ def check_q_cluster():
logger.info("Checking missing configuration for django q cluster")
# Check if Django Q is installed
if 'django_q' not in settings.INSTALLED_APPS:
missing_conf.append("Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n"
"Example: \n\n"
"INSTALLED_APPS = [\n"
" ...\n"
" 'appointment',\n"
" 'django_q',\n"
"]\n")
missing_conf.append("Django Q is not in settings.INSTALLED_APPS. Please add it to the list. "
"See https://django-appt-doc.adamspierredavid.com/getting-started/#installation "
"for more information")

# Check if Q_CLUSTER configuration is defined
if not hasattr(settings, 'Q_CLUSTER'):
missing_conf.append("Q_CLUSTER is not defined in settings. Please define it.\n"
"Example: \n\n"
"Q_CLUSTER = {\n"
" 'name': 'DjangORM',\n"
" 'workers': 4,\n"
" 'timeout': 90,\n"
" 'retry': 120,\n"
" 'queue_limit': 50,\n"
" 'bulk': 10,\n"
" 'orm': 'default',\n"
"}\n"
"Then run 'python manage.py qcluster' to start the worker.\n"
"See https://django-q.readthedocs.io/en/latest/configure.html for more information.")
missing_conf.append("Q_CLUSTER is not defined in settings. Please define it. "
"See https://django-appt-doc.adamspierredavid.com/project-structure/#configuration "
"for more information.")

# Log warnings if any configurations are missing
if missing_conf:
Expand Down
10 changes: 3 additions & 7 deletions appointment/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ def test_check_q_cluster_with_django_q_missing(self, mock_logger, mock_settings)
self.assertFalse(result)
# Verify logger was called with the expected warning about 'django_q' not being installed
mock_logger.warning.assert_called_with(
"Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n"
"Example: \n\n"
"INSTALLED_APPS = [\n"
" ...\n"
" 'appointment',\n"
" 'django_q',\n"
"]\n")
"Django Q is not in settings.INSTALLED_APPS. Please add it to the list. "
"See https://django-appt-doc.adamspierredavid.com/getting-started/#installation "
"for more information")

@patch('appointment.settings.settings')
@patch('appointment.settings.logger')
Expand Down
87 changes: 73 additions & 14 deletions appointment/tests/utils/test_db_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Path: appointment/tests/utils/test_db_helpers.py

import datetime
from unittest import skip
from unittest.mock import MagicMock, PropertyMock, patch

from django.apps import apps
Expand All @@ -12,8 +13,8 @@
from django.test.client import RequestFactory
from django.urls import reverse
from django.utils import timezone
from django_q.models import Schedule

from appointment.logger_config import get_logger
from appointment.models import Config, DayOff, PaymentInfo
from appointment.tests.base.base_test import BaseTest
from appointment.tests.mixins.base_mixin import ConfigMixin
Expand All @@ -31,6 +32,25 @@
staff_change_allowed_on_reschedule, update_appointment_reminder, username_in_user_model, working_hours_exist
)

logger = get_logger(__name__)

# Check if django-q is installed
try:
from django_q.models import Schedule
from django_q.tasks import schedule

DJANGO_Q_AVAILABLE = True
except ImportError:
DJANGO_Q_AVAILABLE = False
Schedule = None
schedule = None


@skip("Django-Q is not available")
class DjangoQUnavailableTest(TestCase):
def test_placeholder(self):
self.skipTest("Django-Q is not available")


class TestCalculateSlots(TestCase):
def setUp(self):
Expand Down Expand Up @@ -165,18 +185,12 @@ def test_another_staff_member_no_day_off(self):
self.assertFalse(check_day_off_for_staff(self.staff_member2, "2023-10-06"))


class TestCreateAndSaveAppointment(BaseTest):

class TestCreateAndSaveAppointment(BaseTest, TestCase):
def setUp(self):
super().setUp() # Call the parent class setup
# Specific setups for this test class
self.ar = self.create_appt_request_for_sm1()
super().setUp()
self.factory = RequestFactory()
self.request = self.factory.get('/')

def tearDown(self):
Appointment.objects.all().delete()
AppointmentRequest.objects.all().delete()
self.ar = self.create_appt_request_for_sm1()

def test_create_and_save_appointment(self):
client_data = {
Expand All @@ -190,7 +204,8 @@ def test_create_and_save_appointment(self):
'additional_info': 'Please bring a Zat gun.'
}

appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request)
with patch('appointment.utils.db_helpers.schedule_email_reminder') as mock_schedule_reminder:
appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request)

self.assertIsNotNone(appointment)
self.assertEqual(appointment.client.email, client_data['email'])
Expand All @@ -199,6 +214,32 @@ def test_create_and_save_appointment(self):
self.assertEqual(appointment.address, appointment_data['address'])
self.assertEqual(appointment.additional_info, appointment_data['additional_info'])

if DJANGO_Q_AVAILABLE:
mock_schedule_reminder.assert_called_once()
else:
mock_schedule_reminder.assert_not_called()

@patch('appointment.utils.db_helpers.DJANGO_Q_AVAILABLE', False)
def test_create_and_save_appointment_without_django_q(self):
client_data = {
'email': 'samantha.carter@django-appointment.com',
'name': 'samantha.carter',
}
appointment_data = {
'phone': '987654321',
'want_reminder': True,
'address': '456, SGC, Colorado Springs, USA',
'additional_info': 'Bring naquadah generator.'
}

with patch('appointment.utils.db_helpers.logger.warning') as mock_logger_warning:
appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request)

self.assertIsNotNone(appointment)
self.assertEqual(appointment.client.email, client_data['email'])
mock_logger_warning.assert_called_with(
f"Email reminder requested for appointment {appointment.id}, but django-q is not available.")


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


class ScheduleEmailReminderTest(BaseTest):
@classmethod
def setUpClass(cls):
if not DJANGO_Q_AVAILABLE:
import unittest
raise unittest.SkipTest("Django-Q is not available")
super().setUpClass()

def setUp(self):
super().setUp()
self.factory = RequestFactory()
Expand All @@ -217,6 +265,7 @@ def setUp(self):
def tearDown(self):
Appointment.objects.all().delete()
AppointmentRequest.objects.all().delete()
super().tearDown()

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


class UpdateAppointmentReminderTest(BaseTest):
class UpdateAppointmentReminderTest(BaseTest, TestCase):
@classmethod
def setUpClass(cls):
if not DJANGO_Q_AVAILABLE:
import unittest
raise unittest.SkipTest("Django-Q is not available")
super().setUpClass()

def setUp(self):
super().setUp()
self.factory = RequestFactory()
Expand All @@ -243,6 +299,7 @@ def setUp(self):
def tearDown(self):
Appointment.objects.all().delete()
AppointmentRequest.objects.all().delete()
super().tearDown()

def test_update_appointment_reminder_date_time_changed(self):
appointment = self.create_appt_for_sm1()
Expand All @@ -267,7 +324,7 @@ def test_update_appointment_reminder_no_change(self):
mock_cancel_existing_reminder.assert_not_called()
mock_schedule_email_reminder.assert_not_called()

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

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

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

Expand Down
Loading
Loading