ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

579 lines
21 KiB
Python

import json
import subprocess
import tempfile
import os
from pathlib import Path
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Q, Max
from django.http import JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from apps.core.models import UUIDModel
from apps.organizations.models import Hospital
from .models import Presentation, Slide, PresentationTheme, SlideLayout, PresentationStatus
from .models import ReportTemplate, ReportTemplateSlide
from .services import PresentationGeneratorService, DoctorRatingsReportService
from .data_sources import REPORT_DATA_SOURCES, get_data_source_choices
from .template_generator import TemplateReportGenerator
from .template_parser import ReportTemplateParser
@login_required
def presentation_list(request):
presentations = Presentation.objects.filter(
Q(created_by=request.user) | Q(is_shared=True)
).select_related('created_by', 'hospital').prefetch_related('slides')
status_filter = request.GET.get('status', '')
theme_filter = request.GET.get('theme', '')
if status_filter:
presentations = presentations.filter(status=status_filter)
if theme_filter:
presentations = presentations.filter(theme=theme_filter)
presentations = presentations.distinct()
context = {
'presentations': presentations,
'status_filter': status_filter,
'theme_filter': theme_filter,
'theme_choices': PresentationTheme.choices,
'status_choices': PresentationStatus.choices,
'layout_choices': SlideLayout.choices,
}
return render(request, 'presentations/presentation_list.html', context)
@login_required
def presentation_create(request):
if request.method == 'POST':
presentation = Presentation.objects.create(
title=request.POST.get('title', ''),
subtitle=request.POST.get('subtitle', ''),
description=request.POST.get('description', ''),
theme=request.POST.get('theme', PresentationTheme.HEALTHCARE_MODERN),
presentation_type=request.POST.get('presentation_type', ''),
created_by=request.user,
hospital=request.user.hospital if hasattr(request.user, 'hospital') else None,
presentation_date=request.POST.get('presentation_date') or None,
)
messages.success(request, _('Presentation created successfully.'))
return redirect('presentations:detail', pk=presentation.pk)
context = {
'theme_choices': PresentationTheme.choices,
'layout_choices': SlideLayout.choices,
}
return render(request, 'presentations/presentation_form.html', context)
@login_required
def presentation_detail(request, pk):
presentation = get_object_or_404(
Presentation.objects.select_related('created_by', 'hospital').prefetch_related('slides'),
pk=pk,
)
slides = presentation.slides.all()
context = {
'presentation': presentation,
'slides': slides,
'layout_choices': SlideLayout.choices,
}
return render(request, 'presentations/presentation_detail.html', context)
@login_required
def presentation_edit(request, pk):
presentation = get_object_or_404(Presentation, pk=pk, created_by=request.user)
if request.method == 'POST':
presentation.title = request.POST.get('title', presentation.title)
presentation.subtitle = request.POST.get('subtitle', '')
presentation.description = request.POST.get('description', '')
presentation.theme = request.POST.get('theme', presentation.theme)
presentation.status = request.POST.get('status', presentation.status)
presentation.presentation_type = request.POST.get('presentation_type', '')
presentation.is_shared = request.POST.get('is_shared') == 'on'
presentation.presentation_date = request.POST.get('presentation_date') or None
presentation.save()
messages.success(request, _('Presentation updated.'))
return redirect('presentations:detail', pk=presentation.pk)
context = {
'presentation': presentation,
'theme_choices': PresentationTheme.choices,
'status_choices': PresentationStatus.choices,
}
return render(request, 'presentations/presentation_form.html', context)
@login_required
@require_POST
def presentation_delete(request, pk):
presentation = get_object_or_404(Presentation, pk=pk, created_by=request.user)
presentation.delete()
messages.success(request, _('Presentation deleted.'))
return redirect('presentations:list')
@login_required
def presentation_view(request, pk):
presentation = get_object_or_404(
Presentation.objects.select_related('created_by', 'hospital').prefetch_related('slides'),
pk=pk,
)
slides = presentation.slides.all()
rendered_slides = []
for slide in slides:
try:
rendered = render_to_string(slide.template_name, {
'slide': slide,
'presentation': presentation,
'slide_index': slide.order,
'total_slides': slides.count(),
'request': request,
})
rendered_slides.append({
'id': str(slide.id),
'html': rendered,
'layout': slide.layout,
'title': slide.title,
'notes': slide.speaker_notes,
})
except Exception:
rendered_slides.append({
'id': str(slide.id),
'html': f'<div style="padding:80px;text-align:center;color:#64748b;">Slide {slide.order}: {slide.get_layout_display()}</div>',
'layout': slide.layout,
'title': slide.title,
'notes': '',
})
context = {
'presentation': presentation,
'rendered_slides': rendered_slides,
'slides': slides,
}
return render(request, 'presentations/presentation_view.html', context)
@login_required
def slide_add(request, pk):
presentation = get_object_or_404(Presentation, pk=pk, created_by=request.user)
if request.method == 'POST':
max_order = presentation.slides.aggregate(models.Max('order'))['order__max'] or 0
slide = Slide.objects.create(
presentation=presentation,
layout=request.POST.get('layout', SlideLayout.COVER),
order=max_order + 1,
title=request.POST.get('title', ''),
subtitle=request.POST.get('subtitle', ''),
content=json.loads(request.POST.get('content', '{}')),
speaker_notes=request.POST.get('speaker_notes', ''),
)
messages.success(request, _('Slide added.'))
return redirect('presentations:detail', pk=presentation.pk)
context = {
'presentation': presentation,
'layout_choices': SlideLayout.choices,
}
return render(request, 'presentations/slide_form.html', context)
@login_required
def slide_edit(request, pk, slide_id):
presentation = get_object_or_404(Presentation, pk=pk, created_by=request.user)
slide = get_object_or_404(Slide, pk=slide_id, presentation=presentation)
if request.method == 'POST':
slide.layout = request.POST.get('layout', slide.layout)
slide.title = request.POST.get('title', '')
slide.subtitle = request.POST.get('subtitle', '')
slide.content = json.loads(request.POST.get('content', '{}'))
slide.speaker_notes = request.POST.get('speaker_notes', '')
slide.background_color = request.POST.get('background_color', '')
slide.save()
messages.success(request, _('Slide updated.'))
return redirect('presentations:detail', pk=presentation.pk)
context = {
'presentation': presentation,
'slide': slide,
'layout_choices': SlideLayout.choices,
}
return render(request, 'presentations/slide_form.html', context)
@login_required
@require_POST
def slide_delete(request, pk, slide_id):
presentation = get_object_or_404(Presentation, pk=pk, created_by=request.user)
slide = get_object_or_404(Slide, pk=slide_id, presentation=presentation)
slide.delete()
for i, s in enumerate(presentation.slides.order_by('order'), 1):
s.order = i
s.save(update_fields=['order'])
messages.success(request, _('Slide deleted.'))
return redirect('presentations:detail', pk=presentation.pk)
@login_required
@require_POST
def slides_reorder(request, pk):
presentation = get_object_or_404(Presentation, pk=pk, created_by=request.user)
order_data = json.loads(request.POST.get('order', '[]'))
for item in order_data:
Slide.objects.filter(
pk=item.get('id'),
presentation=presentation,
).update(order=item.get('order', 0))
return JsonResponse({'status': 'ok'})
@login_required
def export_pdf(request, pk):
presentation = get_object_or_404(Presentation, pk=pk)
slides = presentation.slides.all()
rendered_pages = []
for slide in slides:
try:
rendered = render_to_string(slide.template_name, {
'slide': slide,
'presentation': presentation,
'slide_index': slide.order,
'total_slides': slides.count(),
'request': request,
'for_export': True,
})
rendered_pages.append(rendered)
except Exception:
rendered_pages.append(
f'<div style="width:1920px;height:1080px;padding:80px;">Slide {slide.order}</div>'
)
html_content = render_to_string('presentations/export_pdf.html', {
'presentation': presentation,
'pages': rendered_pages,
})
response = HttpResponse(html_content, content_type='text/html')
response['Content-Disposition'] = f'attachment; filename="{presentation.title}.html"'
return response
@login_required
def export_pptx(request, pk):
presentation = get_object_or_404(Presentation, pk=pk)
slides = presentation.slides.all()
slide_data = []
for slide in slides:
slide_data.append({
'layout': slide.layout,
'title': slide.title,
'subtitle': slide.subtitle,
'content': slide.content,
'order': slide.order,
})
data = {
'title': presentation.title,
'subtitle': presentation.subtitle,
'theme': presentation.theme,
'slides': slide_data,
}
response = JsonResponse(data)
response['Content-Disposition'] = f'attachment; filename="{presentation.title}.json"'
return response
@login_required
def presentation_generate(request):
hospitals = Hospital.objects.filter(status='active').order_by('name')
if request.method == 'POST':
hospital_id = request.POST.get('hospital')
year = int(request.POST.get('year', timezone.now().year))
quarter = request.POST.get('quarter')
quarter = int(quarter) if quarter else None
if not hospital_id:
messages.error(request, _('Please select a hospital.'))
else:
try:
report_type = request.POST.get('report_type', 'complaints')
if report_type == 'doctor_ratings':
service = DoctorRatingsReportService(
hospital_id=hospital_id,
year=year,
quarter=quarter,
created_by=request.user,
)
else:
service = PresentationGeneratorService(
hospital_id=hospital_id,
year=year,
quarter=quarter,
created_by=request.user,
)
presentation = service.generate()
messages.success(request, _('Presentation generated successfully with %d slides.') % presentation.slide_count)
return redirect('presentations:detail', pk=presentation.pk)
except Exception as e:
messages.error(request, _('Error generating presentation: %s') % str(e))
current_year = timezone.now().year
years = list(range(current_year, current_year - 5, -1))
context = {
'hospitals': hospitals,
'years': years,
'selected_hospital': request.POST.get('hospital'),
'selected_year': int(request.POST.get('year', current_year)),
'selected_quarter': request.POST.get('quarter'),
'selected_report_type': request.POST.get('report_type', 'complaints'),
}
return render(request, 'presentations/presentation_generate.html', context)
# ── Report Template Views ─────────────────────────────────────
@login_required
def template_list(request):
templates = ReportTemplate.objects.select_related('created_by', 'hospital').prefetch_related('template_slides')
context = {
'templates': templates,
'data_sources': REPORT_DATA_SOURCES,
}
return render(request, 'presentations/template_list.html', context)
@login_required
def template_create(request):
if request.method == 'POST':
name = request.POST.get('name', '')
slug = request.POST.get('slug', '')
data_source = request.POST.get('data_source', '')
description = request.POST.get('description', '')
template = ReportTemplate.objects.create(
name=name,
slug=slug,
data_source=data_source,
description=description,
created_by=request.user,
hospital_id=request.POST.get('hospital') or None,
)
if request.FILES.get('reference_pdf'):
template.reference_pdf = request.FILES['reference_pdf']
template.save()
try:
parser = ReportTemplateParser(template)
parser.parse()
messages.success(request, _('Template created and PDF parsed successfully.'))
except Exception as e:
messages.warning(request, _('Template created but PDF parsing failed: %s') % str(e))
else:
messages.success(request, _('Template created. Upload a reference PDF to auto-generate slides.'))
return redirect('presentations:template_detail', pk=template.pk)
context = {
'data_source_choices': get_data_source_choices(),
'data_sources': REPORT_DATA_SOURCES,
}
return render(request, 'presentations/template_create.html', context)
@login_required
def template_detail(request, pk):
template = get_object_or_404(ReportTemplate.objects.prefetch_related('template_slides'), pk=pk)
template_slides = template.template_slides.order_by('order')
context = {
'template': template,
'template_slides': template_slides,
'layout_choices': SlideLayout.choices,
'data_source_info': REPORT_DATA_SOURCES.get(template.data_source, {}),
}
return render(request, 'presentations/template_detail.html', context)
@login_required
@require_POST
def template_delete(request, pk):
template = get_object_or_404(ReportTemplate, pk=pk, created_by=request.user)
template.delete()
messages.success(request, _('Template deleted.'))
return redirect('presentations:template_list')
@login_required
@require_POST
def template_parse(request, pk):
template = get_object_or_404(ReportTemplate, pk=pk)
if not template.reference_pdf:
return JsonResponse({'status': 'error', 'message': 'No reference PDF uploaded'}, status=400)
try:
parser = ReportTemplateParser(template)
parser.parse()
slide_count = template.template_slides.count()
return JsonResponse({
'status': 'ok',
'slide_count': slide_count,
'message': f'Parsed successfully. {slide_count} slides created.',
})
except Exception as e:
return JsonResponse({'status': 'error', 'message': str(e)}, status=500)
@login_required
@require_POST
def template_slide_create(request, pk):
template = get_object_or_404(ReportTemplate, pk=pk)
max_order = template.template_slides.aggregate(Max('order'))['order__max'] or 0
slide = ReportTemplateSlide.objects.create(
template=template,
order=max_order + 1,
layout=request.POST.get('layout', SlideLayout.COVER),
title_template=request.POST.get('title_template', ''),
subtitle_template=request.POST.get('subtitle_template', ''),
content_mapping=json.loads(request.POST.get('content_mapping', '{}')),
repeat_source=request.POST.get('repeat_source', ''),
max_rows=int(request.POST.get('max_rows', 18)),
)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'status': 'ok',
'slide': {
'id': str(slide.id),
'order': slide.order,
'layout': slide.layout,
'title_template': slide.title_template,
},
})
messages.success(request, _('Slide added.'))
return redirect('presentations:template_detail', pk=template.pk)
@login_required
@require_POST
def template_slide_update(request, pk, slide_id):
template = get_object_or_404(ReportTemplate, pk=pk)
slide = get_object_or_404(ReportTemplateSlide, pk=slide_id, template=template)
slide.layout = request.POST.get('layout', slide.layout)
slide.section_label = request.POST.get('section_label', '')
slide.title_template = request.POST.get('title_template', '')
slide.subtitle_template = request.POST.get('subtitle_template', '')
slide.content_mapping = json.loads(request.POST.get('content_mapping', '{}'))
slide.repeat_source = request.POST.get('repeat_source', '')
slide.repeat_title_key = request.POST.get('repeat_title_key', 'name')
slide.repeat_subtitle_template = request.POST.get('repeat_subtitle_template', '')
slide.max_rows = int(request.POST.get('max_rows', 18))
slide.style_overrides = json.loads(request.POST.get('style_overrides', '{}'))
slide.speaker_notes_template = request.POST.get('speaker_notes_template', '')
slide.save()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'status': 'ok'})
messages.success(request, _('Slide updated.'))
return redirect('presentations:template_detail', pk=template.pk)
@login_required
@require_POST
def template_slide_delete(request, pk, slide_id):
template = get_object_or_404(ReportTemplate, pk=pk)
slide = get_object_or_404(ReportTemplateSlide, pk=slide_id, template=template)
slide.delete()
for i, s in enumerate(template.template_slides.order_by('order'), 1):
s.order = i
s.save(update_fields=['order'])
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'status': 'ok'})
messages.success(request, _('Slide deleted.'))
return redirect('presentations:template_detail', pk=template.pk)
@login_required
@require_POST
def template_slide_reorder(request, pk):
template = get_object_or_404(ReportTemplate, pk=pk)
order_data = json.loads(request.POST.get('order', '[]'))
for item in order_data:
ReportTemplateSlide.objects.filter(
pk=item.get('id'),
template=template,
).update(order=item.get('order', 0))
return JsonResponse({'status': 'ok'})
@login_required
def template_generate(request, pk):
template = get_object_or_404(ReportTemplate, pk=pk, active=True)
if request.method == 'POST':
hospital_id = request.POST.get('hospital')
year = int(request.POST.get('year', timezone.now().year))
quarter = request.POST.get('quarter')
quarter = int(quarter) if quarter else None
if not hospital_id:
messages.error(request, _('Please select a hospital.'))
else:
try:
generator = TemplateReportGenerator(
template_id=template.pk,
hospital_id=hospital_id,
year=year,
quarter=quarter,
created_by=request.user,
)
presentation = generator.generate()
messages.success(request, _('Report generated with %d slides.') % presentation.slide_count)
return redirect('presentations:detail', pk=presentation.pk)
except Exception as e:
messages.error(request, _('Error generating report: %s') % str(e))
hospitals = Hospital.objects.filter(status='active').order_by('name')
current_year = timezone.now().year
years = list(range(current_year, current_year - 5, -1))
context = {
'template': template,
'hospitals': hospitals,
'years': years,
}
return render(request, 'presentations/template_generate.html', context)