diff --git a/api/drf_views.py b/api/drf_views.py index 36b010199..328257949 100644 --- a/api/drf_views.py +++ b/api/drf_views.py @@ -297,12 +297,15 @@ def get_queryset(self): user = getattr(self.request, "user", None) + power_bi_embed = Q(snippet__contains='data-snippet-type="power_bi"') + if not user or not user.is_authenticated: - snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC) + # Guests: only PUBLIC; exclude power_bi embeds + snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC).exclude(power_bi_embed) else: profile = getattr(user, "profile", None) if profile and profile.limit_access_to_guest: - snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC) + snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC).exclude(power_bi_embed) elif is_user_ifrc(user): snip_qs = RegionSnippet.objects.all() else: @@ -319,6 +322,8 @@ def get_queryset(self): snip_qs = snip_qs.exclude( Q(visibility=VisibilityChoices.IFRC_NS) & ~Q(region_id__in=allowed_region_ids_for_ifrc_ns) ) + # Exclude power_bi embedded snippets when user is not IFRC + snip_qs = snip_qs.exclude(power_bi_embed) return self.queryset.prefetch_related(models.Prefetch("snippets", queryset=snip_qs)) @@ -680,6 +685,17 @@ def get_serializer_class(self): return RegionSnippetTableauSerializer return RegionSnippetSerializer + def get_queryset(self): + qs = super().get_queryset() + user = getattr(self.request, "user", None) + power_bi_embed = Q(snippet__contains='data-snippet-type="power_bi"') + if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest): + return qs.exclude(power_bi_embed) + # Non-IFRC auth users: still exclude power_bi embeds + if not is_user_ifrc(user): + return qs.exclude(power_bi_embed) + return qs + class CountrySnippetViewset(ReadOnlyVisibilityViewset): authentication_classes = (TokenAuthentication,) @@ -692,6 +708,16 @@ def get_serializer_class(self): return CountrySnippetTableauSerializer return CountrySnippetSerializer + def get_queryset(self): + qs = super().get_queryset() + user = getattr(self.request, "user", None) + power_bi_embed = Q(snippet__contains='data-snippet-type="power_bi"') + if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest): + return qs.exclude(power_bi_embed) + if not is_user_ifrc(user): + return qs.exclude(power_bi_embed) + return qs + class DistrictViewset(viewsets.ReadOnlyModelViewSet): queryset = District.objects.select_related("country").filter(country__is_deprecated=False).filter(is_deprecated=False) @@ -891,6 +917,16 @@ class EventSnippetViewset(ReadOnlyVisibilityViewset): visibility_model_class = Snippet ordering_fields = "__all__" + def get_queryset(self): + qs = super().get_queryset() + user = getattr(self.request, "user", None) + power_bi_embed = Q(snippet__contains='data-snippet-type="power_bi"') + if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest): + return qs.exclude(power_bi_embed) + if not is_user_ifrc(user): + return qs.exclude(power_bi_embed) + return qs + class SituationReportTypeViewset(viewsets.ReadOnlyModelViewSet): queryset = SituationReportType.objects.all() diff --git a/api/serializers.py b/api/serializers.py index 8a6b08e91..497a49e40 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -12,7 +12,7 @@ # from api.utils import pdf_exporter from api.tasks import generate_url -from api.utils import CountryValidator, RegionValidator +from api.utils import CountryValidator, RegionValidator, parse_snippet_embed from deployments.models import EmergencyProject, Personnel, PersonnelDeployment from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate from lang.models import String @@ -485,6 +485,7 @@ class Meta: class RegionSnippetSerializer(ModelSerializer): visibility_display = serializers.CharField(source="get_visibility_display", read_only=True) + embed = serializers.SerializerMethodField() class Meta: model = RegionSnippet @@ -494,6 +495,7 @@ class Meta: "image", "visibility", "visibility_display", + "embed", "id", ) @@ -501,6 +503,10 @@ def validate_image(self, image): validate_file_type(image) return image + @staticmethod + def get_embed(obj): + return parse_snippet_embed(obj.snippet) + class RegionEmergencySnippetSerializer(ModelSerializer): class Meta: @@ -553,6 +559,7 @@ class Meta: class CountrySnippetSerializer(ModelSerializer): visibility_display = serializers.CharField(source="get_visibility_display", read_only=True) + embed = serializers.SerializerMethodField() class Meta: model = CountrySnippet @@ -562,6 +569,7 @@ class Meta: "image", "visibility", "visibility_display", + "embed", "id", ) @@ -569,6 +577,10 @@ def validate_image(self, image): validate_file_type(image) return image + @staticmethod + def get_embed(obj): + return parse_snippet_embed(obj.snippet) + class RegionLinkSerializer(ModelSerializer): class Meta: @@ -883,6 +895,7 @@ class SnippetSerializer(ModelSerializer): visibility_display = serializers.CharField(source="get_visibility_display", read_only=True) position_display = serializers.CharField(source="get_position_display", read_only=True) tab_display = serializers.CharField(source="get_tab_display", read_only=True) + embed = serializers.SerializerMethodField() class Meta: model = Snippet @@ -897,12 +910,17 @@ class Meta: "position_display", "tab", "tab_display", + "embed", ) def validate_image(self, image): validate_file_type(image) return image + @staticmethod + def get_embed(obj): + return parse_snippet_embed(obj.snippet) + class EventContactSerializer(ModelSerializer): class Meta: diff --git a/api/test_snippet_embed.py b/api/test_snippet_embed.py new file mode 100644 index 000000000..a8c13f1e9 --- /dev/null +++ b/api/test_snippet_embed.py @@ -0,0 +1,42 @@ +from api.utils import parse_snippet_embed + + +def test_parse_power_bi_with_report_id(): + html = ( + '
' + ) + res = parse_snippet_embed(html) + assert res is not None + assert res.get("type") == "power_bi" + assert res.get("report_id") == "00000000-0000-0000-0000-000000000000" + assert res.get("auth_required") is True + + +def test_parse_power_bi_with_embed_url_and_default_auth(): + html = ( + '' + ) + res = parse_snippet_embed(html) + assert res is not None + assert res.get("type") == "power_bi" + assert res.get("report_id") is None + assert res.get("embed_url", "").startswith("https://") + # default is True when data-auth-required missing + assert res.get("auth_required") is True + + +def test_parse_non_power_bi_returns_none(): + html = '' + assert parse_snippet_embed(html) is None + + +def test_parse_empty_or_none_returns_none(): + assert parse_snippet_embed("") is None + # Defensive handling: function returns None for falsy html + # NOTE: pass a whitespace-only string instead of None to satisfy type check + assert parse_snippet_embed(" ") is None diff --git a/api/utils.py b/api/utils.py index c0f43f674..aa8c035cb 100644 --- a/api/utils.py +++ b/api/utils.py @@ -156,3 +156,51 @@ class RegionValidator(TypedDict): class CountryValidator(TypedDict): country: int local_unit_types: list[int] + + +# --- Snippet embed helpers --- +def parse_snippet_embed(html: str) -> Optional[dict]: + """ + Parse supported embed metadata from an HTML snippet. + + Convention: use a lightweight container tag with data-attributes, e.g. + + + Returns a dict like {"type": "power_bi", "report_id": "...", "auth_required": True} + if detected; otherwise None. + + Notes: + - No script parsing; explicit data- attributes only. + - auth_required is always True for power_bi embeds. + """ + if not html: + return None + + try: + # Simple, safe regex extraction without executing or parsing scripts + import re + + # Look for a tag with data-snippet-type="power_bi" + type_match = re.search(r'data-snippet-type\s*=\s*"(power_bi)"', html, re.IGNORECASE) + if not type_match: + return None + + # Extract report-id or embed-url + report_id_match = re.search(r'data-report-id\s*=\s*"([^"]+)"', html) + embed_url_match = re.search(r'data-embed-url\s*=\s*"([^"]+)"', html) + + report_id = report_id_match.group(1) if report_id_match else None + embed_url = embed_url_match.group(1) if embed_url_match else None + + result = { + "type": "power_bi", + "auth_required": True, + } + if report_id: + result["report_id"] = report_id + if embed_url: + result["embed_url"] = embed_url + return result + except Exception: + # Be resilient – any parsing issue just returns None + return None diff --git a/assets b/assets index c4a88de18..517436c7a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit c4a88de1876e5d92a9b49870b416b1cc082c44fa +Subproject commit 517436c7a179917b8aa8c6a985128a01b41baf28