diff --git a/care/emr/api/viewsets/report/report_upload.py b/care/emr/api/viewsets/report/report_upload.py index f30f9b8cb7..3b01b1356d 100644 --- a/care/emr/api/viewsets/report/report_upload.py +++ b/care/emr/api/viewsets/report/report_upload.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.utils import timezone from django_filters import BooleanFilter, CharFilter, FilterSet from django_filters.rest_framework import DjangoFilterBackend @@ -20,7 +21,11 @@ read_report_authorizer, write_report_authorizer, ) +from care.emr.reports.context_builder.data_point_registry import DataPointRegistry from care.emr.reports.renderer.generators import GeneratorRegistry +from care.emr.reports.renderer.renderer import Renderer +from care.emr.reports.report_type_registry import ReportTypeRegistry +from care.emr.reports.report_type_utils import validate_associating_id from care.emr.resources.report.report_upload.spec import ( ReportUploadListSpec, ReportUploadRetrieveSpec, @@ -148,6 +153,62 @@ def generate(self, request, *args, **kwargs): status=status.HTTP_201_CREATED, ) + @extend_schema( + request=GenerateReportRequest, responses={200: "Success"}, tags=["report"] + ) + @action(detail=False, methods=["POST"]) + def preview(self, request, *args, **kwargs): + if not settings.TEMPLATE_LIVE_PREVIEW_ENABLED: + raise PermissionDenied("Live preview is not enabled") + request_data = GenerateReportRequest.model_validate(request.data) + template = get_object_or_404(Template, external_id=request_data.template_id) + + if template.facility and not AuthorizationController.call( + "can_preview_report_from_template", request.user, template.facility + ): + raise PermissionDenied("You do not have permission to preview reports") + + try: + report_type_config = ReportTypeRegistry.get(template.template_type) + except KeyError as err: + error_msg = f"Report Type '{template.template_type}' not found in ReportTypeRegistry" + raise ValidationError(error_msg) from err + + report_authorizer( + request.user, template.template_type, request_data.associating_id, "read" + ) + output_format = request_data.output_format or template.default_format + + try: + generator_class = GeneratorRegistry.get(output_format) + generator = generator_class() + + validated_options = generator.options_model.model_validate(template.options) + + context_class = DataPointRegistry.get(template.context) + if not context_class: + error_msg = ( + f"Context '{template.context}' not found in DataPointRegistry" + ) + raise ValidationError(error_msg) + + context_key = context_class.context_key or template.context + associating_object = validate_associating_id( + associating_model=report_type_config.associating_model, + associating_id=str(request_data.associating_id), + report_type_key=template.template_type, + ) + context = {context_key: context_class(context=associating_object)} + + rendered_content = Renderer(generator).render( + template.template_data, context, validated_options + ) + + return generator.get_http_response(rendered_content) + + except Exception as e: + raise ValidationError("Preview generation failed") from e + class ArchiveRequestSpec(BaseModel): archive_reason: str diff --git a/care/emr/reports/authorizers/encounter.py b/care/emr/reports/authorizers/encounter.py index 6af4d918ea..1bc23cd201 100644 --- a/care/emr/reports/authorizers/encounter.py +++ b/care/emr/reports/authorizers/encounter.py @@ -8,8 +8,8 @@ class EncounterReportAuthorizer(BaseReportAuthorizer): def authorize_read(self, user, associating_id: str) -> bool: encounter_obj = get_object_or_404(Encounter, external_id=associating_id) return AuthorizationController.call( - "can_view_clinical_data", user, encounter_obj.patient - ) or AuthorizationController.call("can_view_encounter_obj", user, encounter_obj) + "can_preview_report_for_encounter", user, encounter_obj + ) def authorize_write(self, user, associating_id: str) -> bool: encounter_obj = get_object_or_404(Encounter, external_id=associating_id) diff --git a/care/security/authorization/encounter.py b/care/security/authorization/encounter.py index dfa3cb045f..35c48cb0a8 100644 --- a/care/security/authorization/encounter.py +++ b/care/security/authorization/encounter.py @@ -178,6 +178,20 @@ def can_generate_report_for_encounter(self, user, encounter): user, encounter, EncounterPermissions.can_write_encounter.name ) + def can_preview_report_for_encounter(self, user, encounter): + """ + Check if the user has permission to preview report for this encounter + """ + if encounter.status in COMPLETED_CHOICES: + return self.check_permission_in_encounter( + user, + encounter, + TemplatePermissions.can_generate_report_for_completed_encounter.name, + ) + return self.check_permission_in_encounter( + user, encounter, EncounterPermissions.can_read_encounter.name + ) + def get_filtered_encounters(self, qs, user, facility): qs = qs.filter(facility=facility) if user.is_superuser: diff --git a/care/security/authorization/template.py b/care/security/authorization/template.py index a4dc1c3776..7730f2f649 100644 --- a/care/security/authorization/template.py +++ b/care/security/authorization/template.py @@ -53,5 +53,15 @@ def can_generate_report_from_template(self, user, facility): facility=facility, ) + def can_preview_report_from_template(self, user, facility): + """ + Check if the user has permission to preview reports from templates + """ + return self.check_permission_in_facility_organization( + [TemplatePermissions.can_read_template.name], + user, + facility=facility, + ) + AuthorizationController.register_internal_controller(TemplateAccess) diff --git a/config/settings/config.py b/config/settings/config.py index dcca022d37..5b61fcd5ad 100644 --- a/config/settings/config.py +++ b/config/settings/config.py @@ -323,3 +323,12 @@ QUESTIONNAIRE_ERRORED_TIME_LIMIT_MINUTES = env.int( "QUESTIONNAIRE_ERRORED_TIME_LIMIT_MINUTES", default=120 ) + +TEMPLATE_LIVE_PREVIEW_ENABLED = env.bool("TEMPLATE_LIVE_PREVIEW_ENABLED", default=False) + +if TEMPLATE_LIVE_PREVIEW_ENABLED: + import logging + + logging.getLogger(__name__).warning( + "\x1b[31;20m TEMPLATE_LIVE_PREVIEW_ENABLED is enabled. This is an experimental feature and should NOT BE USED IN PRODUCTION AT ANY COST. PLEASE USE WITH CAUTION \x1b[0m" + )