Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions api/drf_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,16 @@ def get_queryset(self):
user = getattr(self.request, "user", None)

if not user or not user.is_authenticated:
snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC)
# Guests: only PUBLIC and exclude backend-gated power_bi embeds entirely
snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC).exclude(
snippet__contains='data-snippet-type="power_bi"'
)
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(
snippet__contains='data-snippet-type="power_bi"'
)
elif is_user_ifrc(user):
snip_qs = RegionSnippet.objects.all()
else:
Expand All @@ -318,6 +323,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 if marked auth-required and user is not IFRC
snip_qs = snip_qs.exclude(snippet__contains='data-snippet-type="power_bi"')

return self.queryset.prefetch_related(models.Prefetch("snippets", queryset=snip_qs))

Expand Down Expand Up @@ -679,6 +686,16 @@ def get_serializer_class(self):
return RegionSnippetTableauSerializer
return RegionSnippetSerializer

def get_queryset(self):
qs = super().get_queryset()
user = getattr(self.request, "user", None)
if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest):
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
# Non-IFRC auth users: still exclude power_bi when present and auth_required
if not is_user_ifrc(user):
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
return qs


class CountrySnippetViewset(ReadOnlyVisibilityViewset):
authentication_classes = (TokenAuthentication,)
Expand All @@ -691,6 +708,15 @@ def get_serializer_class(self):
return CountrySnippetTableauSerializer
return CountrySnippetSerializer

def get_queryset(self):
qs = super().get_queryset()
user = getattr(self.request, "user", None)
if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest):
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
if not is_user_ifrc(user):
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
return qs


class DistrictViewset(viewsets.ReadOnlyModelViewSet):
queryset = District.objects.select_related("country").filter(country__is_deprecated=False).filter(is_deprecated=False)
Expand Down Expand Up @@ -888,6 +914,15 @@ class EventSnippetViewset(ReadOnlyVisibilityViewset):
visibility_model_class = Snippet
ordering_fields = "__all__"

def get_queryset(self):
qs = super().get_queryset()
user = getattr(self.request, "user", None)
if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest):
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
if not is_user_ifrc(user):
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
return qs


class SituationReportTypeViewset(viewsets.ReadOnlyModelViewSet):
queryset = SituationReportType.objects.all()
Expand Down
20 changes: 19 additions & 1 deletion api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -494,13 +495,18 @@ class Meta:
"image",
"visibility",
"visibility_display",
"embed",
"id",
)

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:
Expand Down Expand Up @@ -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
Expand All @@ -562,13 +569,18 @@ class Meta:
"image",
"visibility",
"visibility_display",
"embed",
"id",
)

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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
42 changes: 42 additions & 0 deletions api/test_snippet_embed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from api.utils import parse_snippet_embed


def test_parse_power_bi_with_report_id():
html = (
'<div class="embed-power-bi" '
'data-snippet-type="power_bi" '
'data-report-id="00000000-0000-0000-0000-000000000000" '
'data-auth-required="true"></div>'
)
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 = (
'<div class="embed-power-bi" '
'data-snippet-type="power_bi" '
'data-embed-url="https://app.powerbi.com/reportEmbed?x=1"></div>'
)
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 = '<div data-snippet-type="tableau" data-report-id="x"></div>'
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
54 changes: 54 additions & 0 deletions api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,57 @@ 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.
<div class="embed-power-bi" data-snippet-type="power_bi" data-report-id="<GUID>" data-auth-required="true"></div>

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 defaults to True when missing.
"""
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

# auth-required (defaults true)
auth_match = re.search(r'data-auth-required\s*=\s*"?(true|false)"?', html, re.IGNORECASE)
auth_required = True
if auth_match:
auth_required = auth_match.group(1).lower() == "true"

result = {
"type": "power_bi",
"auth_required": auth_required,
}
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
Loading