diff --git a/appointment/messages_.py b/appointment/messages_.py index 40b9aca..4923145 100644 --- a/appointment/messages_.py +++ b/appointment/messages_.py @@ -8,10 +8,9 @@ from django.utils.translation import gettext as _ -thank_you_no_payment = _("""We're excited to have you on board! Thank you for booking us. -We hope you enjoy using our services and find them valuable.""") +thank_you_no_payment = _("""We're excited to have you on board!""") -thank_you_payment_plus_down = _("""We're excited to have you on board! Thank you for booking us. The next step is +thank_you_payment_plus_down = _("""We're excited to have you on board! The next step is to pay for the booking. You have the choice to pay the whole amount or a down deposit. If you choose the deposit, you will have to pay the rest of the amount on the day of the booking.""") @@ -19,3 +18,7 @@ the booking.""") appt_updated_successfully = _("Appointment updated successfully.") + +passwd_set_successfully = _("We've successfully set your password. You can now log in to your account.") + +passwd_error = _("The password reset link is invalid or has expired.") diff --git a/appointment/models.py b/appointment/models.py index 7c88fa2..62b7812 100644 --- a/appointment/models.py +++ b/appointment/models.py @@ -9,6 +9,7 @@ import datetime import random import string +import uuid from babel.numbers import get_currency_symbol from django.conf import settings @@ -776,6 +777,84 @@ def check_code(self, code): return self.code == code +class PasswordResetToken(models.Model): + """ + Represents a password reset token for users. + + Author: Adams Pierre David + Version: 3.x.x + Since: 3.x.x + """ + + class TokenStatus(models.TextChoices): + ACTIVE = 'active', 'Active' + VERIFIED = 'verified', 'Verified' + INVALIDATED = 'invalidated', 'Invalidated' + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='password_reset_tokens') + token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + expires_at = models.DateTimeField() + status = models.CharField(max_length=11, choices=TokenStatus.choices, default=TokenStatus.ACTIVE) + + # meta data + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Password reset token for {self.user} [{self.token} status: {self.status} expires at {self.expires_at}]" + + @property + def is_expired(self): + """Checks if the token has expired.""" + return timezone.now() >= self.expires_at + + @property + def is_verified(self): + """Checks if the token has been verified.""" + return self.status == self.TokenStatus.VERIFIED + + @property + def is_active(self): + """Checks if the token is still active.""" + return self.status == self.TokenStatus.ACTIVE + + @property + def is_invalidated(self): + """Checks if the token has been invalidated.""" + return self.status == self.TokenStatus.INVALIDATED + + @classmethod + def create_token(cls, user, expiration_minutes=60): + """ + Generates a new token for the user with a specified expiration time. + Before creating a new token, invalidate all previous active tokens by marking them as invalidated. + """ + cls.objects.filter(user=user, expires_at__gte=timezone.now(), status=cls.TokenStatus.ACTIVE).update( + status=cls.TokenStatus.INVALIDATED) + expires_at = timezone.now() + timezone.timedelta(minutes=expiration_minutes) + token = cls.objects.create(user=user, expires_at=expires_at, status=cls.TokenStatus.ACTIVE) + return token + + def mark_as_verified(self): + """ + Marks the token as verified. + """ + self.status = self.TokenStatus.VERIFIED + self.save(update_fields=['status']) + + @classmethod + def verify_token(cls, user, token): + """ + Verifies if the provided token is valid and belongs to the given user. + Additionally, checks if the token has not been marked as verified. + """ + try: + return cls.objects.get(user=user, token=token, expires_at__gte=timezone.now(), + status=cls.TokenStatus.ACTIVE) + except cls.DoesNotExist: + return None + + class DayOff(models.Model): staff_member = models.ForeignKey(StaffMember, on_delete=models.CASCADE) start_date = models.DateField() diff --git a/appointment/tasks.py b/appointment/tasks.py index e45f935..8e78a50 100644 --- a/appointment/tasks.py +++ b/appointment/tasks.py @@ -20,16 +20,19 @@ def send_email_reminder(to_email, first_name, reschedule_link, appointment_id): # Fetch the appointment using appointment_id logger.info(f"Sending reminder to {to_email} for appointment {appointment_id}") appointment = Appointment.objects.get(id=appointment_id) + recipient_type = 'client' email_context = { 'first_name': first_name, 'appointment': appointment, 'reschedule_link': reschedule_link, + 'recipient_type': recipient_type, } send_email( 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 diff --git a/appointment/templates/appointment/default_thank_you.html b/appointment/templates/appointment/default_thank_you.html index 0fb4f4c..7d066b6 100644 --- a/appointment/templates/appointment/default_thank_you.html +++ b/appointment/templates/appointment/default_thank_you.html @@ -18,7 +18,6 @@

{% trans "See you soon" %} !

{% trans "Appointment details" %}: