579 lines
21 KiB
Python
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)
|