Skip to content

Commit 0e8b3f1

Browse files
authored
Merge pull request #201 from adamspd/153-add-possibility-to-send-all-emails-using-django-q
Added possibility to send all emails with django q
2 parents d0ccbef + 2e31549 commit 0e8b3f1

File tree

10 files changed

+170
-79
lines changed

10 files changed

+170
-79
lines changed

CODE_OF_CONDUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
106106
### 4. Permanent Ban
107107

108108
**Community Impact**: Demonstrating a pattern of violation of community
109-
standards, including sustained inappropriate behavior, harassment of an
109+
standards, including sustained inappropriate behavior, harassment of an
110110
individual, or aggression toward or disparagement of classes of individuals.
111111

112112
**Consequence**: A permanent ban from any sort of public interaction within

README.md

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ see their [release notes](https://github.com/adamspd/django-appointment/tree/mai
132132
```
133133

134134
To be able to send email reminders after adding `django_q` to your `INSTALLED_APPS`, you must add this variable
135-
`Q_CLUSTER` in your Django's `settings.py`. If done, and users check the box to receive reminders, you and them
135+
`Q_CLUSTER` in your Django's `settings.py`. If done, and users check the box to receive reminders, you and they
136136
will receive an email reminder 24 hours before the appointment.
137137

138-
Here's a configuration example, that you can use without modification (if you don't want to do much research):
138+
Here's a configuration example that you can use without modification (if you don't want to do much research):
139139

140140
```python
141141
Q_CLUSTER = {
@@ -147,6 +147,7 @@ see their [release notes](https://github.com/adamspd/django-appointment/tree/mai
147147
'bulk': 10,
148148
'orm': 'default',
149149
}
150+
USE_DJANGO_Q_FOR_EMAILS = True # 🆕 Use Django Q for sending ALL email.
150151
```
151152

152153
5. Next would be to create the migrations and run them by doing `python manage.py makemigrations appointment` and right
@@ -162,6 +163,44 @@ see their [release notes](https://github.com/adamspd/django-appointment/tree/mai
162163
9. Visit http://127.0.0.1:8000/appointment/request/<service_id>/ to view the available time slots and schedule an
163164
appointment.
164165

166+
## Template Configuration 📝
167+
168+
If you're using a base.html template, you must include the following block in your template:
169+
170+
```
171+
{% block customCSS %}
172+
{% endblock %}
173+
174+
{% block title %}
175+
{% endblock %}
176+
177+
{% block description %}
178+
{% endblock %}
179+
180+
{% block body %}
181+
{% endblock %}
182+
183+
{% block customJS %}
184+
{% endblock %}
185+
```
186+
187+
At least the block for css, body and js are required; otherwise the application will not work properly.
188+
Jquery is also required to be included in the template.
189+
190+
The title and description are optional but recommended for SEO purposes.
191+
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+
194+
195+
## Customization 🔧
196+
197+
1. In your Django project's `settings.py`, you can override the default values for the appointment scheduler.
198+
More information regarding available configurations can be found in
199+
the [documentation](https://github.com/adamspd/django-appointment/tree/main/docs/README.md#configuration).
200+
2. Modify these values as needed for your application, and the app will adapt to the new settings.
201+
3. For further customization, you can extend the provided models, views, and templates or create your own.
202+
203+
165204
## Docker Support 🐳
166205

167206
Django-Appointment now supports Docker, making it easier to set up, develop, and test.
@@ -194,8 +233,10 @@ Here's how you can set it up:
194233
You should include your email host user and password for Django's email functionality (if you want it to work):
195234

196235
```plaintext
197-
EMAIL_HOST_USER=your_email@gmail.com
236+
EMAIL_HOST_USER=your_email@example.com
198237
EMAIL_HOST_PASSWORD=your_password
238+
ADMIN_NAME='Example Name'
239+
199240
```
200241

201242
> **Note:** The `.env` file is used to store sensitive information and should not be committed to version control.
@@ -256,14 +297,7 @@ Here's how you can set it up:
256297
> **Note:** I used the default database settings for the Docker container.
257298
> If you want to use a different database, you can modify the Dockerfile and docker-compose.yml files to use your
258299
> preferred database.
259-
260-
## Customization 🔧
261-
262-
1. In your Django project's `settings.py`, you can override the default values for the appointment scheduler. More
263-
information regarding available configurations can be found in
264-
the [documentation](https://github.com/adamspd/django-appointment/tree/main/docs/README.md#configuration).
265-
2. Modify these values as needed for your application, and the app will adapt to the new settings.
266-
3. For further customization, you can extend the provided models, views, and templates or create your own.
300+
267301
268302
## Compatibility Matrix 📊
269303

appointment/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
__url__ = "https://github.com/adamspd/django-appointment"
77
__package_website__ = "https://django-appt.adamspierredavid.com/"
88
__package_doc_url__ = "https://django-appt-doc.adamspierredavid.com/overview.html"
9-
__version__ = "3.5.2"
9+
__version__ = "3.6.0"
1010
__test_version__ = False
Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,90 @@
1-
from django.conf import settings
1+
# email_sender.py
2+
# Path: appointment/email_sender/email_sender.py
3+
24
from django.core.mail import mail_admins, send_mail
35
from django.template import loader
6+
from django_q.tasks import async_task
47

5-
from appointment.settings import APP_DEFAULT_FROM_EMAIL
8+
from appointment.settings import APP_DEFAULT_FROM_EMAIL, check_q_cluster
69

710

811
def has_required_email_settings():
12+
"""Check if all required email settings are configured and warn if any are missing."""
13+
from django.conf import settings as s
914
required_settings = [
10-
'EMAIL_BACKEND',
11-
'EMAIL_HOST',
12-
'EMAIL_PORT',
13-
'EMAIL_HOST_USER',
14-
'EMAIL_HOST_PASSWORD',
15-
'EMAIL_USE_TLS',
16-
'EMAIL_USE_LOCALTIME',
17-
'ADMINS',
15+
'EMAIL_BACKEND', 'EMAIL_HOST', 'EMAIL_PORT',
16+
'EMAIL_HOST_USER', 'EMAIL_HOST_PASSWORD', 'EMAIL_USE_TLS',
17+
'EMAIL_USE_LOCALTIME', 'ADMINS',
18+
]
19+
missing_settings = [
20+
setting_name for setting_name in required_settings if not hasattr(s, setting_name)
1821
]
1922

20-
for setting_name in required_settings:
21-
if not hasattr(settings, setting_name):
22-
print(f"Warning: '{setting_name}' not found in settings. Email functionality will be disabled.")
23-
return False
23+
if missing_settings:
24+
missing_settings_str = ", ".join(missing_settings)
25+
print(f"Warning: The following settings are missing in settings.py: {missing_settings_str}. "
26+
"Email functionality will be disabled.")
27+
return False
2428
return True
2529

2630

31+
def render_email_template(template_url, context):
32+
if template_url:
33+
return loader.render_to_string(template_url, context)
34+
return ""
35+
36+
2737
def send_email(recipient_list, subject: str, template_url: str = None, context: dict = None, from_email=None,
2838
message: str = None):
2939
if not has_required_email_settings():
3040
return
3141

32-
if from_email is None:
33-
from_email = APP_DEFAULT_FROM_EMAIL
34-
35-
html_message = ""
36-
37-
if template_url:
38-
html_message = loader.render_to_string(
39-
template_name=template_url,
40-
context=context
41-
)
42+
from_email = from_email or APP_DEFAULT_FROM_EMAIL
43+
html_message = render_email_template(template_url, context)
4244

43-
try:
44-
send_mail(
45-
subject=subject,
46-
message=message if not template_url else "",
47-
html_message=html_message if template_url else None,
48-
from_email=from_email,
49-
recipient_list=recipient_list,
50-
fail_silently=False,
45+
if get_use_django_q_for_emails() and check_q_cluster():
46+
# Asynchronously send the email using Django-Q
47+
async_task(
48+
"appointment.tasks.send_email_task", recipient_list=recipient_list, subject=subject,
49+
message=message, html_message=html_message if template_url else None, from_email=from_email
5150
)
52-
except Exception as e:
53-
print(f"Error sending email: {e}")
51+
else:
52+
# Synchronously send the email
53+
try:
54+
send_mail(
55+
subject=subject, message=message if not template_url else "",
56+
html_message=html_message if template_url else None, from_email=from_email,
57+
recipient_list=recipient_list, fail_silently=False,
58+
)
59+
except Exception as e:
60+
print(f"Error sending email: {e}")
5461

5562

5663
def notify_admin(subject: str, template_url: str = None, context: dict = None, message: str = None):
5764
if not has_required_email_settings():
5865
return
5966

60-
html_message = ""
61-
if template_url:
62-
html_message = loader.render_to_string(
63-
template_name=template_url,
64-
context=context
65-
)
67+
html_message = render_email_template(template_url, context)
68+
69+
if get_use_django_q_for_emails() and check_q_cluster():
70+
# Enqueue the task to send admin email asynchronously
71+
async_task('appointment.tasks.notify_admin_task', subject=subject, message=message, html_message=html_message)
72+
else:
73+
# Synchronously send admin email
74+
try:
75+
mail_admins(
76+
subject=subject, message=message if not template_url else "",
77+
html_message=html_message if template_url else None
78+
)
79+
except Exception as e:
80+
print(f"Error sending email to admin: {e}")
81+
82+
83+
def get_use_django_q_for_emails():
84+
"""Get the value of the USE_DJANGO_Q_FOR_EMAILS setting."""
6685
try:
67-
mail_admins(
68-
subject=subject,
69-
message=message if not template_url else "",
70-
html_message=html_message if template_url else None
71-
)
72-
except Exception as e:
73-
print(f"Error sending email to admin: {e}")
86+
from django.conf import settings
87+
return getattr(settings, 'USE_DJANGO_Q_FOR_EMAILS', False)
88+
except AttributeError:
89+
print("Error accessing USE_DJANGO_Q_FOR_EMAILS. Defaulting to False.")
90+
return False

appointment/tasks.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,38 @@ def send_email_reminder(to_email, first_name, reschedule_link, appointment_id):
2828
'recipient_type': recipient_type,
2929
}
3030
send_email(
31-
recipient_list=[to_email], subject=_("Reminder: Upcoming Appointment"),
32-
template_url='email_sender/reminder_email.html', context=email_context
31+
recipient_list=[to_email], subject=_("Reminder: Upcoming Appointment"),
32+
template_url='email_sender/reminder_email.html', context=email_context
3333
)
3434
# Notify the admin
3535
email_context['recipient_type'] = 'admin'
3636
notify_admin(
37-
subject=_("Admin Reminder: Upcoming Appointment"),
38-
template_url='email_sender/reminder_email.html', context=email_context
37+
subject=_("Admin Reminder: Upcoming Appointment"),
38+
template_url='email_sender/reminder_email.html', context=email_context
3939
)
40+
41+
42+
def send_email_task(recipient_list, subject, message, html_message, from_email):
43+
"""
44+
Task function to send an email asynchronously using Django's send_mail function.
45+
This function tries to send an email and logs an error if it fails.
46+
"""
47+
try:
48+
from django.core.mail import send_mail
49+
send_mail(
50+
subject=subject, message=message, html_message=html_message, from_email=from_email,
51+
recipient_list=recipient_list, fail_silently=False,
52+
)
53+
except Exception as e:
54+
print(f"Error sending email from task: {e}")
55+
56+
57+
def notify_admin_task(subject, message, html_message):
58+
"""
59+
Task function to send an admin email asynchronously.
60+
"""
61+
try:
62+
from django.core.mail import mail_admins
63+
mail_admins(subject=subject, message=message, html_message=html_message, fail_silently=False)
64+
except Exception as e:
65+
print(f"Error sending admin email from task: {e}")

appointment/tests/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,7 @@ def setUp(self):
12391239

12401240
@patch('appointment.views.create_and_save_appointment')
12411241
@patch('appointment.views.redirect_to_payment_or_thank_you_page')
1242+
@patch('django.conf.settings.USE_DJANGO_Q_FOR_EMAILS', new=False)
12421243
def test_create_appointment_success(self, mock_redirect, mock_create_and_save):
12431244
"""Test successful creation of an appointment and redirection."""
12441245
# Mock the appointment creation to return an Appointment instance

appointment/utils/db_helpers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ def create_and_save_appointment(ar, client_data: dict, appointment_data: dict, r
108108
)
109109
appointment.save()
110110
logger.info(f"New appointment created: {appointment.to_dict()}")
111-
schedule_email_reminder(appointment, request)
111+
if appointment.want_reminder:
112+
schedule_email_reminder(appointment, request)
112113
return appointment
113114

114115

@@ -179,7 +180,8 @@ def update_appointment_reminder(appointment, new_date, new_start_time, request,
179180
schedule_email_reminder(appointment, request, new_datetime)
180181
else:
181182
logger.info(
182-
f"Reminder for appointment {appointment.id} is not scheduled per user's preference or past datetime.")
183+
f"Reminder for appointment {appointment.id} is not scheduled per "
184+
f"user's preference or past datetime.")
183185

184186
# Update the appointment's reminder preference
185187
appointment.want_reminder = want_reminder

appointment/utils/email_ops.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N
102102
ui_db64 = urlsafe_base64_encode(force_bytes(user.pk))
103103
relative_set_passwd_link = reverse('appointment:set_passwd', args=[ui_db64, token.token])
104104
set_passwd_link = get_absolute_url_(relative_set_passwd_link, request=request)
105+
website_name = get_website_name()
105106

106107
message = _("""
107108
Hello {first_name},
@@ -122,7 +123,7 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N
122123
""").format(
123124
first_name=user.first_name,
124125
current_year=datetime.datetime.now().year,
125-
company=get_website_name(),
126+
company=website_name,
126127
activation_link=set_passwd_link,
127128
account_details=account_details if account_details else _("No additional details provided."),
128129
username=user.username
@@ -131,7 +132,7 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N
131132
# Assuming send_email is a method you have that sends an email
132133
send_email(
133134
recipient_list=[email],
134-
subject=_("Set Your Password for {company}").format(company=get_website_name()),
135+
subject=_("Set Your Password for {company}").format(company=website_name),
135136
message=message,
136137
)
137138

appointments/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@
6161
TEMPLATES = [
6262
{
6363
"BACKEND": "django.template.backends.django.DjangoTemplates",
64-
"DIRS": [BASE_DIR / 'appointment/templates']
65-
,
64+
"DIRS": [BASE_DIR / 'appointment/templates'],
6665
"APP_DIRS": True,
6766
"OPTIONS": {
6867
"context_processors": [
@@ -146,6 +145,7 @@
146145
EMAIL_SUBJECT_PREFIX = ""
147146
EMAIL_USE_LOCALTIME = True
148147
SERVER_EMAIL = EMAIL_HOST_USER
148+
USE_DJANGO_Q_FOR_EMAILS = True
149149

150150
ADMINS = [
151151
(os.getenv('ADMIN_NAME'), os.getenv('ADMIN_EMAIL')),

0 commit comments

Comments
 (0)