HH/apps/projects/ui_views.py
2026-03-09 16:10:24 +03:00

733 lines
26 KiB
Python

"""
QI Projects Console UI views
Provides full CRUD functionality for Quality Improvement projects,
task management, and template handling.
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from apps.core.decorators import block_source_user
from apps.organizations.models import Hospital
from apps.px_action_center.models import PXAction
from .forms import ConvertToProjectForm, QIProjectForm, QIProjectTaskForm, QIProjectTemplateForm
from .models import QIProject, QIProjectTask
@block_source_user
@login_required
def project_list(request):
"""QI Projects list view with filtering and pagination"""
# Exclude templates from the list
queryset = QIProject.objects.filter(is_template=False).select_related(
'hospital', 'department', 'project_lead'
).prefetch_related('team_members', 'related_actions')
# Apply RBAC filters
user = request.user
# Get selected hospital for PX Admins (from middleware)
selected_hospital = getattr(request, 'tenant_hospital', None)
if user.is_px_admin():
# PX Admins see all, but filter by selected hospital if set
if selected_hospital:
queryset = queryset.filter(hospital=selected_hospital)
elif user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Apply filters
status_filter = request.GET.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name__icontains=search_query) |
Q(description__icontains=search_query) |
Q(name_ar__icontains=search_query)
)
# Ordering
queryset = queryset.order_by('-created_at')
# 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)
# Get hospitals for filter
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
# Statistics
stats = {
'total': queryset.count(),
'active': queryset.filter(status='active').count(),
'completed': queryset.filter(status='completed').count(),
'pending': queryset.filter(status='pending').count(),
}
context = {
'page_obj': page_obj,
'projects': page_obj.object_list,
'hospitals': hospitals,
'stats': stats,
'filters': request.GET,
}
return render(request, 'projects/project_list.html', context)
@block_source_user
@login_required
def project_detail(request, pk):
"""QI Project detail view with task management"""
project = get_object_or_404(
QIProject.objects.filter(is_template=False).select_related(
'hospital', 'department', 'project_lead'
).prefetch_related(
'team_members', 'related_actions', 'tasks'
),
pk=pk
)
# Check permission
user = request.user
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to view this project."))
return redirect('projects:project_list')
# Get tasks
tasks = project.tasks.all().order_by('order', 'created_at')
# Get related actions
related_actions = project.related_actions.all()
context = {
'project': project,
'tasks': tasks,
'related_actions': related_actions,
'can_edit': user.is_px_admin() or user.is_hospital_admin or user.is_department_manager,
}
return render(request, 'projects/project_detail.html', context)
@block_source_user
@login_required
def project_create(request, template_pk=None):
"""Create a new QI Project"""
user = request.user
# Check permission (PX Admin, Hospital Admin, or Department Manager)
if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager):
messages.error(request, _("You don't have permission to create projects."))
return redirect('projects:project_list')
# Check for template parameter (from URL or GET)
template_id = template_pk or request.GET.get('template')
initial_data = {}
template = None
if template_id:
try:
template = QIProject.objects.get(pk=template_id, is_template=True)
initial_data = {
'name': template.name,
'name_ar': template.name_ar,
'description': template.description,
'department': template.department,
'target_completion_date': template.target_completion_date,
}
if not user.is_px_admin() and user.hospital:
initial_data['hospital'] = user.hospital
except QIProject.DoesNotExist:
pass
# Check for PX Action parameter (convert action to project)
action_id = request.GET.get('action')
if action_id:
try:
action = PXAction.objects.get(pk=action_id)
initial_data['name'] = f"QI Project: {action.title}"
initial_data['description'] = action.description
if not user.is_px_admin() and user.hospital:
initial_data['hospital'] = user.hospital
else:
initial_data['hospital'] = action.hospital
except PXAction.DoesNotExist:
pass
if request.method == 'POST':
form = QIProjectForm(request.POST, user=user)
# Check for template in POST data (hidden field)
template_id_post = request.POST.get('template_id')
if template_id_post:
try:
template = QIProject.objects.get(pk=template_id_post, is_template=True)
except QIProject.DoesNotExist:
template = None
if form.is_valid():
project = form.save(commit=False)
project.created_by = user
project.save()
form.save_m2m() # Save many-to-many relationships
# If created from template, copy tasks
task_count = 0
if template:
for template_task in template.tasks.all():
QIProjectTask.objects.create(
project=project,
title=template_task.title,
description=template_task.description,
order=template_task.order,
status='pending'
)
task_count += 1
# If created from action, link it
if action_id:
try:
action = PXAction.objects.get(pk=action_id)
project.related_actions.add(action)
except PXAction.DoesNotExist:
pass
if template and task_count > 0:
messages.success(
request,
_('QI Project created successfully with %(count)d task(s) from template.') % {'count': task_count}
)
else:
messages.success(request, _("QI Project created successfully."))
return redirect('projects:project_detail', pk=project.pk)
else:
form = QIProjectForm(user=user, initial=initial_data)
context = {
'form': form,
'is_create': True,
'template': template,
}
return render(request, 'projects/project_form.html', context)
@block_source_user
@login_required
def project_edit(request, pk):
"""Edit an existing QI Project"""
user = request.user
project = get_object_or_404(QIProject, pk=pk, is_template=False)
# Check permission
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to edit this project."))
return redirect('projects:project_list')
# Check edit permission (PX Admin, Hospital Admin, or Department Manager)
if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager):
messages.error(request, _("You don't have permission to edit projects."))
return redirect('projects:project_detail', pk=project.pk)
if request.method == 'POST':
form = QIProjectForm(request.POST, instance=project, user=user)
if form.is_valid():
form.save()
messages.success(request, _("QI Project updated successfully."))
return redirect('projects:project_detail', pk=project.pk)
else:
form = QIProjectForm(instance=project, user=user)
context = {
'form': form,
'project': project,
'is_create': False,
}
return render(request, 'projects/project_form.html', context)
@block_source_user
@login_required
def project_delete(request, pk):
"""Delete a QI Project"""
user = request.user
project = get_object_or_404(QIProject, pk=pk, is_template=False)
# Check permission (only PX Admin or Hospital Admin can delete)
if not (user.is_px_admin() or user.is_hospital_admin):
messages.error(request, _("You don't have permission to delete projects."))
return redirect('projects:project_detail', pk=project.pk)
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to delete this project."))
return redirect('projects:project_list')
if request.method == 'POST':
project_name = project.name
project.delete()
messages.success(request, _('Project "%(name)s" deleted successfully.') % {'name': project_name})
return redirect('projects:project_list')
context = {
'project': project,
}
return render(request, 'projects/project_delete_confirm.html', context)
@block_source_user
@login_required
def project_save_as_template(request, pk):
"""Save an existing project and its tasks as a template"""
user = request.user
project = get_object_or_404(QIProject, pk=pk, is_template=False)
# Check permission (only PX Admin or Hospital Admin can create templates)
if not (user.is_px_admin() or user.is_hospital_admin):
messages.error(request, _("You don't have permission to create templates."))
return redirect('projects:project_detail', pk=project.pk)
# Check hospital access
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to create templates from this project."))
return redirect('projects:project_list')
if request.method == 'POST':
template_name = request.POST.get('template_name', '').strip()
template_description = request.POST.get('template_description', '').strip()
make_global = request.POST.get('make_global') == 'on'
if not template_name:
messages.error(request, _('Please provide a template name.'))
return redirect('projects:project_save_as_template', pk=project.pk)
# Create template from project
template = QIProject.objects.create(
name=template_name,
name_ar=template_name, # Can be edited later
description=template_description or project.description,
is_template=True,
# If global, hospital is None; otherwise use project's hospital
hospital=None if make_global else project.hospital,
department=project.department,
status='pending', # Default status for templates
created_by=user
)
# Copy tasks from project to template
for task in project.tasks.all():
QIProjectTask.objects.create(
project=template,
title=task.title,
description=task.description,
order=task.order,
status='pending' # Reset status for template
)
messages.success(
request,
_('Template "%(name)s" created successfully with %(count)d task(s).') % {
'name': template_name,
'count': project.tasks.count()
}
)
return redirect('projects:template_list')
context = {
'project': project,
'suggested_name': f"Template: {project.name}",
}
return render(request, 'projects/project_save_as_template.html', context)
# =============================================================================
# Task Management Views
# =============================================================================
@block_source_user
@login_required
def task_create(request, project_pk):
"""Add a new task to a project"""
user = request.user
project = get_object_or_404(QIProject, pk=project_pk, is_template=False)
# Check permission
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to add tasks to this project."))
return redirect('projects:project_detail', pk=project.pk)
if request.method == 'POST':
form = QIProjectTaskForm(request.POST, project=project)
if form.is_valid():
task = form.save(commit=False)
task.project = project
task.save()
messages.success(request, _("Task added successfully."))
return redirect('projects:project_detail', pk=project.pk)
else:
form = QIProjectTaskForm(project=project)
context = {
'form': form,
'project': project,
'is_create': True,
}
return render(request, 'projects/task_form.html', context)
@block_source_user
@login_required
def task_edit(request, project_pk, task_pk):
"""Edit an existing task"""
user = request.user
project = get_object_or_404(QIProject, pk=project_pk, is_template=False)
task = get_object_or_404(QIProjectTask, pk=task_pk, project=project)
# Check permission
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to edit tasks in this project."))
return redirect('projects:project_detail', pk=project.pk)
if request.method == 'POST':
form = QIProjectTaskForm(request.POST, instance=task, project=project)
if form.is_valid():
task = form.save()
# If status changed to completed, set completed_date
if task.status == 'completed' and not task.completed_date:
from django.utils import timezone
task.completed_date = timezone.now().date()
task.save()
messages.success(request, _("Task updated successfully."))
return redirect('projects:project_detail', pk=project.pk)
else:
form = QIProjectTaskForm(instance=task, project=project)
context = {
'form': form,
'project': project,
'task': task,
'is_create': False,
}
return render(request, 'projects/task_form.html', context)
@block_source_user
@login_required
def task_delete(request, project_pk, task_pk):
"""Delete a task"""
user = request.user
project = get_object_or_404(QIProject, pk=project_pk, is_template=False)
task = get_object_or_404(QIProjectTask, pk=task_pk, project=project)
# Check permission
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to delete tasks in this project."))
return redirect('projects:project_detail', pk=project.pk)
if request.method == 'POST':
task.delete()
messages.success(request, _("Task deleted successfully."))
return redirect('projects:project_detail', pk=project.pk)
context = {
'project': project,
'task': task,
}
return render(request, 'projects/task_delete_confirm.html', context)
@block_source_user
@login_required
def task_toggle_status(request, project_pk, task_pk):
"""Quick toggle task status between pending and completed"""
user = request.user
project = get_object_or_404(QIProject, pk=project_pk, is_template=False)
task = get_object_or_404(QIProjectTask, pk=task_pk, project=project)
# Check permission
if not user.is_px_admin() and user.hospital and project.hospital != user.hospital:
messages.error(request, _("You don't have permission to update tasks in this project."))
return redirect('projects:project_detail', pk=project.pk)
from django.utils import timezone
if task.status == 'completed':
task.status = 'pending'
task.completed_date = None
else:
task.status = 'completed'
task.completed_date = timezone.now().date()
task.save()
messages.success(request, _("Task status updated."))
return redirect('projects:project_detail', pk=project.pk)
# =============================================================================
# Template Management Views
# =============================================================================
@block_source_user
@login_required
def template_list(request):
"""List QI Project templates"""
user = request.user
# Only admins can manage templates
if not (user.is_px_admin() or user.is_hospital_admin):
messages.error(request, _("You don't have permission to view templates."))
return redirect('projects:project_list')
queryset = QIProject.objects.filter(is_template=True).select_related('hospital', 'department')
# Apply RBAC filters
if not user.is_px_admin():
from django.db.models import Q
queryset = queryset.filter(
Q(hospital=user.hospital) | Q(hospital__isnull=True)
)
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(name__icontains=search_query) |
Q(description__icontains=search_query) |
Q(name_ar__icontains=search_query)
)
queryset = queryset.order_by('name')
context = {
'templates': queryset,
'can_create': user.is_px_admin() or user.is_hospital_admin,
'can_edit': user.is_px_admin() or user.is_hospital_admin,
}
return render(request, 'projects/template_list.html', context)
@block_source_user
@login_required
def template_detail(request, pk):
"""View template details with tasks"""
user = request.user
# Only admins can view templates
if not (user.is_px_admin() or user.is_hospital_admin):
messages.error(request, _("You don't have permission to view templates."))
return redirect('projects:project_list')
template = get_object_or_404(
QIProject.objects.filter(is_template=True).select_related('hospital', 'department'),
pk=pk
)
# Check permission for hospital-specific templates
if not user.is_px_admin():
from django.db.models import Q
if template.hospital and template.hospital != user.hospital:
messages.error(request, _("You don't have permission to view this template."))
return redirect('projects:template_list')
# Get tasks
tasks = template.tasks.all().order_by('order', 'created_at')
context = {
'template': template,
'tasks': tasks,
}
return render(request, 'projects/template_detail.html', context)
@block_source_user
@login_required
def template_create(request):
"""Create a new project template"""
user = request.user
# Only admins can create templates
if not (user.is_px_admin() or user.is_hospital_admin):
messages.error(request, _("You don't have permission to create templates."))
return redirect('projects:project_list')
if request.method == 'POST':
form = QIProjectTemplateForm(request.POST, user=user)
if form.is_valid():
template = form.save(commit=False)
template.is_template = True
template.created_by = user
template.save()
messages.success(request, _("Project template created successfully."))
return redirect('projects:template_list')
else:
form = QIProjectTemplateForm(user=user)
context = {
'form': form,
'is_create': True,
}
return render(request, 'projects/template_form.html', context)
@block_source_user
@login_required
def template_edit(request, pk):
"""Edit an existing project template"""
user = request.user
template = get_object_or_404(QIProject, pk=pk, is_template=True)
# Check permission
if not user.is_px_admin():
if template.hospital and template.hospital != user.hospital:
messages.error(request, _("You don't have permission to edit this template."))
return redirect('projects:template_list')
if request.method == 'POST':
form = QIProjectTemplateForm(request.POST, instance=template, user=user)
if form.is_valid():
form.save()
messages.success(request, _("Project template updated successfully."))
return redirect('projects:template_list')
else:
form = QIProjectTemplateForm(instance=template, user=user)
context = {
'form': form,
'template': template,
'is_create': False,
}
return render(request, 'projects/template_form.html', context)
@block_source_user
@login_required
def template_delete(request, pk):
"""Delete a project template"""
user = request.user
template = get_object_or_404(QIProject, pk=pk, is_template=True)
# Check permission
if not user.is_px_admin():
if template.hospital and template.hospital != user.hospital:
messages.error(request, _("You don't have permission to delete this template."))
return redirect('projects:template_list')
if request.method == 'POST':
template_name = template.name
template.delete()
messages.success(request, _('Template "%(name)s" deleted successfully.') % {'name': template_name})
return redirect('projects:template_list')
context = {
'template': template,
}
return render(request, 'projects/template_delete_confirm.html', context)
# =============================================================================
# PX Action Conversion View
# =============================================================================
@block_source_user
@login_required
def convert_action_to_project(request, action_pk):
"""Convert a PX Action to a QI Project"""
user = request.user
# Check permission
if not (user.is_px_admin() or user.is_hospital_admin or user.is_department_manager):
messages.error(request, _("You don't have permission to create projects."))
return redirect('px_action_center:action_detail', pk=action_pk)
action = get_object_or_404(PXAction, pk=action_pk)
# Check hospital access
if not user.is_px_admin() and user.hospital and action.hospital != user.hospital:
messages.error(request, _("You don't have permission to convert this action."))
return redirect('px_action_center:action_detail', pk=action_pk)
if request.method == 'POST':
form = ConvertToProjectForm(request.POST, user=user, action=action)
if form.is_valid():
# Create project from template or blank
template = form.cleaned_data.get('template')
if template:
# Copy from template
project = QIProject.objects.create(
name=form.cleaned_data['project_name'],
name_ar=template.name_ar,
description=template.description,
hospital=action.hospital,
department=template.department,
project_lead=form.cleaned_data['project_lead'],
target_completion_date=form.cleaned_data['target_completion_date'],
status='pending',
created_by=user
)
# Copy tasks from template
for template_task in template.tasks.all():
QIProjectTask.objects.create(
project=project,
title=template_task.title,
description=template_task.description,
order=template_task.order,
status='pending'
)
else:
# Create blank project
project = QIProject.objects.create(
name=form.cleaned_data['project_name'],
description=action.description,
hospital=action.hospital,
project_lead=form.cleaned_data['project_lead'],
target_completion_date=form.cleaned_data['target_completion_date'],
status='pending',
created_by=user
)
# Link to the action
project.related_actions.add(action)
messages.success(request, _("PX Action converted to QI Project successfully."))
return redirect('projects:project_detail', pk=project.pk)
else:
initial_data = {
'project_name': f"QI Project: {action.title}",
}
form = ConvertToProjectForm(user=user, action=action, initial=initial_data)
context = {
'form': form,
'action': action,
}
return render(request, 'projects/convert_action.html', context)