Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions appointment/messages_.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@

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.""")

thank_you_payment = _("""We're excited to have you on board! Thank you for booking us. The next step is to pay for
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.")
79 changes: 79 additions & 0 deletions appointment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import datetime
import random
import string
import uuid

from babel.numbers import get_currency_symbol
from django.conf import settings
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions appointment/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion appointment/templates/appointment/default_thank_you.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ <h1 class="thank-you-title">{% trans "See you soon" %} !</h1>
<p class="appointment-details-title">{% trans "Appointment details" %}:</p>
<ul class="appointment-details">
<li>{% trans 'Service' %}: {{ appointment.get_service_name }}</li>
<li>{% trans 'Appointment ID' %}: {{ appointment.id_request }}</li>
<li>{% trans 'Appointment Date' %}: {{ appointment.get_appointment_date }}</li>
<li>{% trans 'Appointment Time' %}: {{ appointment.get_start_time }}</li>
<li>{% trans 'Duration' %}: {{ appointment.get_service_duration }}</li>
Expand Down
104 changes: 104 additions & 0 deletions appointment/templates/appointment/set_password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}

.container {
background-color: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}

h2 {
color: #333;
margin-bottom: 20px;
}

.messages {
list-style: none;
padding: 0;
margin-bottom: 20px;
}

.messages li {
background-color: #f8d7da;
color: #721c24;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}

form {
display: flex;
flex-direction: column;
}

form p {
margin-bottom: 10px;
}

input[type="password"] {
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 4px;
}

button {
background-color: #007bff;
color: #fff;
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}

button:hover {
background-color: #0056b3;
}
</style>
{% endblock %}
{% block title %}
{% trans 'Reset Your Password' %}
{% endblock %}
{% block description %}
{% trans 'Reset Your Password' %}
{% endblock %}
{% block body %}
<div class="container">
<h2>{% trans 'Reset Your Password' %}</h2>
<!-- Display messages -->
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}

<!-- Password Reset Form -->
<form method="post">
{% csrf_token %}
{{ form.as_p }} <!-- Renders the form fields -->
<button type="submit">{% trans 'Reset Password' %}</button>
</form>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}
54 changes: 54 additions & 0 deletions appointment/templates/appointment/thank_you.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block customCSS %}
<style>
/* Add your custom CSS here */
body {
font-family: 'Nunito', sans-serif;
background-color: #f7f7f7;
margin: 0;
padding: 20px;
}

.container {
max-width: 600px;
background: #ffffff;
margin: 40px auto;
padding: 40px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.title {
color: #333;
text-align: center;
}

.message {
font-size: 18px;
text-align: center;
line-height: 1.6;
margin-top: 20px;
}
</style>
{% endblock %}
{% block title %}{{ page_title }}{% endblock %}
{% block description %}{{ page_description }}{% endblock %}
{% block body %}
<div class="container">
<h1 class="title">{{ page_title }}</h1>
<p class="message">
{{ page_message }}
</p>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible {% if message.tags %}alert-{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}
{% block customJS %}
<script src="{% static 'js/js-utils.js' %}"></script>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate 'Appointment Request Notification' %}</title>
<style>
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
padding: 20px;
margin: 0;
}
.email-container {
background-color: #ffffff;
padding: 25px;
margin: 0 auto;
max-width: 650px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
h1 {
color: #333;
font-size: 24px;
}
p {
font-size: 16px;
line-height: 1.6;
}
.appointment-details {
background-color: #f9f9f9;
padding: 15px;
margin-top: 20px;
border-left: 5px solid #007bff;
}
.footer {
margin-top: 30px;
font-size: 14px;
text-align: left;
color: #999;
}
</style>
</head>
<body>
<div class="email-container">
<h1>{% translate 'New Appointment Request' %}</h1>
<p>{% translate 'Dear Admin,' %}</p>
<p>{% translate 'You have received a new appointment request. Here are the details:' %}</p>

<div class="appointment-details">
<p><strong>{% translate 'Client Name' %}:</strong> {{ client_name }}</p>
<p><strong>{% translate 'Service Requested' %}:</strong> {{ appointment.get_service_name }}</p>
<p><strong>{% translate 'Appointment Date' %}:</strong> {{ appointment.appointment_request.date }}</p>
<p><strong>{% translate 'Time' %}:</strong> {{ appointment.appointment_request.start_time }} - {{ appointment.appointment_request.end_time }}</p>
<p><strong>{% translate 'Contact Details' %}:</strong> {{ appointment.phone }} | {{ client_email }}</p>
<p><strong>{% translate 'Additional Info' %}:</strong> {{ appointment.additional_info|default:"N/A" }}</p>
</div>

<p>{% translate 'Please review the appointment request and take the necessary action.' %}</p>

<div class="footer">
<p>{% translate 'This is an automated message. Please do not reply directly to this email.' %}</p>
</div>
</div>
</body>
</html>
Loading