HH/apps/appreciation/ui_views.py
ismail badb6a9ebf
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m41s
feat: QI Projects — team-member task management + My Tasks + notifications
Fixed 3 gaps in the QI Projects module:

Gap 1 (critical): team members couldn't manage their own tasks — the toggle
checkbox/edit/delete were gated behind admin-only can_edit. Now:
- _can_manage_task() helper: admins OR the task assignee OR project team members
- task_toggle_status + htmx_task_toggle_status use the new helper
- task_row.html shows the toggle for assignees (task.assigned_to.user_id check)

Gap 2: no "My QI Tasks" view — added /projects/my-tasks/ showing tasks assigned
to the current user across all projects, with toggle links + stats. Sidebar link.

Gap 3: no notification on task assignment — added apps/projects/signals.py
(post_save on QIProjectTask → create_in_app_notification). apps.py ready() wired.

Tested (headed, 11 PASS / 1 FAIL):
- Cross-department team members (Contact Center + different dept) can VIEW the
  project AND toggle their assigned tasks (both PASS)
- My Tasks view loads for team members
- Excel export returns 500 (real bug, reported)
- Project close via edit form needs correct hospital UUID (test harness issue)

Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.

Harness: seed_e2e_project + get_e2e_project_state CLI + qi-projects-workflow.spec.ts
2026-06-16 23:55:58 +03:00

1089 lines
38 KiB
Python

"""
Appreciation UI views - Server-rendered templates for appreciation management
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods
from apps.accounts.models import User
from apps.accounts.services import StaffActivityService
from apps.core.services import AuditService
from apps.organizations.models import Department, Hospital, Staff
from .models import (
Appreciation,
AppreciationBadge,
AppreciationCategory,
AppreciationStatus,
AppreciationVisibility,
AppreciationStats,
UserBadge,
)
# ============================================================================
# APPRECIATION LIST & DETAIL VIEWS
# ============================================================================
@login_required
def appreciation_list(request):
queryset = Appreciation.objects.select_related(
"hospital", "department", "category"
).order_by("-created_at")
user = request.user
if user.is_px_admin():
pass
elif user.hospital:
queryset = queryset.filter(hospital=user.hospital)
else:
queryset = queryset.none()
status_filter_val = request.GET.get("status", "")
if status_filter_val:
queryset = queryset.filter(status=status_filter_val)
search_query = request.GET.get("search", "").strip()
if search_query:
queryset = queryset.filter(
Q(message_en__icontains=search_query)
| Q(message_ar__icontains=search_query)
| Q(metadata__submitted_by_name__icontains=search_query)
)
base_qs = Appreciation.objects.all()
if not user.is_px_admin() and user.hospital:
base_qs = base_qs.filter(hospital=user.hospital)
elif not user.is_px_admin():
base_qs = base_qs.none()
stats = {
"total": base_qs.count(),
"draft": base_qs.filter(status=AppreciationStatus.DRAFT).count(),
"activated": base_qs.filter(
status__in=[AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED]
).count(),
"sent": base_qs.filter(status__in=[AppreciationStatus.SENT, AppreciationStatus.ACKNOWLEDGED]).count(),
}
paginator = Paginator(queryset, 25)
page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"appreciations": page_obj.object_list,
"stats": stats,
"status_filter": status_filter_val,
"search_query": search_query,
}
return render(request, "appreciation/appreciation_list.html", context)
@login_required
def appreciation_detail(request, pk):
appreciation = get_object_or_404(
Appreciation.objects.select_related("hospital", "department", "category"),
pk=pk,
)
user = request.user
if not (
user.is_px_admin() or user.is_hospital_admin()
or user.is_px_management() or user.is_px_employee()
):
if not (user.hospital and appreciation.hospital_id == user.hospital_id):
messages.error(request, _("You don't have permission to view this appreciation."))
return redirect("appreciation:appreciation_list")
metadata = appreciation.metadata or {}
staff_queryset = Staff.objects.filter(status="active").select_related("department").order_by("first_name")
if user.hospital and not user.is_px_admin():
staff_queryset = staff_queryset.filter(hospital=user.hospital)
departments = Department.objects.filter(status="active").order_by("name")
if user.hospital and not user.is_px_admin():
departments = departments.filter(hospital=user.hospital)
categories = AppreciationCategory.objects.filter(is_active=True).order_by("order", "name_en")
from django.contrib.contenttypes.models import ContentType
appreciation_ct = ContentType.objects.get_for_model(appreciation)
generic_notes = appreciation.notes.select_related("created_by").all()
context = {
"appreciation": appreciation,
"metadata": metadata,
"staff_list": staff_queryset,
"departments": departments,
"categories": categories,
"can_activate": appreciation.status == AppreciationStatus.DRAFT,
"can_send": appreciation.status in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED),
"is_recipient": False,
"content_type_id": appreciation_ct.pk,
"object_id": appreciation.pk,
"notes": generic_notes,
"notes_count": generic_notes.count(),
}
return render(request, "appreciation/appreciation_detail.html", context)
@login_required
@require_http_methods(["POST"])
def appreciation_activate(request, pk):
appreciation = get_object_or_404(Appreciation, pk=pk)
if appreciation.status != AppreciationStatus.DRAFT:
messages.error(request, _("This appreciation has already been processed."))
return redirect("appreciation:appreciation_detail", pk=appreciation.pk)
user = request.user
if not (
user.is_px_admin() or user.is_hospital_admin()
or user.is_px_management() or user.is_px_employee()
):
if not (user.hospital and appreciation.hospital_id == user.hospital_id):
messages.error(request, _("Permission denied."))
return redirect("appreciation:appreciation_list")
staff_id = request.POST.get("staff")
department_id = request.POST.get("department")
category_id = request.POST.get("category")
if not staff_id:
messages.error(request, _("Please select a staff member before activating."))
return redirect("appreciation:appreciation_detail", pk=appreciation.pk)
staff = Staff.objects.filter(id=staff_id).select_related("department", "user").first()
if staff:
if staff.user:
user_ct = ContentType.objects.get_for_model(staff.user)
appreciation.recipient_content_type = user_ct
appreciation.recipient_object_id = staff.user.id
else:
staff_ct = ContentType.objects.get_for_model(staff)
appreciation.recipient_content_type = staff_ct
appreciation.recipient_object_id = staff.id
if not department_id and staff.department:
department_id = str(staff.department.id)
if department_id:
appreciation.department_id = department_id
if category_id:
appreciation.category_id = category_id
appreciation.status = AppreciationStatus.ACTIVATED
appreciation.activated_at = timezone.now()
appreciation.activated_by = user
appreciation.save()
StaffActivityService.log_from_request(
request,
activity_type="create",
description=f"Activated appreciation {appreciation.pk}",
content_object=appreciation,
module="appreciation",
)
AuditService.log_event(
event_type="appreciation_activated",
description=f"Appreciation {appreciation.pk} activated by {user.get_full_name()}",
content_object=appreciation,
)
try:
from apps.core.ai_service import AIService
analysis_result = AIService.chat_completion(
prompt=f'Analyze this patient appreciation message and provide:\n'
f'1. A summary of what the patient appreciated (in English and Arabic)\n'
f'2. Key themes mentioned\n'
f'3. Suggested category if not already set\n'
f'4. Tone analysis\n\n'
f'Message: "{appreciation.message_en}"\n\n'
f'Respond in JSON format with keys:\n'
f'- summary_en: English summary\n'
f'- summary_ar: Arabic summary\n'
f'- themes: List of key themes\n'
f'- tone: "warm", "formal", or "casual"\n'
f'- suggested_response_en: Suggested response in English\n'
f'- suggested_response_ar: Suggested response in Arabic',
system_prompt="You are analyzing patient appreciation messages. Always respond with valid JSON.",
response_format="json_object",
)
import json
analysis_data = json.loads(analysis_result)
appreciation.mark_ai_analyzed(analysis_data)
except Exception as e:
import logging
logging.getLogger(__name__).error(f"AI analysis failed for appreciation {appreciation.pk}: {str(e)}")
messages.success(request, _("Appreciation activated successfully."))
return redirect("appreciation:appreciation_detail", pk=appreciation.pk)
@login_required
@require_http_methods(["POST"])
def appreciation_send(request, pk):
appreciation = get_object_or_404(Appreciation, pk=pk)
if appreciation.status not in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED):
messages.error(request, _("This appreciation must be activated before sending."))
return redirect("appreciation:appreciation_detail", pk=pk)
user = request.user
if not (
user.is_px_admin() or user.is_hospital_admin()
or user.is_px_management() or user.is_px_employee()
):
if not (user.hospital and appreciation.hospital_id == user.hospital_id):
messages.error(request, _("Permission denied."))
return redirect("appreciation:appreciation_list")
send_to_manager = request.POST.get("send_to_manager") == "on"
send_to_department = request.POST.get("send_to_department") == "on"
custom_message = request.POST.get("custom_message", "").strip()
cc_emails = request.POST.get("cc_emails", "").strip()
appreciation.send_to_manager = send_to_manager
appreciation.send_to_department = send_to_department
appreciation.custom_message = custom_message
cc_list = [e.strip() for e in cc_emails.split(",") if e.strip()] if cc_emails else []
appreciation.cc_list = cc_list
appreciation.send()
StaffActivityService.log_from_request(
request,
activity_type="send",
description=f"Sent appreciation {appreciation.pk} to {appreciation.get_recipient_name()}",
content_object=appreciation,
module="appreciation",
)
AuditService.log_event(
event_type="appreciation_sent",
description=f"Appreciation {appreciation.pk} sent to {appreciation.get_recipient_name()}",
user=user,
content_object=appreciation,
metadata={
"send_to_manager": send_to_manager,
"send_to_department": send_to_department,
"cc_count": len(cc_list),
},
)
messages.success(request, _("Appreciation sent successfully."))
return redirect("appreciation:appreciation_detail", pk=pk)
# ============================================================================
# ACKNOWLEDGE
# ============================================================================
@login_required
@require_http_methods(["POST"])
def appreciation_acknowledge(request, pk):
"""Acknowledge appreciation"""
appreciation = get_object_or_404(Appreciation, pk=pk)
# Check if user is recipient
user_content_type = ContentType.objects.get_for_model(request.user)
if not (
appreciation.recipient_content_type == user_content_type and
appreciation.recipient_object_id == request.user.id
):
messages.error(request, "You can only acknowledge appreciations sent to you.")
return redirect('appreciation:appreciation_detail', pk=pk)
if appreciation.status != AppreciationStatus.SENT:
messages.error(request, "This appreciation cannot be acknowledged in its current status.")
return redirect('appreciation:appreciation_detail', pk=pk)
# Acknowledge
appreciation.acknowledge()
messages.success(request, "Appreciation acknowledged successfully.")
return redirect('appreciation:appreciation_detail', pk=pk)
# ============================================================================
# LEADERBOARD VIEWS
# ============================================================================
@login_required
def leaderboard_view(request):
"""
Appreciation leaderboard view.
Features:
- Monthly rankings
- Hospital and department filters
- Top recipients with badges
"""
user = request.user
# Get date range
now = timezone.now()
year = int(request.GET.get('year', now.year))
month = int(request.GET.get('month', now.month))
# Build base query
queryset = AppreciationStats.objects.filter(year=year, month=month)
# Apply RBAC
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Apply filters
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
department_filter = request.GET.get('department')
if department_filter:
queryset = queryset.filter(department_id=department_filter)
# Order by received count
queryset = queryset.order_by('-received_count')
# Pagination
page_size = int(request.GET.get('page_size', 50))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital)
# Get months for filter
months = [(i, timezone.datetime(year=year, month=i, day=1).strftime('%B')) for i in range(1, 13)]
years = range(now.year - 1, now.year + 2)
context = {
'page_obj': page_obj,
'leaderboard': page_obj.object_list,
'departments': departments,
'months': months,
'years': years,
'selected_year': year,
'selected_month': month,
'filters': request.GET,
}
return render(request, 'appreciation/leaderboard.html', context)
@login_required
def my_badges_view(request):
"""
User's badges view.
Features:
- All earned badges
- Badge details and criteria
- Progress toward next badges
"""
user = request.user
user_content_type = ContentType.objects.get_for_model(user)
# Get user's badges
queryset = UserBadge.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).select_related('badge').order_by('-earned_at')
# Pagination
page_size = int(request.GET.get('page_size', 20))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get available badges for progress tracking
available_badges = AppreciationBadge.objects.filter(is_active=True)
if not request.user.is_px_admin() and request.user.hospital:
available_badges = available_badges.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True))
# Calculate progress for each badge
badge_progress = []
total_received = Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count()
for badge in available_badges:
earned = queryset.filter(badge=badge).exists()
progress = 0
if badge.criteria_type in ('received_count', 'received_month'):
progress = min(100, int((total_received / badge.criteria_value) * 100)) if badge.criteria_value else 0
elif badge.criteria_type == 'diverse_senders':
unique_senders = Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).values('sender').distinct().count()
progress = min(100, int((unique_senders / badge.criteria_value) * 100)) if badge.criteria_value else 0
elif badge.criteria_type == 'streak_weeks':
from datetime import timedelta
now = timezone.now()
streak = 0
for i in range(badge.criteria_value):
week_start = now - timedelta(weeks=i+1)
week_end = now - timedelta(weeks=i)
if Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id,
sent_at__gte=week_start,
sent_at__lt=week_end,
).exists():
streak += 1
else:
break
progress = min(100, int((streak / badge.criteria_value) * 100)) if badge.criteria_value else 0
badge_progress.append({
'badge': badge,
'earned': earned,
'progress': progress,
})
context = {
'page_obj': page_obj,
'badges': page_obj.object_list,
'total_received': total_received,
'badge_progress': badge_progress,
}
return render(request, 'appreciation/my_badges.html', context)
# ============================================================================
# ADMIN: CATEGORY MANAGEMENT
# ============================================================================
@login_required
def category_list(request):
"""List and manage appreciation categories"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to manage categories.")
return redirect('appreciation:appreciation_list')
# Base queryset
queryset = AppreciationCategory.objects.annotate(
appreciation_count=Count('appreciations'),
)
# Apply RBAC
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True))
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name_en__icontains=search_query) |
Q(name_ar__icontains=search_query) |
Q(code__icontains=search_query)
)
# Ordering
queryset = queryset.order_by('order', 'code')
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'categories': page_obj.object_list,
}
return render(request, 'appreciation/category_list.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def category_create(request):
"""Create appreciation category"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to create categories.")
return redirect('appreciation:appreciation_list')
if request.method == 'POST':
try:
code = request.POST.get('code')
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar', '')
description_en = request.POST.get('description_en', '')
description_ar = request.POST.get('description_ar', '')
icon = request.POST.get('icon', 'fa-heart')
color = request.POST.get('color', '#FF5733')
order = request.POST.get('order', 0)
is_active = request.POST.get('is_active') == 'on'
# Get hospital
hospital = None
if user.is_hospital_admin() and user.hospital:
hospital = user.hospital
# Validate
if not all([code, name_en]):
messages.error(request, "Please fill in all required fields.")
return redirect('appreciation:category_create')
# Create category
AppreciationCategory.objects.create(
code=code,
name_en=name_en,
name_ar=name_ar,
description_en=description_en,
description_ar=description_ar,
icon=icon,
color=color,
order=order,
is_active=is_active,
hospital=hospital,
)
messages.success(request, "Category created successfully.")
return redirect('appreciation:category_list')
except Exception as e:
messages.error(request, f"Error creating category: {str(e)}")
return redirect('appreciation:category_create')
context = {}
return render(request, 'appreciation/admin/category_form.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def category_edit(request, pk):
"""Edit appreciation category"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to edit categories.")
return redirect('appreciation:appreciation_list')
category = get_object_or_404(AppreciationCategory, pk=pk)
# Check access
if not user.is_px_admin() and category.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this category.")
return redirect('appreciation:category_list')
if request.method == 'POST':
try:
category.code = request.POST.get('code')
category.name_en = request.POST.get('name_en')
category.name_ar = request.POST.get('name_ar', '')
category.description_en = request.POST.get('description_en', '')
category.description_ar = request.POST.get('description_ar', '')
category.icon = request.POST.get('icon', 'fa-heart')
category.color = request.POST.get('color', '#FF5733')
category.order = request.POST.get('order', 0)
category.is_active = request.POST.get('is_active') == 'on'
category.save()
messages.success(request, "Category updated successfully.")
return redirect('appreciation:category_list')
except Exception as e:
messages.error(request, f"Error updating category: {str(e)}")
return redirect('appreciation:category_edit', pk=pk)
context = {
'category': category,
}
return render(request, 'appreciation/admin/category_form.html', context)
@login_required
@require_http_methods(["POST"])
def category_delete(request, pk):
"""Delete appreciation category"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to delete categories.")
return redirect('appreciation:appreciation_list')
category = get_object_or_404(AppreciationCategory, pk=pk)
# Check if category is in use
if Appreciation.objects.filter(category=category).exists():
messages.error(request, "Cannot delete category that is in use.")
return redirect('appreciation:category_list')
# Log audit
AuditService.log_event(
event_type='category_deleted',
description=f"Appreciation category deleted: {category.name_en}",
user=request.user,
metadata={'category_code': category.code}
)
category.delete()
messages.success(request, "Category deleted successfully.")
return redirect('appreciation:category_list')
# ============================================================================
# ADMIN: BADGE MANAGEMENT
# ============================================================================
@login_required
def badge_list(request):
"""List and manage appreciation badges"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to manage badges.")
return redirect('appreciation:appreciation_list')
# Base queryset
queryset = AppreciationBadge.objects.annotate(
earned_count=Count('earned_by'),
)
# Apply RBAC
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(Q(hospital_id=user.hospital.id) | Q(hospital__isnull=True))
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name_en__icontains=search_query) |
Q(name_ar__icontains=search_query) |
Q(code__icontains=search_query)
)
# Ordering
queryset = queryset.order_by('order', 'code')
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'badges': page_obj.object_list,
}
return render(request, 'appreciation/badge_list.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def badge_create(request):
"""Create appreciation badge"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to create badges.")
return redirect('appreciation:appreciation_list')
if request.method == 'POST':
try:
code = request.POST.get('code')
name_en = request.POST.get('name_en')
name_ar = request.POST.get('name_ar', '')
description_en = request.POST.get('description_en', '')
description_ar = request.POST.get('description_ar', '')
icon = request.POST.get('icon', 'fa-award')
color = request.POST.get('color', '#FFD700')
criteria_type = request.POST.get('criteria_type', 'received_count')
criteria_value = request.POST.get('criteria_value', 5)
order = request.POST.get('order', 0)
is_active = request.POST.get('is_active') == 'on'
# Get hospital
hospital = None
if user.is_hospital_admin() and user.hospital:
hospital = user.hospital
# Validate
if not all([code, name_en, criteria_value]):
messages.error(request, "Please fill in all required fields.")
return redirect('appreciation:badge_create')
# Create badge
AppreciationBadge.objects.create(
code=code,
name_en=name_en,
name_ar=name_ar,
description_en=description_en,
description_ar=description_ar,
icon=icon,
color=color,
criteria_type=criteria_type,
criteria_value=int(criteria_value),
order=order,
is_active=is_active,
hospital=hospital,
)
messages.success(request, "Badge created successfully.")
return redirect('appreciation:badge_list')
except Exception as e:
messages.error(request, f"Error creating badge: {str(e)}")
return redirect('appreciation:badge_create')
context = {}
return render(request, 'appreciation/admin/badge_form.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def badge_edit(request, pk):
"""Edit appreciation badge"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to edit badges.")
return redirect('appreciation:appreciation_list')
badge = get_object_or_404(AppreciationBadge, pk=pk)
# Check access
if not user.is_px_admin() and badge.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this badge.")
return redirect('appreciation:badge_list')
if request.method == 'POST':
try:
badge.code = request.POST.get('code')
badge.name_en = request.POST.get('name_en')
badge.name_ar = request.POST.get('name_ar', '')
badge.description_en = request.POST.get('description_en', '')
badge.description_ar = request.POST.get('description_ar', '')
badge.icon = request.POST.get('icon', 'fa-award')
badge.color = request.POST.get('color', '#FFD700')
badge.criteria_type = request.POST.get('criteria_type', 'received_count')
badge.criteria_value = request.POST.get('criteria_value', 5)
badge.order = request.POST.get('order', 0)
badge.is_active = request.POST.get('is_active') == 'on'
badge.save()
messages.success(request, "Badge updated successfully.")
return redirect('appreciation:badge_list')
except Exception as e:
messages.error(request, f"Error updating badge: {str(e)}")
return redirect('appreciation:badge_edit', pk=pk)
context = {
'badge': badge,
}
return render(request, 'appreciation/admin/badge_form.html', context)
@login_required
@require_http_methods(["POST"])
def badge_delete(request, pk):
"""Delete appreciation badge"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to delete badges.")
return redirect('appreciation:appreciation_list')
badge = get_object_or_404(AppreciationBadge, pk=pk)
# Check if badge is in use
if UserBadge.objects.filter(badge=badge).exists():
messages.error(request, "Cannot delete badge that has been earned.")
return redirect('appreciation:badge_list')
# Log audit
AuditService.log_event(
event_type='badge_deleted',
description=f"Appreciation badge deleted: {badge.name_en}",
user=request.user,
metadata={'badge_code': badge.code}
)
badge.delete()
messages.success(request, "Badge deleted successfully.")
return redirect('appreciation:badge_list')
@login_required
@require_http_methods(["POST"])
def appreciation_restore(request, pk):
"""Restore deleted appreciation"""
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to restore appreciation.")
return redirect('config:deleted_items')
appreciation = get_object_or_404(Appreciation.all_objects, pk=pk, is_deleted=True)
appreciation.restore()
messages.success(request, "Appreciation restored successfully.")
return redirect('config:deleted_items')
# ============================================================================
# AJAX/API HELPERS
# ============================================================================
@login_required
def get_users_by_hospital(request):
"""Get users for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'users': []})
users = User.objects.filter(
hospital_id=hospital_id,
is_active=True
).values('id', 'first_name', 'last_name')
results = [
{
'id': str(u['id']),
'name': f"{u['first_name']} {u['last_name']}",
}
for u in users
]
return JsonResponse({'users': results})
@login_required
def get_staff_by_hospital(request):
"""Get staff for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'staff': []})
staff = Staff.objects.filter(
hospital_id=hospital_id,
status='active'
).values('id', 'user__first_name', 'user__last_name')
results = [
{
'id': str(s['id']),
'name': f"{s['user__first_name']} {s['user__last_name']}",
}
for s in staff
]
return JsonResponse({'staff': results})
@login_required
def get_physicians_by_hospital(request):
"""Get physicians for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'physicians': []})
physicians = Staff.objects.filter(
hospital_id=hospital_id,
status='active',
staff_type='physician'
).values('id', 'user__first_name', 'user__last_name')
results = [
{
'id': str(p['id']),
'name': f"{p['user__first_name']} {p['user__last_name']}",
}
for p in physicians
]
return JsonResponse({'physicians': results})
@login_required
def get_departments_by_hospital(request):
"""Get departments for a hospital (AJAX)"""
hospital_id = request.GET.get('hospital_id')
if not hospital_id:
return JsonResponse({'departments': []})
departments = Department.objects.filter(
hospital_id=hospital_id,
status='active'
).values('id', 'name', 'name_ar')
results = [
{
'id': str(d['id']),
'name': d['name'],
}
for d in departments
]
return JsonResponse({'departments': results})
@login_required
def appreciation_summary_ajax(request):
"""Get appreciation summary for current user (AJAX)"""
user = request.user
user_content_type = ContentType.objects.get_for_model(user)
now = timezone.now()
current_year = now.year
current_month = now.month
summary = {
'total_received': Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count(),
'total_sent': Appreciation.objects.filter(sender=user).count(),
'this_month_received': Appreciation.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id,
sent_at__year=current_year,
sent_at__month=current_month
).count(),
'this_month_sent': Appreciation.objects.filter(
sender=user,
sent_at__year=current_year,
sent_at__month=current_month
).count(),
'badges_earned': UserBadge.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id
).count(),
}
# Get hospital rank
if user.hospital:
stats = AppreciationStats.objects.filter(
recipient_content_type=user_content_type,
recipient_object_id=user.id,
year=current_year,
month=current_month
).first()
summary['hospital_rank'] = stats.hospital_rank if stats else None
return JsonResponse(summary)
@csrf_exempt
@require_http_methods(["POST"])
def public_appreciation_submit(request):
import json
import logging
logger = logging.getLogger(__name__)
client_ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() or request.META.get('REMOTE_ADDR', '')
cache_key = f"appreciation_rate:{client_ip}"
from django.core.cache import cache
if cache.get(cache_key, 0) >= 5:
return JsonResponse({"success": False, "message": "Too many requests. Please try again later."}, status=429)
cache.set(cache_key, cache.get(cache_key, 0) + 1, 300)
try:
try:
data = json.loads(request.body) if request.content_type == 'application/json' else request.POST
except json.JSONDecodeError:
data = request.POST
contact_name = data.get("contact_name", "").strip()
contact_phone = data.get("contact_phone", "").strip()
message = data.get("message", "").strip()
hospital_id = data.get("hospital", "")
staff_name = data.get("staff_name", "").strip()
department_id = data.get("department", "").strip()
section_id = data.get("section", "").strip()
if not contact_name or not contact_phone or not message:
return JsonResponse({"success": False, "message": "Name, phone, and message are required."}, status=400)
if not hospital_id:
return JsonResponse({"success": False, "message": "Please select a hospital."}, status=400)
try:
hospital = Hospital.objects.get(id=hospital_id)
except Hospital.DoesNotExist:
return JsonResponse({"success": False, "message": "Invalid hospital."}, status=400)
from apps.organizations.models import Department, Section, OrgSubSection
department = Department.objects.filter(id=department_id).first() if department_id else None
section = Section.objects.filter(id=section_id).first() if section_id else None
appreciation = Appreciation(
hospital=hospital,
department=department,
section=section,
category=None,
message_en=message,
message_ar="",
is_anonymous=False,
status=AppreciationStatus.DRAFT,
visibility=AppreciationVisibility.PUBLIC,
metadata={
"source": "public_form",
"submitted_by_name": contact_name,
"submitted_by_phone": contact_phone,
"staff_name_mentioned": staff_name,
},
)
appreciation.save()
try:
from apps.complaints.tasks import notify_staff_new_item
notify_staff_new_item.delay("appreciation", str(appreciation.id))
except Exception:
pass
AuditService.log_event(
event_type="public_appreciation_submitted",
description=f"Public appreciation submitted by {contact_name}",
content_object=appreciation,
metadata={"hospital": str(hospital.id), "staff_mentioned": staff_name},
)
return JsonResponse({"success": True, "reference": appreciation.reference_number})
except Exception as e:
logger.exception("ERROR in public_appreciation_submit")
return JsonResponse({"success": False, "message": str(e)}, status=500)