diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 4bb6ad5..22bd6ee 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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 diff --git a/README.md b/README.md index f6190e6..d7a2c67 100644 --- a/README.md +++ b/README.md @@ -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 = { @@ -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 @@ -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// 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. @@ -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. @@ -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 📊 diff --git a/appointment/__init__.py b/appointment/__init__.py index 34cd265..17663c1 100644 --- a/appointment/__init__.py +++ b/appointment/__init__.py @@ -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 diff --git a/appointment/email_sender/email_sender.py b/appointment/email_sender/email_sender.py index 5336429..4f6ef77 100644 --- a/appointment/email_sender/email_sender.py +++ b/appointment/email_sender/email_sender.py @@ -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 diff --git a/appointment/tasks.py b/appointment/tasks.py index 8e78a50..4e6a265 100644 --- a/appointment/tasks.py +++ b/appointment/tasks.py @@ -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}") diff --git a/appointment/tests/test_views.py b/appointment/tests/test_views.py index 3870708..6da2da9 100644 --- a/appointment/tests/test_views.py +++ b/appointment/tests/test_views.py @@ -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 diff --git a/appointment/utils/db_helpers.py b/appointment/utils/db_helpers.py index f93c9e5..fefede7 100644 --- a/appointment/utils/db_helpers.py +++ b/appointment/utils/db_helpers.py @@ -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 @@ -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 diff --git a/appointment/utils/email_ops.py b/appointment/utils/email_ops.py index 48e9c25..5834ba4 100644 --- a/appointment/utils/email_ops.py +++ b/appointment/utils/email_ops.py @@ -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}, @@ -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 @@ -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, ) diff --git a/appointments/settings.py b/appointments/settings.py index ba9b798..c0d87aa 100644 --- a/appointments/settings.py +++ b/appointments/settings.py @@ -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": [ @@ -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')), diff --git a/docs/README.md b/docs/README.md index d897193..bfc4280 100644 --- a/docs/README.md +++ b/docs/README.md @@ -90,20 +90,21 @@ configurations: ### Essential Configurations: -These configurations are crucial for the application to operate correctly; -the values provided here represent the default settings. -Change them to suit your needs. +These configurations are needed for the application to operate correctly; most of them can also be set in the Config +model in the admin panel. However, you can also set them here. +The values provided here represent the default settings. Change them to suit your needs. ```python APPOINTMENT_BASE_TEMPLATE = 'base_templates/base.html' -APPOINTMENT_ADMIN_BASE_TEMPLATE = 'base_templates/base.html' # 🆕 (optional) Specify a different base template for the admin panel -APPOINTMENT_WEBSITE_NAME = 'Website' +APPOINTMENT_ADMIN_BASE_TEMPLATE = 'base_templates/base.html' # (optional) Specify a different base template for the admin panel +APPOINTMENT_WEBSITE_NAME = 'Website' # Can be set in the Config model. APPOINTMENT_PAYMENT_URL = None APPOINTMENT_THANK_YOU_URL = None -APPOINTMENT_BUFFER_TIME = 0 # 🆕 Minutes between now and the first available slot for the current day (doesn't affect future dates) -APPOINTMENT_SLOT_DURATION = 30 # Duration of each appointment slot in minutes -APPOINTMENT_LEAD_TIME = (9, 0) # Start time of the appointment slots (in 24-hour format) -APPOINTMENT_FINISH_TIME = (16, 30) # End time of the appointment slots (in 24-hour format) +APPOINTMENT_BUFFER_TIME = 0 # Can be set in the Config Model. Minutes between now and the first available slot for the current day (doesn't affect future dates) +APPOINTMENT_SLOT_DURATION = 30 # Can be set in the Config Model. Duration of each appointment slot in minutes +APPOINTMENT_LEAD_TIME = (9, 0) # Can be set in the Config Model. Start time of the appointment slots (in 24-hour format) +APPOINTMENT_FINISH_TIME = (16, 30) # Can be set in the Config Model. End time of the appointment slots (in 24-hour format) +USE_DJANGO_Q_FOR_EMAILS = False # 🆕 Use Django Q for sending ALL emails. ``` For email reminders with Django Q, you can configure the following settings after adding `django_q` to @@ -124,6 +125,15 @@ Q_CLUSTER = { If those settings are not provided, the application won't send email reminders. You also have to run `python manage.py qcluster` to start the Django Q cluster. +#### New Configurations: + +A new configuration has been added to the application to allow you to use Django Q for sending all emails. +If you're already using Django Q to send email reminders, it is nice to set this configuration. + +```python +USE_DJANGO_Q_FOR_EMAILS = False # 🆕 Use Django Q for sending email reminders +``` + ### Django Default Settings Utilization: The application leverages some of the default settings from your Django project.