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'
Slide {slide.order}: {slide.get_layout_display()}
', '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'
Slide {slide.order}
' ) 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)