Skip to content

Commit

Permalink
Merge pull request #201 from adamspd/153-add-possibility-to-send-all-…
Browse files Browse the repository at this point in the history
…emails-using-django-q

Added possibility to send all emails with django q
  • Loading branch information
adamspd authored May 24, 2024
2 parents d0ccbef + 2e31549 commit 0e8b3f1
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 79 deletions.
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban

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

**Consequence**: A permanent ban from any sort of public interaction within
Expand Down
56 changes: 45 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@ see their [release notes](https://github.com/adamspd/django-appointment/tree/mai
```
To be able to send email reminders after adding `django_q` to your `INSTALLED_APPS`, you must add this variable
`Q_CLUSTER` in your Django's `settings.py`. If done, and users check the box to receive reminders, you and them
`Q_CLUSTER` in your Django's `settings.py`. If done, and users check the box to receive reminders, you and they
will receive an email reminder 24 hours before the appointment.

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

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

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

## Template Configuration 📝

If you're using a base.html template, you must include the following block in your template:
```
{% block customCSS %}
{% endblock %}
{% block title %}
{% endblock %}
{% block description %}
{% endblock %}
{% block body %}
{% endblock %}
{% block customJS %}
{% endblock %}
```
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).
## Customization 🔧
1. In your Django project's `settings.py`, you can override the default values for the appointment scheduler.
More information regarding available configurations can be found in
the [documentation](https://github.com/adamspd/django-appointment/tree/main/docs/README.md#configuration).
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 @@ -194,8 +233,10 @@ Here's how you can set it up:
You should include your email host user and password for Django's email functionality (if you want it to work):

```plaintext
EMAIL_HOST_USER=your_email@gmail.com
EMAIL_HOST_USER=your_email@example.com
EMAIL_HOST_PASSWORD=your_password
ADMIN_NAME='Example Name'
ADMIN_EMAIL=django-appt@example.com
```

> **Note:** The `.env` file is used to store sensitive information and should not be committed to version control.
Expand Down Expand Up @@ -256,14 +297,7 @@ 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.
## Customization 🔧
1. In your Django project's `settings.py`, you can override the default values for the appointment scheduler. More
information regarding available configurations can be found in
the [documentation](https://github.com/adamspd/django-appointment/tree/main/docs/README.md#configuration).
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.
## Compatibility Matrix 📊
Expand Down
2 changes: 1 addition & 1 deletion appointment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
__url__ = "https://github.com/adamspd/django-appointment"
__package_website__ = "https://django-appt.adamspierredavid.com/"
__package_doc_url__ = "https://django-appt-doc.adamspierredavid.com/overview.html"
__version__ = "3.5.2"
__version__ = "3.6.0"
__test_version__ = False
111 changes: 64 additions & 47 deletions appointment/email_sender/email_sender.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,90 @@
from django.conf import settings
# email_sender.py
# Path: appointment/email_sender/email_sender.py

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

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


def has_required_email_settings():
"""Check if all required email settings are configured and warn if any are missing."""
from django.conf import settings as s
required_settings = [
'EMAIL_BACKEND',
'EMAIL_HOST',
'EMAIL_PORT',
'EMAIL_HOST_USER',
'EMAIL_HOST_PASSWORD',
'EMAIL_USE_TLS',
'EMAIL_USE_LOCALTIME',
'ADMINS',
'EMAIL_BACKEND', 'EMAIL_HOST', 'EMAIL_PORT',
'EMAIL_HOST_USER', 'EMAIL_HOST_PASSWORD', 'EMAIL_USE_TLS',
'EMAIL_USE_LOCALTIME', 'ADMINS',
]
missing_settings = [
setting_name for setting_name in required_settings if not hasattr(s, setting_name)
]

for setting_name in required_settings:
if not hasattr(settings, setting_name):
print(f"Warning: '{setting_name}' not found in settings. Email functionality will be disabled.")
return False
if missing_settings:
missing_settings_str = ", ".join(missing_settings)
print(f"Warning: The following settings are missing in settings.py: {missing_settings_str}. "
"Email functionality will be disabled.")
return False
return True


def render_email_template(template_url, context):
if template_url:
return loader.render_to_string(template_url, context)
return ""


def send_email(recipient_list, subject: str, template_url: str = None, context: dict = None, from_email=None,
message: str = None):
if not has_required_email_settings():
return

if from_email is None:
from_email = APP_DEFAULT_FROM_EMAIL

html_message = ""

if template_url:
html_message = loader.render_to_string(
template_name=template_url,
context=context
)
from_email = from_email or APP_DEFAULT_FROM_EMAIL
html_message = render_email_template(template_url, context)

try:
send_mail(
subject=subject,
message=message if not template_url else "",
html_message=html_message if template_url else None,
from_email=from_email,
recipient_list=recipient_list,
fail_silently=False,
if get_use_django_q_for_emails() and check_q_cluster():
# Asynchronously send the email using Django-Q
async_task(
"appointment.tasks.send_email_task", recipient_list=recipient_list, subject=subject,
message=message, html_message=html_message if template_url else None, from_email=from_email
)
except Exception as e:
print(f"Error sending email: {e}")
else:
# Synchronously send the email
try:
send_mail(
subject=subject, message=message if not template_url else "",
html_message=html_message if template_url else None, from_email=from_email,
recipient_list=recipient_list, fail_silently=False,
)
except Exception as e:
print(f"Error sending email: {e}")


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

html_message = ""
if template_url:
html_message = loader.render_to_string(
template_name=template_url,
context=context
)
html_message = render_email_template(template_url, context)

if get_use_django_q_for_emails() and check_q_cluster():
# Enqueue the task to send admin email asynchronously
async_task('appointment.tasks.notify_admin_task', subject=subject, message=message, html_message=html_message)
else:
# Synchronously send admin email
try:
mail_admins(
subject=subject, message=message if not template_url else "",
html_message=html_message if template_url else None
)
except Exception as e:
print(f"Error sending email to admin: {e}")


def get_use_django_q_for_emails():
"""Get the value of the USE_DJANGO_Q_FOR_EMAILS setting."""
try:
mail_admins(
subject=subject,
message=message if not template_url else "",
html_message=html_message if template_url else None
)
except Exception as e:
print(f"Error sending email to admin: {e}")
from django.conf import settings
return getattr(settings, 'USE_DJANGO_Q_FOR_EMAILS', False)
except AttributeError:
print("Error accessing USE_DJANGO_Q_FOR_EMAILS. Defaulting to False.")
return False
34 changes: 30 additions & 4 deletions appointment/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,38 @@ def send_email_reminder(to_email, first_name, reschedule_link, appointment_id):
'recipient_type': recipient_type,
}
send_email(
recipient_list=[to_email], subject=_("Reminder: Upcoming Appointment"),
template_url='email_sender/reminder_email.html', context=email_context
recipient_list=[to_email], subject=_("Reminder: Upcoming Appointment"),
template_url='email_sender/reminder_email.html', context=email_context
)
# Notify the admin
email_context['recipient_type'] = 'admin'
notify_admin(
subject=_("Admin Reminder: Upcoming Appointment"),
template_url='email_sender/reminder_email.html', context=email_context
subject=_("Admin Reminder: Upcoming Appointment"),
template_url='email_sender/reminder_email.html', context=email_context
)


def send_email_task(recipient_list, subject, message, html_message, from_email):
"""
Task function to send an email asynchronously using Django's send_mail function.
This function tries to send an email and logs an error if it fails.
"""
try:
from django.core.mail import send_mail
send_mail(
subject=subject, message=message, html_message=html_message, from_email=from_email,
recipient_list=recipient_list, fail_silently=False,
)
except Exception as e:
print(f"Error sending email from task: {e}")


def notify_admin_task(subject, message, html_message):
"""
Task function to send an admin email asynchronously.
"""
try:
from django.core.mail import mail_admins
mail_admins(subject=subject, message=message, html_message=html_message, fail_silently=False)
except Exception as e:
print(f"Error sending admin email from task: {e}")
1 change: 1 addition & 0 deletions appointment/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,7 @@ def setUp(self):

@patch('appointment.views.create_and_save_appointment')
@patch('appointment.views.redirect_to_payment_or_thank_you_page')
@patch('django.conf.settings.USE_DJANGO_Q_FOR_EMAILS', new=False)
def test_create_appointment_success(self, mock_redirect, mock_create_and_save):
"""Test successful creation of an appointment and redirection."""
# Mock the appointment creation to return an Appointment instance
Expand Down
6 changes: 4 additions & 2 deletions appointment/utils/db_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def create_and_save_appointment(ar, client_data: dict, appointment_data: dict, r
)
appointment.save()
logger.info(f"New appointment created: {appointment.to_dict()}")
schedule_email_reminder(appointment, request)
if appointment.want_reminder:
schedule_email_reminder(appointment, request)
return appointment


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

# Update the appointment's reminder preference
appointment.want_reminder = want_reminder
Expand Down
5 changes: 3 additions & 2 deletions appointment/utils/email_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N
ui_db64 = urlsafe_base64_encode(force_bytes(user.pk))
relative_set_passwd_link = reverse('appointment:set_passwd', args=[ui_db64, token.token])
set_passwd_link = get_absolute_url_(relative_set_passwd_link, request=request)
website_name = get_website_name()

message = _("""
Hello {first_name},
Expand All @@ -122,7 +123,7 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N
""").format(
first_name=user.first_name,
current_year=datetime.datetime.now().year,
company=get_website_name(),
company=website_name,
activation_link=set_passwd_link,
account_details=account_details if account_details else _("No additional details provided."),
username=user.username
Expand All @@ -131,7 +132,7 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N
# Assuming send_email is a method you have that sends an email
send_email(
recipient_list=[email],
subject=_("Set Your Password for {company}").format(company=get_website_name()),
subject=_("Set Your Password for {company}").format(company=website_name),
message=message,
)

Expand Down
4 changes: 2 additions & 2 deletions appointments/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / 'appointment/templates']
,
"DIRS": [BASE_DIR / 'appointment/templates'],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
Expand Down Expand Up @@ -146,6 +145,7 @@
EMAIL_SUBJECT_PREFIX = ""
EMAIL_USE_LOCALTIME = True
SERVER_EMAIL = EMAIL_HOST_USER
USE_DJANGO_Q_FOR_EMAILS = True

ADMINS = [
(os.getenv('ADMIN_NAME'), os.getenv('ADMIN_EMAIL')),
Expand Down
Loading

0 comments on commit 0e8b3f1

Please sign in to comment.