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
2 changes: 1 addition & 1 deletion appointment/static/js/appointments.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function getAvailableSlots(selectedDate, staffId = null) {
console.log('No staff ID provided, displaying error message.');
const errorMessage = $('<p class="djangoAppt_no-availability-text">'+ noStaffMemberSelectedTxt + '</p>');
errorMessageContainer.append(errorMessage);
// Optionally disable the submit button here
// Optionally disable the "submit" button here
$('.btn-submit-appointment').attr('disabled', 'disabled');
return; // Exit the function early
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ <h2>{% trans 'Staff Personal Information' %}</h2>
{{ form.email }}
</div>

<button type="submit" class="btn btn-primary">{% trans 'Update' %}</button>
<button type="submit" class="btn btn-primary">{{ btn_text }}</button>
</form>
<div class="messages" style="margin: 20px 0">
{% if messages %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css"
integrity="sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg=="
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css"
integrity="sha512-rt/SrQ4UNIaGfDyEXZtNcyWvQeOq0QLygHluFQcSjaGB04IxWhal71tKuzP6K8eYXYB6vJV4pHkXcmFGGQ1/0w=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.39.0/css/tempusdominus-bootstrap-4.min.css"
Expand Down Expand Up @@ -96,8 +96,8 @@ <h2>{% trans "Manage Working Hours" %}</h2>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"
integrity="sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js"
integrity="sha512-WW8/jxkELe2CAiE4LvQfwm1rajOS8PHasCCx+knHG0gBHt8EXxS6T6tJRTGuDQVnluuAvMxWF4j8SNFDKceLFg=="
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/js/bootstrap.min.js"
integrity="sha512-7rusk8kGPFynZWu26OKbTeI+QPoYchtxsmPeBqkHIEXJxeun4yJ4ISYe7C6sz9wdxeE1Gk3VxsIWgCZTc+vX3g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.js"
integrity="sha512-3CuraBvy05nIgcoXjVN33mACRyI89ydVHg7y/HMN9wcTVbHeur0SeBzweSd/rxySapO7Tmfu68+JlKkLTnDFNg=="
Expand Down
12 changes: 6 additions & 6 deletions appointment/templates/appointment/default_thank_you.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ <h1 class="thank-you-title">{% trans "See you soon" %} !</h1>
<li>{% trans 'Appointment Time' %}: {{ appointment.get_start_time }}</li>
<li>{% trans 'Duration' %}: {{ appointment.get_service_duration }}</li>
</ul>
{% 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>
{% 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 %}
Expand Down
103 changes: 51 additions & 52 deletions appointment/templates/error_pages/403_forbidden.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
{% extends BASE_TEMPLATE %}
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% load static %}
{% block site_description %}Designer: Adams Pierre David, Category: Resources forbidden, code 403{% endblock %}
{% block title %}Resources forbidden | 403{% endblock %}
{% block customMetaTag %}
<meta name="robots" content="noindex,nofollow">
{% endblock %}
{% block customCSS %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description"
content="Author: Adams Pierre David, Designer: Adams Pierre David, Category: Technology/IT/Blog, Resources forbidden, code 403">
<meta name="google-site-verification" content=""/>
<meta name="robots" content="noindex,nofollow">
<title>Resources forbidden | 403</title>
<link href="/static/root/img/favicon/favicon.ico"
rel="shortcut icon"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
Expand Down Expand Up @@ -83,47 +79,50 @@
}

</style>
</head>
<body>
<br><br><br><br>
<div class="logo">
{% trans "Reality" %}
</div>
<br>
<div class="error">
{% trans "Error 403" %}!
</div>
<br>
<div class="error2">
{% if message %}
{{ message }}
{% else %}
{% trans "You don't have permission to view this resource, and you know it" %}!
{% endif %}
</div>
<br>
<div class="options" align="center">
{% if back_url %}
<button type="button" class="btn btn-primary" id="back" onclick="Back()">
<i class="fa fa-undo"></i> {% trans "Go to profile" %}
</button>
{% else %}
<button type="button" class="btn btn-primary" id="back" onclick="Back()">
<i class="fa fa-undo"></i> {% trans "Go back" %}
</button>
{% endif %}
</div>
<script>
const back_url = "{{ back_url }}";
{% endblock %}
{% block body %}

function Back() {
if (back_url) {
window.location.href = back_url;
} else {
window.history.back();
<body>
<br><br><br><br>
<div class="logo">
{% trans "Reality" %}
</div>
<br>
<div class="error">
{% trans "Error 403" %}!
</div>
<br>
<div class="error2">
{% if message %}
{{ message }}
{% else %}
{% trans "You don't have permission to view this resource, and you know it" %}!
{% endif %}
</div>
<br>
<div class="options" align="center">
{% if back_url %}
<button type="button" class="btn btn-primary" id="back" onclick="Back()">
<i class="fa fa-undo"></i> {% trans "Go to profile" %}
</button>
{% else %}
<button type="button" class="btn btn-primary" id="back" onclick="Back()">
<i class="fa fa-undo"></i> {% trans "Go back" %}
</button>
{% endif %}
</div>
<script>
const back_url = "{{ back_url }}";

function Back() {
if (back_url) {
window.location.href = back_url;
} else {
window.history.back();
}
}
}
</script>
</script>


</body>
</body>
{% endblock %}
2 changes: 1 addition & 1 deletion appointment/templates/error_pages/404_not_found.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends ADMIN_BASE_TEMPLATE %}
{% extends BASE_TEMPLATE %}
{% load i18n %}
{% load static %}
{% block site_description %}Designer: Adams Pierre David, Category: Resources not found, code 404{% endblock %}
Expand Down
35 changes: 35 additions & 0 deletions appointment/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,41 @@ def test_delete_appointment_ajax(self):
appointment_exists = Appointment.objects.filter(pk=self.appointment.id).exists()
self.assertFalse(appointment_exists, "Appointment should be deleted but still exists.")

def test_delete_appointment_without_permission(self):
"""Test that deleting an appointment without permission fails."""
self.need_staff_login() # Login as a regular staff user

# Try to delete an appointment belonging to a different staff member
different_appointment = self.create_appointment_for_user2()
url = reverse('appointment:delete_appointment', args=[different_appointment.id])

response = self.client.post(url)

# Check that the user is redirected due to lack of permissions
self.assertEqual(response.status_code, 403)

# Verify that the appointment still exists in the database
self.assertTrue(Appointment.objects.filter(id=different_appointment.id).exists())

def test_delete_appointment_ajax_without_permission(self):
"""Test that deleting an appointment via AJAX without permission fails."""
self.need_staff_login() # Login as a regular staff user

# Try to delete an appointment belonging to a different staff member
different_appointment = self.create_appointment_for_user2()
url = reverse('appointment:delete_appointment_ajax')

response = self.client.post(url, {'appointment_id': different_appointment.id}, content_type='application/json')

# Check that the response indicates failure due to lack of permissions
self.assertEqual(response.status_code, 403)
response_data = response.json()
self.assertEqual(response_data['message'], _("You can only delete your own appointments."))
self.assertFalse(response_data['success'])

# Verify that the appointment still exists in the database
self.assertTrue(Appointment.objects.filter(id=different_appointment.id).exists())

def test_remove_staff_member(self):
self.need_superuser_login()
self.clean_staff_member_objects()
Expand Down
3 changes: 2 additions & 1 deletion appointment/utils/json_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def handle_unauthorized_response(request, message, response_type):
# If not 'json', handle as HTML response by default.
context = {
'message': message,
'back_url': reverse('appointment:user_profile')
'back_url': reverse('appointment:user_profile'),
'BASE_TEMPLATE': APPOINTMENT_BASE_TEMPLATE,
}
# set return code to 403
return render(request, 'error_pages/403_forbidden.html', context=context, status=403)
8 changes: 8 additions & 0 deletions appointment/utils/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ def check_permissions(staff_user_id, user):
if (staff_user_id and int(staff_user_id) == user.pk) or user.is_superuser:
return True
return False


def has_permission_to_delete_appointment(user, appointment):
"""
Check if the user has permission to delete the given appointment.
Returns True if the user has permission, False otherwise.
"""
return check_extensive_permissions(appointment.get_staff_member().user_id, user, appointment)
15 changes: 12 additions & 3 deletions appointment/views_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
from appointment.utils.json_context import (
convert_appointment_to_json, get_generic_context, get_generic_context_with_extra, handle_unauthorized_response,
json_response)
from appointment.utils.permissions import check_extensive_permissions, check_permissions
from appointment.utils.permissions import check_extensive_permissions, check_permissions, \
has_permission_to_delete_appointment


###############################################################
Expand Down Expand Up @@ -346,7 +347,7 @@ def update_personal_info(request, staff_user_id=None):
'email': user.email,
}, user=user)

context = get_generic_context_with_extra(request=request, extra={'form': form})
context = get_generic_context_with_extra(request=request, extra={'form': form, 'btn_text': _("Update")})
return render(request, 'administration/manage_staff_personal_info.html', context)


Expand Down Expand Up @@ -386,7 +387,7 @@ def create_new_staff_member(request):
return redirect('appointment:add_staff_member_personal_info')

form = PersonalInformationForm()
context = get_generic_context_with_extra(request=request, extra={'form': form})
context = get_generic_context_with_extra(request=request, extra={'form': form, 'btn_text': _("Create")})
return render(request, 'administration/manage_staff_personal_info.html', context=context)


Expand Down Expand Up @@ -454,6 +455,8 @@ def delete_service(request, service_id):

###############################################################
# Remove staff member
@require_user_authenticated
@require_superuser
def remove_staff_member(request, staff_user_id):
staff_member = get_object_or_404(StaffMember, user_id=staff_user_id)
staff_member.delete()
Expand Down Expand Up @@ -491,6 +494,9 @@ def get_service_list(request, response_type='html'):
@require_staff_or_superuser
def delete_appointment(request, appointment_id):
appointment = get_object_or_404(Appointment, pk=appointment_id)
if not has_permission_to_delete_appointment(request.user, appointment):
message = _("You can only delete your own appointments.")
return handle_unauthorized_response(request, message, 'html')
appointment.delete()
messages.success(request, _("Appointment deleted successfully!"))
return redirect('appointment:get_user_appointments')
Expand All @@ -502,6 +508,9 @@ def delete_appointment_ajax(request):
data = json.loads(request.body)
appointment_id = data.get("appointment_id")
appointment = get_object_or_404(Appointment, pk=appointment_id)
if not has_permission_to_delete_appointment(request.user, appointment):
message = _("You can only delete your own appointments.")
return json_response(message, status=403, success=False, error_code=ErrorCode.NOT_AUTHORIZED)
appointment.delete()
return json_response(_("Appointment deleted successfully."))

Expand Down