diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7fa116..1a4a7e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: testing: runs-on: ubuntu-20.04 strategy: - max-parallel: 4 + max-parallel: 10 matrix: python-version: [3.10.5] fail-fast: [false] diff --git a/appointment/views.py b/appointment/views.py index 9565b63..1cc99b0 100644 --- a/appointment/views.py +++ b/appointment/views.py @@ -16,10 +16,12 @@ from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.encoding import force_str from django.utils.http import urlsafe_base64_decode from django.utils.timezone import get_current_timezone_name from django.utils.translation import gettext as _ +from django.views.generic import FormView, RedirectView, TemplateView, View from appointment.forms import AppointmentForm, AppointmentRequestForm, SlotForm, ClientDataForm from appointment.logger_config import logger @@ -50,110 +52,117 @@ CLIENT_MODEL = get_user_model() -@require_ajax -def get_available_slots_ajax(request): - """This view function handles AJAX requests to get available slots for a selected date. +@method_decorator(require_ajax, name='dispatch') +class GetAvailableSlotsAjax(View): + """This view handles AJAX requests to get available slots for a selected date. :param request: The request instance. :return: A JSON response containing available slots, selected date, an error flag, and an optional error message. """ - - slot_form = SlotForm(request.GET) - error_code = 0 - if not slot_form.is_valid(): - custom_data = {'error': True, 'available_slots': [], 'date_chosen': ''} - if 'selected_date' in slot_form.errors: - error_code = ErrorCode.PAST_DATE - elif 'staff_member' in slot_form.errors: - error_code = ErrorCode.STAFF_ID_REQUIRED - message = list(slot_form.errors.as_data().items())[0][1][0].messages[0] # dirty way to keep existing behavior - return json_response(message=message, custom_data=custom_data, success=False, - error_code=error_code) - - selected_date = slot_form.cleaned_data['selected_date'] - sm = slot_form.cleaned_data['staff_member'] - date_chosen = selected_date.strftime("%a, %B %d, %Y") - custom_data = {'date_chosen': date_chosen} - - days_off_exist = check_day_off_for_staff(staff_member=sm, date=selected_date) - if days_off_exist: - message = _("Day off. Please select another date!") - custom_data['available_slots'] = [] - return json_response(message=message, custom_data=custom_data, success=False, error_code=ErrorCode.INVALID_DATE) - # if selected_date is not a working day for the staff, return an empty list of slots and 'message' is Day Off - weekday_num = get_weekday_num_from_date(selected_date) - is_working_day_ = is_working_day(staff_member=sm, day=weekday_num) - - 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()) - 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) - - # Check if the selected_date is today and filter out past slots - if selected_date == date.today(): - current_time = timezone.now().time() - available_slots = [slot for slot in available_slots if slot.time() > current_time] - - custom_data['available_slots'] = [slot.strftime('%I:%M %p') for slot in available_slots] - if len(available_slots) == 0: - custom_data['error'] = True - message = _('No availability') - return json_response(message=message, custom_data=custom_data, success=False, error_code=ErrorCode.INVALID_DATE) - custom_data['error'] = False - return json_response(message='Successfully retrieved available slots', custom_data=custom_data, success=True) + def get(self, request, *args, **kwargs): + slot_form = SlotForm(request.GET) + error_code = 0 + if not slot_form.is_valid(): + custom_data = {'error': True, 'available_slots': [], 'date_chosen': ''} + if 'selected_date' in slot_form.errors: + error_code = ErrorCode.PAST_DATE + elif 'staff_member' in slot_form.errors: + error_code = ErrorCode.STAFF_ID_REQUIRED + message = list(slot_form.errors.as_data().items())[0][1][0].messages[0] # dirty way to keep existing behavior + return json_response(message=message, custom_data=custom_data, success=False, + error_code=error_code) + + selected_date = slot_form.cleaned_data['selected_date'] + sm = slot_form.cleaned_data['staff_member'] + date_chosen = selected_date.strftime("%a, %B %d, %Y") + custom_data = {'date_chosen': date_chosen} + + days_off_exist = check_day_off_for_staff(staff_member=sm, date=selected_date) + if days_off_exist: + message = _("Day off. Please select another date!") + custom_data['available_slots'] = [] + return json_response(message=message, custom_data=custom_data, success=False, error_code=ErrorCode.INVALID_DATE) + # if selected_date is not a working day for the staff, return an empty list of slots and 'message' is Day Off + weekday_num = get_weekday_num_from_date(selected_date) + is_working_day_ = is_working_day(staff_member=sm, day=weekday_num) + + 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()) + 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) + + # Check if the selected_date is today and filter out past slots + if selected_date == date.today(): + current_time = timezone.now().time() + available_slots = [slot for slot in available_slots if slot.time() > current_time] + + custom_data['available_slots'] = [slot.strftime('%I:%M %p') for slot in available_slots] + if len(available_slots) == 0: + custom_data['error'] = True + message = _('No availability') + return json_response(message=message, custom_data=custom_data, success=False, error_code=ErrorCode.INVALID_DATE) + custom_data['error'] = False + return json_response(message='Successfully retrieved available slots', custom_data=custom_data, success=True) + + +get_available_slots_ajax = GetAvailableSlotsAjax.as_view() # TODO: service id and staff id are not checked -@require_ajax -def get_next_available_date_ajax(request, service_id): +@method_decorator(require_ajax, name='dispatch') +class GetNextAvailableDataAjaxView(View): """This view function handles AJAX requests to get the next available date for a service. :param request: The request instance. :param service_id: The ID of the service. :return: A JSON response containing the next available date. """ - staff_id = request.GET.get('staff_member') + def get(self, request, service_id, *args, **kwargs): + staff_id = request.GET.get('staff_member') + + # If staff_id is not provided, you should handle it accordingly. + if staff_id and staff_id != 'none': + staff_member = get_object_or_404(StaffMember, pk=staff_id) + service = get_object_or_404(Service, pk=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()) + ) - # If staff_id is not provided, you should handle it accordingly. - if staff_id and staff_id != 'none': - staff_member = get_object_or_404(StaffMember, pk=staff_id) - service = get_object_or_404(Service, pk=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()) - ) - - current_date = date.today() - next_available_date = None - day_offset = 0 - - while next_available_date is None: - potential_date = current_date + timedelta(days=day_offset) - - # Check if the potential date is a day off for the staff - is_day_off = any([day_off.start_date <= potential_date <= day_off.end_date for day_off in days_off]) - # Check if the potential date is a working day for the staff - weekday_num = get_weekday_num_from_date(potential_date) - is_working_day_ = is_working_day(staff_member=staff_member, day=weekday_num) - - if not is_day_off and is_working_day_: - x, available_slots = get_appointments_and_slots(potential_date, service) - if available_slots: - next_available_date = potential_date - - day_offset += 1 - message = _('Successfully retrieved next available date') - data = {'next_available_date': next_available_date.isoformat()} - return json_response(message=message, custom_data=data, success=True) - else: - data = {'error': True} - message = _('No staff member selected') - return json_response(message=message, custom_data=data, success=False, error_code=ErrorCode.STAFF_ID_REQUIRED) + current_date = date.today() + next_available_date = None + day_offset = 0 + + while next_available_date is None: + potential_date = current_date + timedelta(days=day_offset) + + # Check if the potential date is a day off for the staff + is_day_off = any([day_off.start_date <= potential_date <= day_off.end_date for day_off in days_off]) + # Check if the potential date is a working day for the staff + weekday_num = get_weekday_num_from_date(potential_date) + is_working_day_ = is_working_day(staff_member=staff_member, day=weekday_num) + + if not is_day_off and is_working_day_: + x, available_slots = get_appointments_and_slots(potential_date, service) + if available_slots: + next_available_date = potential_date + + day_offset += 1 + message = _('Successfully retrieved next available date') + data = {'next_available_date': next_available_date.isoformat()} + return json_response(message=message, custom_data=data, success=True) + else: + data = {'error': True} + message = _('No staff member selected') + return json_response(message=message, custom_data=data, success=False, error_code=ErrorCode.STAFF_ID_REQUIRED) + + +get_next_available_date_ajax = GetNextAvailableDataAjaxView.as_view() def get_non_working_days_ajax(request): @@ -174,88 +183,100 @@ def get_non_working_days_ajax(request): return json_response(message=message, custom_data=custom_data, success=not error, error_code=error_code) -def appointment_request(request, service_id=None, staff_member_id=None): - """This view function handles requests to book an appointment for a service. +class AppointmentRequestView(TemplateView): + """This view handles requests to book an appointment for a service. :param request: The request instance. :param service_id: The ID of the service. :param staff_member_id: The ID of the staff member. :return: The rendered HTML page. """ - - service = None - staff_member = None - all_staff_members = None - available_slots = [] - config = Config.objects.first() - label = config.app_offered_by_label if config else _("Offered by") - - if service_id: - service = get_object_or_404(Service, pk=service_id) - all_staff_members = StaffMember.objects.filter(services_offered=service) - - # If only one staff member for a service, choose them by default and fetch their slots. - if all_staff_members.count() == 1: - staff_member = all_staff_members.first() - x, available_slots = get_appointments_and_slots(date.today(), service) - - # If a specific staff member is selected, fetch their slots. - if staff_member_id: - staff_member = get_object_or_404(StaffMember, pk=staff_member_id) - y, available_slots = get_appointments_and_slots(date.today(), service) - - page_title = f"{service.name} - {get_website_name()}" - page_description = _("Book an appointment for {s} at {wn}.").format(s=service.name, wn=get_website_name()) - - date_chosen = date.today().strftime("%a, %B %d, %Y") - extra_context = { - 'service': service, - 'staff_member': staff_member, - 'all_staff_members': all_staff_members, - 'page_title': page_title, - 'page_description': page_description, - 'available_slots': available_slots, - 'date_chosen': date_chosen, - 'locale': get_locale(), - 'timezoneTxt': get_current_timezone_name(), - 'label': label - } - context = get_generic_context_with_extra(request, extra_context, admin=False) - return render(request, 'appointment/appointments.html', context=context) - - -def appointment_request_submit(request): - """This view function handles the submission of the appointment request form. + template_name = 'appointment/appointments.html' + + def get(self, request, service_id=None, staff_member_id=None): + service = None + staff_member = None + all_staff_members = None + available_slots = [] + config = Config.objects.first() + label = config.app_offered_by_label if config else _("Offered by") + + if service_id: + service = get_object_or_404(Service, pk=service_id) + all_staff_members = StaffMember.objects.filter(services_offered=service) + + # If only one staff member for a service, choose them by default and fetch their slots. + if all_staff_members.count() == 1: + staff_member = all_staff_members.first() + x, available_slots = get_appointments_and_slots(date.today(), service) + + # If a specific staff member is selected, fetch their slots. + if staff_member_id: + staff_member = get_object_or_404(StaffMember, pk=staff_member_id) + y, available_slots = get_appointments_and_slots(date.today(), service) + + page_title = f"{service.name} - {get_website_name()}" + page_description = _("Book an appointment for {s} at {wn}.").format(s=service.name, wn=get_website_name()) + + date_chosen = date.today().strftime("%a, %B %d, %Y") + extra_context = { + 'service': service, + 'staff_member': staff_member, + 'all_staff_members': all_staff_members, + 'page_title': page_title, + 'page_description': page_description, + 'available_slots': available_slots, + 'date_chosen': date_chosen, + 'locale': get_locale(), + 'timezoneTxt': get_current_timezone_name(), + 'label': label + } + context = get_generic_context_with_extra(request, extra_context, admin=False) + return render(request, self.template_name, context=context) + + +appointment_request = AppointmentRequestView.as_view() + + +class AppointmentRequestSubmitView(FormView): + """This view handles the submission of the appointment request form. :param request: The request instance. :return: The rendered HTML page. """ - if request.method == 'POST': - form = AppointmentRequestForm(request.POST) - if form.is_valid(): - # Use form.cleaned_data to get the cleaned and validated data - staff_member = form.cleaned_data['staff_member'] - - staff_exists = StaffMember.objects.filter(id=staff_member.id).exists() - if not staff_exists: - 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}") - ar = form.save() - request.session[f'appointment_completed_{ar.id_request}'] = False - # Redirect the user to the account creation page - return redirect('appointment:appointment_client_information', appointment_request_id=ar.id, - id_request=ar.id_request) + template_name = 'appointment/appointments.html' + form_class = AppointmentRequestForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = context['form'] + context.update(get_generic_context_with_extra(self.request, {'form': form}, admin=False)) + return context + + def form_valid(self, form): + # Use form.cleaned_data to get the cleaned and validated data + staff_member = form.cleaned_data['staff_member'] + + staff_exists = StaffMember.objects.filter(id=staff_member.id).exists() + if not staff_exists: + messages.error(self.request, _("Selected staff member does not exist.")) else: - # Handle the case if the form is not valid - messages.error(request, _('There was an error in your submission. Please check the form and try again.')) - else: - form = AppointmentRequestForm() + 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}") + ar = form.save() + self.request.session[f'appointment_completed_{ar.id_request}'] = False + # Redirect the user to the account creation page + return redirect('appointment:appointment_client_information', appointment_request_id=ar.id, + id_request=ar.id_request) + + def form_invalid(self, form): + # Handle the case if the form is not valid + messages.error(self.request, _('There was an error in your submission. Please check the form and try again.')) + return super().form_invalid(form) + - context = get_generic_context_with_extra(request, {'form': form}, admin=False) - return render(request, 'appointment/appointments.html', context=context) +appointment_request_submit = AppointmentRequestSubmitView.as_view() def redirect_to_payment_or_thank_you_page(appointment): @@ -291,20 +312,47 @@ def create_appointment(request, appointment_request_obj, client_data, appointmen return redirect_to_payment_or_thank_you_page(appointment) -def appointment_client_information(request, appointment_request_id, id_request): - """This view function handles client information submission for an appointment. +class AppointmentClientInformationView(TemplateView): + """This view handles client information submission for an appointment. :param request: The request instance. :param appointment_request_id: The ID of the appointment request. :param id_request: The unique ID of the appointment request. :return: The rendered HTML page. """ - ar = get_object_or_404(AppointmentRequest, pk=appointment_request_id) - if request.session.get(f'appointment_submitted_{id_request}', False): - context = get_generic_context_with_extra(request, {'service_id': ar.service_id}, admin=False) - return render(request, 'error_pages/304_already_submitted.html', context=context) - - if request.method == 'POST': + template_name = "appointment/appointment_client_information.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + extra_context = { + 'ar': self.ar, + 'APPOINTMENT_PAYMENT_URL': APPOINTMENT_PAYMENT_URL, + 'service_name': self.ar.service.name, + } + context.update(get_generic_context_with_extra(self.request, extra_context, admin=False)) + return context + + def dispatch(self, request, *args, **kwargs): + self.id_request = self.kwargs['id_request'] + self.appointment_request_id = self.kwargs['appointment_request_id'] + self.ar = get_object_or_404(AppointmentRequest, pk=self.appointment_request_id) + if request.session.get(f'appointment_submitted_{self.id_request}', False): + context = get_generic_context_with_extra(request, {'service_id': self.ar.service_id}, admin=False) + return render(request, 'error_pages/304_already_submitted.html', context=context) + return super().dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + kwargs.update({ + 'form': AppointmentForm(), + 'client_data_form': ClientDataForm(), + }) + response = super().get(request, *args, **kwargs) + return response + + def create_user(self, client_data): + return create_new_user(client_data) + + def post(self, request, *args, **kwargs): appointment_form = AppointmentForm(request.POST) client_data_form = ClientDataForm(request.POST) @@ -312,35 +360,26 @@ def appointment_client_information(request, appointment_request_id, id_request): appointment_data = appointment_form.cleaned_data client_data = client_data_form.cleaned_data payment_type = request.POST.get('payment_type') - ar.payment_type = payment_type - ar.save() + self.ar.payment_type = payment_type + self.ar.save() # Check if email is already in the database is_email_in_db = CLIENT_MODEL.objects.filter(email__exact=client_data['email']).exists() if is_email_in_db: - return handle_existing_email(request, client_data, appointment_data, appointment_request_id, id_request) + return handle_existing_email(request, client_data, appointment_data, self.appointment_request_id, self.id_request) logger.info(f"Creating a new user with the given information {client_data}") - user = create_new_user(client_data) + user = self.create_new_user(client_data) messages.success(request, _("An account was created for you.")) # Create a new appointment - response = create_appointment(request, ar, client_data, appointment_data) - request.session.setdefault(f'appointment_submitted_{id_request}', True) + response = create_appointment(request, self.ar, client_data, appointment_data) + request.session.setdefault(f'appointment_submitted_{self.id_request}', True) return response - else: - appointment_form = AppointmentForm() - client_data_form = ClientDataForm() + return super().get(request, *args, **kwargs) + - extra_context = { - 'ar': ar, - 'APPOINTMENT_PAYMENT_URL': APPOINTMENT_PAYMENT_URL, - 'form': appointment_form, - 'client_data_form': client_data_form, - 'service_name': ar.service.name, - } - context = get_generic_context_with_extra(request, extra_context, admin=False) - return render(request, 'appointment/appointment_client_information.html', context=context) +appointment_client_information = AppointmentClientInformationView.as_view() def verify_user_and_login(request, user, code): @@ -360,21 +399,32 @@ def verify_user_and_login(request, user, code): return False -def enter_verification_code(request, appointment_request_id, id_request): - """This view function handles the submission of the email verification code. +class EnterVerificationCodeView(TemplateView): + """This view handles the submission of the email verification code. :param request: The request instance. :param appointment_request_id: The ID of the appointment request. :param id_request: The unique ID of the appointment request. :return: The rendered HTML page. """ - if request.method == 'POST': + template_name = "appointment/enter_verification_code.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + extra_context = { + 'appointment_request_id': kwargs['appointment_request_id'], + 'id_request': self.kwargs['id_request'], + } + context.update(get_generic_context_with_extra(self.request, extra_context, admin=False)) + return context + + def post(self, request, *args, **kwargs): email = request.session.get('email') code = request.POST.get('code') user = get_user_by_email(email) if verify_user_and_login(request, user, code): - appointment_request_object = AppointmentRequest.objects.get(pk=appointment_request_id) + appointment_request_object = AppointmentRequest.objects.get(pk=self.kwargs['appointment_request_id']) appointment_data = get_appointment_data_from_session(request) response = create_appointment(request=request, appointment_request_obj=appointment_request_object, client_data={'email': email}, appointment_data=appointment_data) @@ -390,130 +440,172 @@ def enter_verification_code(request, appointment_request_id, id_request): return response else: messages.error(request, _("Invalid verification code.")) + return super().get(request, *args, **kwargs) - # base_template = request.session.get('BASE_TEMPLATE', '') - # if base_template == '': - # base_template = APPOINTMENT_BASE_TEMPLATE - extra_context = { - 'appointment_request_id': appointment_request_id, - 'id_request': id_request, - } - context = get_generic_context_with_extra(request, extra_context, admin=False) - return render(request, 'appointment/enter_verification_code.html', context) +enter_verification_code = EnterVerificationCodeView.as_view() -def default_thank_you(request, appointment_id): - """This view function handles the default thank you page. + +class DefaultThankYouView(TemplateView): + """This view handles the default thank you page. :param request: The request instance. :param appointment_id: The ID of the appointment. :return: The rendered HTML page. """ - appointment = get_object_or_404(Appointment, pk=appointment_id) - ar = appointment.appointment_request - email = appointment.client.email - appointment_details = { - _('Service'): appointment.get_service_name(), - _('Appointment Date'): appointment.get_appointment_date(), - _('Appointment Time'): appointment.appointment_request.start_time, - _('Duration'): appointment.get_service_duration() - } - account_details = { - _('Email address'): email, - } - if username_in_user_model(): - account_details[_('Username')] = appointment.client.username - send_thank_you_email(ar=ar, user=appointment.client, email=email, appointment_details=appointment_details, - account_details=account_details, request=request) - extra_context = { - 'appointment': appointment, - } - context = get_generic_context_with_extra(request, extra_context, admin=False) - return render(request, 'appointment/default_thank_you.html', context=context) - - -def set_passwd(request, uidb64, token): - extra = { - 'page_title': _("Error"), - 'page_message': passwd_error, - 'page_description': _("Please try resetting your password again or contact support for help."), - } - context_ = get_generic_context_with_extra(request, extra, admin=False) - try: - uid = force_str(urlsafe_base64_decode(uidb64)) - user = get_user_model().objects.get(pk=uid) - token_verification = PasswordResetToken.verify_token(user, token) - if token_verification is not None: - if request.method == 'POST': - form = SetPasswordForm(user, request.POST) - if form.is_valid(): - form.save() - messages.success(request, _("Password reset successfully.")) - # Invalidate the token after successful password reset - token_verification.mark_as_verified() - extra = { - 'page_title': _("Password Reset Successful"), - 'page_message': passwd_set_successfully, - 'page_description': _("You can now use your new password to log in.") - } - context = get_generic_context_with_extra(request, extra, admin=False) - return render(request, 'appointment/thank_you.html', context=context) - else: - form = SetPasswordForm(user) # Display empty form for GET request - else: + template_name = "appointment/default_thank_you.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + extra_context = { + 'appointment': self.appointment, + } + context.update(get_generic_context_with_extra(self.request, extra_context, admin=False)) + return context + + def get(self, request, *args, **kwargs): + self.appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) + ar = self.appointment.appointment_request + email = self.appointment.client.email + appointment_details = { + _('Service'): self.appointment.get_service_name(), + _('Appointment Date'): self.appointment.get_appointment_date(), + _('Appointment Time'): self.appointment.appointment_request.start_time, + _('Duration'): self.appointment.get_service_duration() + } + account_details = { + _('Email address'): email, + } + if username_in_user_model(): + account_details[_('Username')] = self.appointment.client.username + send_thank_you_email(ar=ar, user=self.appointment.client, email=email, appointment_details=appointment_details, + account_details=account_details, request=request) + return super().get(request, *args, **kwargs) + + +default_thank_you = DefaultThankYouView.as_view() + + +class SetPasswdView(FormView): + template_name = "appointment/set_password.html" + form_class = SetPasswordForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + extra = { + 'page_title': _("Error"), + 'page_message': passwd_error, + 'page_description': _("Please try resetting your password again or contact support for help."), + } + extra_context = get_generic_context_with_extra(self.request, extra, admin=False) + context.update(extra_context) + return context + + def dispatch(self, request, *args, **kwargs): + self.uidb64 = self.kwargs['uidb64'] + self.token = self.kwargs['token'] + self.uid = force_str(urlsafe_base64_decode(self.uidb64)) + self.user = get_user_model().objects.get(pk=self.uid) + self.token_verification = PasswordResetToken.verify_token(self.user, self.token) + + if self.token_verification is None: messages.error(request, passwd_error) - return render(request, 'appointment/thank_you.html', context=context_) - except (TypeError, ValueError, OverflowError, get_user_model().DoesNotExist): - messages.error(request, _("The password reset link is invalid or has expired.")) - return render(request, 'appointment/thank_you.html', context=context_) - - context_.update({'form': form}) - return render(request, 'appointment/set_password.html', context_) - - -def prepare_reschedule_appointment(request, id_request): - ar = get_object_or_404(AppointmentRequest, id_request=id_request) - - if not can_appointment_be_rescheduled(ar): - url = reverse('appointment:appointment_request', kwargs={'service_id': ar.service.id}) - context = get_generic_context_with_extra(request, {'url': url, }, admin=False) - logger.error(f"Appointment with id_request {id_request} cannot be rescheduled") - return render(request, 'error_pages/403_forbidden_rescheduling.html', context=context, status=403) - - service = ar.service - selected_sm = ar.staff_member - config = Config.objects.first() - label = config.app_offered_by_label if config else _("Offered by") - # if staff change allowed, filter all staff offering the service otherwise, filter only the selected staff member - staff_filter_criteria = {'id': ar.staff_member.id} if not staff_change_allowed_on_reschedule() else { - 'services_offered': ar.service} - all_staff_members = StaffMember.objects.filter(**staff_filter_criteria) - available_slots = get_available_slots_for_staff(ar.date, selected_sm) - page_title = _("Rescheduling appointment for {s}").format(s=service.name) - page_description = _("Reschedule your appointment for {s} at {wn}.").format(s=service.name, wn=get_website_name()) - date_chosen = ar.date.strftime("%a, %B %d, %Y") - - extra_context = { - 'service': service, - 'staff_member': selected_sm, - 'all_staff_members': all_staff_members, - 'page_title': page_title, - 'page_description': page_description, - 'available_slots': [slot.strftime('%I:%M %p') for slot in available_slots], - 'date_chosen': date_chosen, - 'locale': get_locale(), - 'timezoneTxt': get_current_timezone_name(), - 'label': label, - 'rescheduled_date': ar.date.strftime("%Y-%m-%d"), - 'page_header': page_title, - 'ar_id_request': ar.id_request, - } - context = get_generic_context_with_extra(request, extra_context, admin=False) - return render(request, 'appointment/appointments.html', context=context) - - -def reschedule_appointment_submit(request): - if request.method == 'POST': + context = self.get_context_data() + return render(request, 'appointment/thank_you.html', context=context) + + try: + return super().dispatch(request, *args, **kwargs) + except (TypeError, ValueError, OverflowError, get_user_model().DoesNotExist): + messages.error(request, _("The password reset link is invalid or has expired.")) + context = self.get_context_data() + return render(request, 'appointment/thank_you.html', context=context) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs.update({ + 'user': self.user, + }) + return kwargs + + def form_valid(self, form): + form.save() + messages.success(self.request, _("Password reset successfully.")) + # Invalidate the token after successful password reset + self.token_verification.mark_as_verified() + extra = { + 'page_title': _("Password Reset Successful"), + 'page_message': passwd_set_successfully, + 'page_description': _("You can now use your new password to log in.") + } + context = get_generic_context_with_extra(self.request, extra, admin=False) + return render(self.request, 'appointment/thank_you.html', context=context) + + +set_passwd = SetPasswdView.as_view() + + +class PrepareRescheduleAppointment(TemplateView): + template_name = 'appointment/appointments.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + service = self.ar.service + selected_sm = self.ar.staff_member + config = Config.objects.first() + label = config.app_offered_by_label if config else _("Offered by") + # if staff change allowed, filter all staff offering the service otherwise, filter only the selected staff member + staff_filter_criteria = {'id': self.ar.staff_member.id} if not staff_change_allowed_on_reschedule() else { + 'services_offered': self.ar.service} + all_staff_members = StaffMember.objects.filter(**staff_filter_criteria) + available_slots = get_available_slots_for_staff(self.ar.date, selected_sm) + page_title = _("Rescheduling appointment for {s}").format(s=service.name) + page_description = _("Reschedule your appointment for {s} at {wn}.").format(s=service.name, wn=get_website_name()) + date_chosen = self.ar.date.strftime("%a, %B %d, %Y") + + extra_context = { + 'service': service, + 'staff_member': selected_sm, + 'all_staff_members': all_staff_members, + 'page_title': page_title, + 'page_description': page_description, + 'available_slots': [slot.strftime('%I:%M %p') for slot in available_slots], + 'date_chosen': date_chosen, + 'locale': get_locale(), + 'timezoneTxt': get_current_timezone_name(), + 'label': label, + 'rescheduled_date': self.ar.date.strftime("%Y-%m-%d"), + 'page_header': page_title, + 'ar_id_request': self.ar.id_request, + } + context.update(get_generic_context_with_extra(self.request, extra_context, admin=False)) + return context + + def get(self, request, *args, **kwargs): + self.id_request = self.kwargs['id_request'] + self.ar = get_object_or_404(AppointmentRequest, id_request=self.id_request) + if not can_appointment_be_rescheduled(self.ar): + url = reverse('appointment:appointment_request', kwargs={'service_id': self.ar.service.id}) + context = get_generic_context_with_extra(self.request, {'url': url, }, admin=False) + logger.error(f"Appointment with id_request {self.id_request} cannot be rescheduled") + return render(self.request, 'error_pages/403_forbidden_rescheduling.html', context=context, status=403) + return super().get(request, *args, **kwargs) + + +prepare_reschedule_appointment = PrepareRescheduleAppointment.as_view() + + +class RescheduleAppointmentSubmitView(TemplateView): + template_name = "appointment/appointments.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = AppointmentRequestForm() + extra_context = {'form': form} + context.update(get_generic_context_with_extra(self.request, extra_context, admin=False)) + return context + + def post(self, request, *args, **kwargs): form = AppointmentRequestForm(request.POST) # get form values: ar_id_request = request.POST.get('appointment_request_id') @@ -541,49 +633,55 @@ def reschedule_appointment_submit(request): send_reschedule_confirmation_email(request=request, reschedule_history=arh, first_name=client_first_name, email=email, appointment_request=ar) return render(request, 'appointment/rescheduling_thank_you.html', context=context) - else: - messages.error(request, _("There was an error in your submission. Please check the form and try again.")) - else: - form = AppointmentRequestForm() - context = get_generic_context_with_extra(request, {'form': form}, admin=False) - return render(request, 'appointment/appointments.html', context=context) - - -def confirm_reschedule(request, id_request): - reschedule_history = get_object_or_404(AppointmentRescheduleHistory, id_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.") - 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) - - ar = reschedule_history.appointment_request - - # Store previous details for logging or other purposes - previous_details = { - 'date': ar.date, - 'start_time': ar.start_time, - 'end_time': ar.end_time, - 'staff_member': ar.staff_member, - } - - # Update AppointmentRequest with new details - ar.date = reschedule_history.date - ar.start_time = reschedule_history.start_time - ar.end_time = reschedule_history.end_time - ar.staff_member = reschedule_history.staff_member - ar.save(update_fields=['date', 'start_time', 'end_time', 'staff_member']) - - reschedule_history.date = previous_details['date'] - reschedule_history.start_time = previous_details['start_time'] - reschedule_history.end_time = previous_details['end_time'] - reschedule_history.staff_member = previous_details['staff_member'] - reschedule_history.reschedule_status = 'confirmed' - reschedule_history.save(update_fields=['date', 'start_time', 'end_time', 'staff_member', 'reschedule_status']) - - messages.success(request, _("Appointment rescheduled successfully")) - # notify admin and the concerned staff admin about client's rescheduling - client_name = Appointment.objects.get(appointment_request=ar).client.get_full_name() - notify_admin_about_reschedule(reschedule_history, ar, client_name) - return redirect('appointment:default_thank_you', appointment_id=ar.appointment.id) + messages.error(request, _("There was an error in your submission. Please check the form and try again.")) + return super().get(request, *args, **kwargs) + + +reschedule_appointment_submit = RescheduleAppointmentSubmitView.as_view() + + +class ConfirmRescheduleView(RedirectView): + def get(self, request, *args, **kwargs): + id_request = self.kwargs['id_request'] + self.reschedule_history = get_object_or_404(AppointmentRescheduleHistory, id_request=id_request) + + if self.reschedule_history.reschedule_status != 'pending' or not self.reschedule_history.still_valid(): + error_message = _("O-o-oh! This link is no longer valid.") if not self.reschedule_history.still_valid() else _( + "O-o-oh! Can't find the pending reschedule request.") + context = get_generic_context_with_extra(self.request, {"error_message": error_message}, admin=False) + return render(self.request, 'error_pages/404_not_found.html', status=404, context=context) + return super().get(request, *args, **kwargs) + + def get_redirect_url(self, *args, **kwargs): + ar = self.reschedule_history.appointment_request + # Store previous details for logging or other purposes + previous_details = { + 'date': ar.date, + 'start_time': ar.start_time, + 'end_time': ar.end_time, + 'staff_member': ar.staff_member, + } + + # Update AppointmentRequest with new details + ar.date = self.reschedule_history.date + ar.start_time = self.reschedule_history.start_time + ar.end_time = self.reschedule_history.end_time + ar.staff_member = self.reschedule_history.staff_member + ar.save(update_fields=['date', 'start_time', 'end_time', 'staff_member']) + + self.reschedule_history.date = previous_details['date'] + self.reschedule_history.start_time = previous_details['start_time'] + self.reschedule_history.end_time = previous_details['end_time'] + self.reschedule_history.staff_member = previous_details['staff_member'] + self.reschedule_history.reschedule_status = 'confirmed' + self.reschedule_history.save(update_fields=['date', 'start_time', 'end_time', 'staff_member', 'reschedule_status']) + + messages.success(self.request, _("Appointment rescheduled successfully")) + # notify admin and the concerned staff admin about client's rescheduling + client_name = Appointment.objects.get(appointment_request=ar).client.get_full_name() + notify_admin_about_reschedule(self.reschedule_history, ar, client_name) + url = reverse('appointment:default_thank_you', kwargs={'appointment_id': ar.appointment.id}) + return url + + +confirm_reschedule = ConfirmRescheduleView.as_view() diff --git a/requirements-test.txt b/requirements-test.txt index c7ce995..e849df4 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,8 @@ -Pillow==10.3.0 -phonenumbers==8.13.38 +Pillow==10.4.0 +phonenumbers==8.13.39 django-phonenumber-field==7.3.0 babel==2.15.0 -setuptools==70.0.0 +setuptools==70.2.0 requests~=2.32.3 django-q2==1.6.2 python-dotenv==1.0.1 diff --git a/requirements.txt b/requirements.txt index bf6c2a5..cb9d730 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ Django==5.0.6 -Pillow==10.3.0 -phonenumbers==8.13.38 +Pillow==10.4.0 +phonenumbers==8.13.39 django-phonenumber-field==7.3.0 babel==2.15.0 -setuptools==70.0.0 +setuptools==70.2.0 requests~=2.32.3 django-q2==1.6.2 python-dotenv==1.0.1