From a95b0dbdf0eec6c6fb1c7f4f3c8358955db09c8c Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sat, 31 Aug 2024 14:31:34 +0200 Subject: [PATCH 1/2] Enhanced logger config Colored output added for readability Improved formatting Added more logging to spot unwanted behavior easily --- appointment/email_sender/email_sender.py | 13 +++--- appointment/logger_config.py | 52 +++++++++++++++++++++--- appointment/settings.py | 8 ++-- appointment/tasks.py | 11 +++-- appointment/utils/db_helpers.py | 5 ++- appointment/utils/email_ops.py | 33 ++++++++------- appointment/utils/session.py | 4 +- appointment/utils/view_helpers.py | 3 +- appointment/views.py | 32 ++++++++------- check_version.py | 5 ++- requirements.txt | 1 + setup.cfg | 1 + 12 files changed, 118 insertions(+), 50 deletions(-) diff --git a/appointment/email_sender/email_sender.py b/appointment/email_sender/email_sender.py index 4f6ef77..465c263 100644 --- a/appointment/email_sender/email_sender.py +++ b/appointment/email_sender/email_sender.py @@ -5,8 +5,11 @@ 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__) + def has_required_email_settings(): """Check if all required email settings are configured and warn if any are missing.""" @@ -22,8 +25,8 @@ def has_required_email_settings(): 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.") + logger.warning(f"Warning: The following settings are missing in settings.py: {missing_settings_str}. " + "Email functionality will be disabled.") return False return True @@ -57,7 +60,7 @@ def send_email(recipient_list, subject: str, template_url: str = None, context: recipient_list=recipient_list, fail_silently=False, ) except Exception as e: - print(f"Error sending email: {e}") + logger.error(f"Error sending email: {e}") def notify_admin(subject: str, template_url: str = None, context: dict = None, message: str = None): @@ -77,7 +80,7 @@ def notify_admin(subject: str, template_url: str = None, context: dict = None, m html_message=html_message if template_url else None ) except Exception as e: - print(f"Error sending email to admin: {e}") + logger.error(f"Error sending email to admin: {e}") def get_use_django_q_for_emails(): @@ -86,5 +89,5 @@ def get_use_django_q_for_emails(): 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.") + logger.error("Error accessing USE_DJANGO_Q_FOR_EMAILS. Defaulting to False.") return False diff --git a/appointment/logger_config.py b/appointment/logger_config.py index 2a4be32..096924c 100644 --- a/appointment/logger_config.py +++ b/appointment/logger_config.py @@ -8,11 +8,51 @@ import logging import sys +from datetime import datetime -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +import colorama -# TODO: change the logger format configuration later -# configure basicConfig with the formatter, log level, and handlers -logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG, - handlers=[logging.StreamHandler(sys.stdout)]) +# Initialize colorama for cross-platform color support +colorama.init() + + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': colorama.Fore.BLUE, + 'INFO': colorama.Fore.GREEN, + 'WARNING': colorama.Fore.YELLOW, + 'ERROR': colorama.Fore.RED, + 'CRITICAL': colorama.Fore.RED + colorama.Style.BRIGHT, + } + + def format(self, record): + log_color = self.COLORS.get(record.levelname, colorama.Fore.WHITE) + log_time = datetime.fromtimestamp(record.created).strftime('%d/%b/%Y %H:%M:%S') + + log_msg = ( + f"{log_color}[{log_time}] {record.levelname:<4}{colorama.Style.RESET_ALL} " + f"{colorama.Fore.LIGHTBLUE_EX}{record.name}:{record.funcName}:{record.lineno}{colorama.Style.RESET_ALL} " + f"- {record.getMessage()}" + ) + + if record.exc_info: + log_msg += '\n' + self.formatException(record.exc_info) + return log_msg + + +def get_logger(name): + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + # Create a colored formatter + formatter = ColoredFormatter() + + # Create a stream handler + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(logging.DEBUG) + stream_handler.setFormatter(formatter) + + # Add the handler to the logger + logger.addHandler(stream_handler) + + return logger diff --git a/appointment/settings.py b/appointment/settings.py index ae7c160..63d7789 100644 --- a/appointment/settings.py +++ b/appointment/settings.py @@ -9,7 +9,9 @@ from django.conf import settings from django.conf.global_settings import DEFAULT_FROM_EMAIL -from appointment.logger_config import logger +from appointment.logger_config import get_logger + +logger = get_logger(__name__) APPOINTMENT_BASE_TEMPLATE = getattr(settings, 'APPOINTMENT_BASE_TEMPLATE', 'base_templates/base.html') APPOINTMENT_ADMIN_BASE_TEMPLATE = getattr(settings, 'APPOINTMENT_ADMIN_BASE_TEMPLATE', 'base_templates/base.html') @@ -32,7 +34,7 @@ def check_q_cluster(): Returns True if configurations are correct, otherwise False. """ missing_conf = [] - + 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" @@ -64,6 +66,6 @@ def check_q_cluster(): for warning in missing_conf: logger.warning(warning) return False - print(f"Missing conf: {missing_conf}") # Both 'django_q' is installed and 'Q_CLUSTER' is configured + logger.info("Django Q cluster is properly configured") return True diff --git a/appointment/tasks.py b/appointment/tasks.py index 4e6a265..9c1a2ae 100644 --- a/appointment/tasks.py +++ b/appointment/tasks.py @@ -8,9 +8,11 @@ from django.utils.translation import gettext as _ from appointment.email_sender import notify_admin, send_email -from appointment.logger_config import logger +from appointment.logger_config import get_logger from appointment.models import Appointment +logger = get_logger(__name__) + def send_email_reminder(to_email, first_name, reschedule_link, appointment_id): """ @@ -32,6 +34,7 @@ def send_email_reminder(to_email, first_name, reschedule_link, appointment_id): template_url='email_sender/reminder_email.html', context=email_context ) # Notify the admin + logger.info(f"Sending admin reminder for appointment {appointment_id}") email_context['recipient_type'] = 'admin' notify_admin( subject=_("Admin Reminder: Upcoming Appointment"), @@ -46,12 +49,13 @@ def send_email_task(recipient_list, subject, message, html_message, from_email): """ try: from django.core.mail import send_mail + logger.info(f"Sending email to {recipient_list} with subject: {subject}") 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}") + logger.error(f"Error sending email from task: {e}") def notify_admin_task(subject, message, html_message): @@ -60,6 +64,7 @@ def notify_admin_task(subject, message, html_message): """ try: from django.core.mail import mail_admins + logger.info(f"Sending admin email with subject: {subject}") 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}") + logger.error(f"Error sending admin email from task: {e}") diff --git a/appointment/utils/db_helpers.py b/appointment/utils/db_helpers.py index fefede7..328d6bb 100644 --- a/appointment/utils/db_helpers.py +++ b/appointment/utils/db_helpers.py @@ -19,7 +19,7 @@ from django_q.models import Schedule from django_q.tasks import schedule -from appointment.logger_config import logger +from appointment.logger_config import get_logger from appointment.settings import ( APPOINTMENT_BUFFER_TIME, APPOINTMENT_FINISH_TIME, APPOINTMENT_LEAD_TIME, APPOINTMENT_PAYMENT_URL, APPOINTMENT_SLOT_DURATION, APPOINTMENT_WEBSITE_NAME @@ -37,6 +37,8 @@ EmailVerificationCode = apps.get_model('appointment', 'EmailVerificationCode') AppointmentRescheduleHistory = apps.get_model('appointment', 'AppointmentRescheduleHistory') +logger = get_logger(__name__) + def calculate_slots(start_time, end_time, buffer_time, slot_duration): """Calculate the available slots between the given start and end times using the given buffer time and slot duration @@ -109,6 +111,7 @@ def create_and_save_appointment(ar, client_data: dict, appointment_data: dict, r appointment.save() logger.info(f"New appointment created: {appointment.to_dict()}") if appointment.want_reminder: + logger.info(f"User wants a reminder for appointment {appointment.id}, scheduling it...") schedule_email_reminder(appointment, request) return appointment diff --git a/appointment/utils/email_ops.py b/appointment/utils/email_ops.py index 5834ba4..65c8b05 100644 --- a/appointment/utils/email_ops.py +++ b/appointment/utils/email_ops.py @@ -16,11 +16,14 @@ from appointment import messages_ as email_messages from appointment.email_sender import notify_admin, send_email +from appointment.logger_config import get_logger from appointment.models import AppointmentRequest, EmailVerificationCode, PasswordResetToken from appointment.settings import APPOINTMENT_PAYMENT_URL from appointment.utils.date_time import convert_24_hour_time_to_12_hour_time from appointment.utils.db_helpers import get_absolute_url_, get_website_name +logger = get_logger(__name__) + def get_thank_you_message(ar: AppointmentRequest) -> str: """ @@ -83,8 +86,8 @@ def send_thank_you_email(ar: AppointmentRequest, user, request, email: str, appo 'reschedule_link': reschedule_link, } send_email( - recipient_list=[email], subject=_("Thank you for booking us."), - template_url='email_sender/thank_you_email.html', context=email_context + recipient_list=[email], subject=_("Thank you for booking us."), + template_url='email_sender/thank_you_email.html', context=email_context ) @@ -121,24 +124,25 @@ def send_reset_link_to_staff_member(user, request, email: str, account_details=N Regards, {company} """).format( - first_name=user.first_name, - current_year=datetime.datetime.now().year, - company=website_name, - activation_link=set_passwd_link, - account_details=account_details if account_details else _("No additional details provided."), - username=user.username + first_name=user.first_name, + current_year=datetime.datetime.now().year, + company=website_name, + activation_link=set_passwd_link, + account_details=account_details if account_details else _("No additional details provided."), + username=user.username ) # 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=website_name), - message=message, + recipient_list=[email], + subject=_("Set Your Password for {company}").format(company=website_name), + message=message, ) def notify_admin_about_appointment(appointment, client_name: str): """Notify the admin and the staff member about a new appointment request.""" + logger.info(f"Sending admin notification for new appointment {appointment.id}") email_context = { 'client_name': client_name, 'appointment': appointment @@ -146,9 +150,10 @@ def notify_admin_about_appointment(appointment, client_name: str): subject = _("New Appointment Request for ") + client_name staff_member = appointment.get_staff_member() - # Assuming notify_admin and send_email are previously defined functions notify_admin(subject=subject, template_url='email_sender/admin_new_appointment_email.html', context=email_context) if staff_member.user.email not in settings.ADMINS: + logger.info( + f"Let's notify the staff member as well for new appointment {appointment.id} since they are not an admin.") send_email(recipient_list=[staff_member.user.email], subject=subject, template_url='email_sender/admin_new_appointment_email.html', context=email_context) @@ -190,8 +195,8 @@ def send_reschedule_confirmation_email(request, reschedule_history, appointment_ subject = _("Confirm Your Appointment Rescheduling") send_email( - recipient_list=[email], subject=subject, - template_url='email_sender/reschedule_email.html', context=email_context + recipient_list=[email], subject=subject, + template_url='email_sender/reschedule_email.html', context=email_context ) diff --git a/appointment/utils/session.py b/appointment/utils/session.py index 754a4c3..618620d 100644 --- a/appointment/utils/session.py +++ b/appointment/utils/session.py @@ -11,10 +11,12 @@ from django.utils.translation import gettext as _ from phonenumber_field.phonenumber import PhoneNumber -from appointment.logger_config import logger +from appointment.logger_config import get_logger from appointment.utils.db_helpers import get_user_by_email from appointment.utils.email_ops import send_verification_email +logger = get_logger(__name__) + def handle_existing_email(request, client_data, appointment_data, appointment_request_id, id_request): """ diff --git a/appointment/utils/view_helpers.py b/appointment/utils/view_helpers.py index f3c8922..4268845 100644 --- a/appointment/utils/view_helpers.py +++ b/appointment/utils/view_helpers.py @@ -8,13 +8,12 @@ import uuid -from django.conf import settings from django.utils.translation import get_language, to_locale def get_locale() -> str: """Get the current locale based on the user's language settings, without the country code. - Used in the javascript files. + Used in the JavaScript files. Can't use the lang_country format because it is not supported. :return: The current locale as a string (language code only) diff --git a/appointment/views.py b/appointment/views.py index 9565b63..d8cfb51 100644 --- a/appointment/views.py +++ b/appointment/views.py @@ -22,7 +22,7 @@ from django.utils.translation import gettext as _ from appointment.forms import AppointmentForm, AppointmentRequestForm, SlotForm, ClientDataForm -from appointment.logger_config import logger +from appointment.logger_config import get_logger from appointment.models import ( Appointment, AppointmentRequest, AppointmentRescheduleHistory, Config, DayOff, EmailVerificationCode, PasswordResetToken, Service, @@ -49,6 +49,8 @@ CLIENT_MODEL = get_user_model() +logger = get_logger(__name__) + @require_ajax def get_available_slots_ajax(request): @@ -87,7 +89,7 @@ def get_available_slots_ajax(request): custom_data['staff_member'] = sm.get_staff_member_name() if not is_working_day_: message = _("Not a working day for {staff_member}. Please select another date!").format( - staff_member=sm.get_staff_member_first_name()) + staff_member=sm.get_staff_member_first_name()) custom_data['available_slots'] = [] return json_response(message=message, custom_data=custom_data, success=False, error_code=ErrorCode.INVALID_DATE) available_slots = get_available_slots_for_staff(selected_date, sm) @@ -124,8 +126,8 @@ def get_next_available_date_ajax(request, service_id): # Fetch the days off for the staff days_off = DayOff.objects.filter(staff_member=staff_member).filter( - Q(start_date__lte=date.today(), end_date__gte=date.today()) | - Q(start_date__gte=date.today()) + Q(start_date__lte=date.today(), end_date__gte=date.today()) | + Q(start_date__gte=date.today()) ) current_date = date.today() @@ -241,8 +243,8 @@ def appointment_request_submit(request): messages.error(request, _("Selected staff member does not exist.")) else: logger.info( - f"date_f {form.cleaned_data['date']} start_time {form.cleaned_data['start_time']} end_time " - f"{form.cleaned_data['end_time']} service {form.cleaned_data['service']} staff {staff_member}") + f"date_f {form.cleaned_data['date']} start_time {form.cleaned_data['start_time']} end_time " + f"{form.cleaned_data['end_time']} service {form.cleaned_data['service']} staff {staff_member}") ar = form.save() request.session[f'appointment_completed_{ar.id_request}'] = False # Redirect the user to the account creation page @@ -265,10 +267,12 @@ def redirect_to_payment_or_thank_you_page(appointment): :return: The redirect response. """ if (APPOINTMENT_PAYMENT_URL is not None and APPOINTMENT_PAYMENT_URL != '') and appointment.service_is_paid(): + logger.info("Creating payment info and get payment url") payment_url = create_payment_info_and_get_url(appointment) return HttpResponseRedirect(payment_url) else: # Determine the correct thank-you URL based on whether APPOINTMENT_THANK_YOU_URL is provided and not empty + logger.info("Redirecting to the thank-you page") thank_you_url_key = 'appointment:default_thank_you' if APPOINTMENT_THANK_YOU_URL: thank_you_url_key = APPOINTMENT_THANK_YOU_URL @@ -320,7 +324,7 @@ def appointment_client_information(request, appointment_request_id, id_request): if is_email_in_db: return handle_existing_email(request, client_data, appointment_data, appointment_request_id, id_request) - logger.info(f"Creating a new user with the given information {client_data}") + logger.info(f"Creating a new user: {client_data}") user = create_new_user(client_data) messages.success(request, _("An account was created for you.")) @@ -527,12 +531,12 @@ def reschedule_appointment_submit(request): reason_for_rescheduling = request.POST.get('reason_for_rescheduling') if form.is_valid(): arh = AppointmentRescheduleHistory.objects.create( - appointment_request=ar, - date=date_, - start_time=start_time, - end_time=end_time, - staff_member=staff_member, - reason_for_rescheduling=reason_for_rescheduling + appointment_request=ar, + date=date_, + start_time=start_time, + end_time=end_time, + staff_member=staff_member, + reason_for_rescheduling=reason_for_rescheduling ) messages.success(request, _("Appointment rescheduled successfully")) context = get_generic_context_with_extra(request, {}, admin=False) @@ -554,7 +558,7 @@ def confirm_reschedule(request, id_request): if reschedule_history.reschedule_status != 'pending' or not reschedule_history.still_valid(): error_message = _("O-o-oh! This link is no longer valid.") if not reschedule_history.still_valid() else _( - "O-o-oh! Can't find the pending reschedule request.") + "O-o-oh! Can't find the pending reschedule request.") context = get_generic_context_with_extra(request, {"error_message": error_message}, admin=False) return render(request, 'error_pages/404_not_found.html', status=404, context=context) diff --git a/check_version.py b/check_version.py index 591e7c4..d983ad0 100644 --- a/check_version.py +++ b/check_version.py @@ -4,6 +4,9 @@ import requests from appointment import __package_name__, __test_version__, __version__ +from appointment.logger_config import get_logger + +logger = get_logger(__name__) def check_package_version(package_name, current_version, github_ref_=None): @@ -33,7 +36,7 @@ def check_package_version(package_name, current_version, github_ref_=None): if response.status_code == 200: released_versions = response.json()["releases"].keys() if current_version in released_versions: - print(f"Version {current_version} already exists on {'TestPyPI' if is_test_version else 'PyPI'}!") + logger.info(f"Version {current_version} already exists on {'TestPyPI' if is_test_version else 'PyPI'}!") version_exists = True else: publish_to_pypi = not is_test_version diff --git a/requirements.txt b/requirements.txt index bc94216..8070ae6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ setuptools==72.2.0 requests~=2.32.3 django-q2==1.6.2 python-dotenv==1.0.1 +colorama~=0.4.6 diff --git a/setup.cfg b/setup.cfg index e0169d7..91a9732 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,3 +28,4 @@ install_requires = django-phonenumber-field==8.0.0 babel==2.15.0 django-q2==1.6.2 + colorama~=0.4.6 From bf11daa602e325ada6c3c607d459ad09a8ff009a Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sat, 31 Aug 2024 14:38:55 +0200 Subject: [PATCH 2/2] Remove appointment id in log to pass github-advanced-security bot test --- appointment/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appointment/tasks.py b/appointment/tasks.py index 9c1a2ae..29a1145 100644 --- a/appointment/tasks.py +++ b/appointment/tasks.py @@ -34,7 +34,7 @@ def send_email_reminder(to_email, first_name, reschedule_link, appointment_id): template_url='email_sender/reminder_email.html', context=email_context ) # Notify the admin - logger.info(f"Sending admin reminder for appointment {appointment_id}") + logger.info(f"Sending admin reminder also") email_context['recipient_type'] = 'admin' notify_admin( subject=_("Admin Reminder: Upcoming Appointment"),