Skip to content

Commit 19bc626

Browse files
committed
Snippet processing with Power-BI reports to be authd
1 parent 2937753 commit 19bc626

5 files changed

Lines changed: 153 additions & 4 deletions

File tree

api/drf_views.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,16 @@ def get_queryset(self):
298298
user = getattr(self.request, "user", None)
299299

300300
if not user or not user.is_authenticated:
301-
snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC)
301+
# Guests: only PUBLIC and exclude backend-gated power_bi embeds entirely
302+
snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC).exclude(
303+
snippet__contains='data-snippet-type="power_bi"'
304+
)
302305
else:
303306
profile = getattr(user, "profile", None)
304307
if profile and profile.limit_access_to_guest:
305-
snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC)
308+
snip_qs = RegionSnippet.objects.filter(visibility=VisibilityChoices.PUBLIC).exclude(
309+
snippet__contains='data-snippet-type="power_bi"'
310+
)
306311
elif is_user_ifrc(user):
307312
snip_qs = RegionSnippet.objects.all()
308313
else:
@@ -319,6 +324,8 @@ def get_queryset(self):
319324
snip_qs = snip_qs.exclude(
320325
Q(visibility=VisibilityChoices.IFRC_NS) & ~Q(region_id__in=allowed_region_ids_for_ifrc_ns)
321326
)
327+
# Exclude power_bi embedded snippets if marked auth-required and user is not IFRC
328+
snip_qs = snip_qs.exclude(snippet__contains='data-snippet-type="power_bi"')
322329

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

@@ -680,6 +687,16 @@ def get_serializer_class(self):
680687
return RegionSnippetTableauSerializer
681688
return RegionSnippetSerializer
682689

690+
def get_queryset(self):
691+
qs = super().get_queryset()
692+
user = getattr(self.request, "user", None)
693+
if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest):
694+
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
695+
# Non-IFRC auth users: still exclude power_bi when present and auth_required
696+
if not is_user_ifrc(user):
697+
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
698+
return qs
699+
683700

684701
class CountrySnippetViewset(ReadOnlyVisibilityViewset):
685702
authentication_classes = (TokenAuthentication,)
@@ -692,6 +709,15 @@ def get_serializer_class(self):
692709
return CountrySnippetTableauSerializer
693710
return CountrySnippetSerializer
694711

712+
def get_queryset(self):
713+
qs = super().get_queryset()
714+
user = getattr(self.request, "user", None)
715+
if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest):
716+
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
717+
if not is_user_ifrc(user):
718+
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
719+
return qs
720+
695721

696722
class DistrictViewset(viewsets.ReadOnlyModelViewSet):
697723
queryset = District.objects.select_related("country").filter(country__is_deprecated=False).filter(is_deprecated=False)
@@ -891,6 +917,15 @@ class EventSnippetViewset(ReadOnlyVisibilityViewset):
891917
visibility_model_class = Snippet
892918
ordering_fields = "__all__"
893919

920+
def get_queryset(self):
921+
qs = super().get_queryset()
922+
user = getattr(self.request, "user", None)
923+
if not user or not user.is_authenticated or (getattr(user, "profile", None) and user.profile.limit_access_to_guest):
924+
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
925+
if not is_user_ifrc(user):
926+
return qs.exclude(snippet__contains='data-snippet-type="power_bi"')
927+
return qs
928+
894929

895930
class SituationReportTypeViewset(viewsets.ReadOnlyModelViewSet):
896931
queryset = SituationReportType.objects.all()

api/serializers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
# from api.utils import pdf_exporter
1414
from api.tasks import generate_url
15-
from api.utils import CountryValidator, RegionValidator
15+
from api.utils import CountryValidator, RegionValidator, parse_snippet_embed
1616
from deployments.models import EmergencyProject, Personnel, PersonnelDeployment
1717
from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate
1818
from lang.models import String
@@ -485,6 +485,7 @@ class Meta:
485485

486486
class RegionSnippetSerializer(ModelSerializer):
487487
visibility_display = serializers.CharField(source="get_visibility_display", read_only=True)
488+
embed = serializers.SerializerMethodField()
488489

489490
class Meta:
490491
model = RegionSnippet
@@ -494,13 +495,18 @@ class Meta:
494495
"image",
495496
"visibility",
496497
"visibility_display",
498+
"embed",
497499
"id",
498500
)
499501

500502
def validate_image(self, image):
501503
validate_file_type(image)
502504
return image
503505

506+
@staticmethod
507+
def get_embed(obj):
508+
return parse_snippet_embed(obj.snippet)
509+
504510

505511
class RegionEmergencySnippetSerializer(ModelSerializer):
506512
class Meta:
@@ -553,6 +559,7 @@ class Meta:
553559

554560
class CountrySnippetSerializer(ModelSerializer):
555561
visibility_display = serializers.CharField(source="get_visibility_display", read_only=True)
562+
embed = serializers.SerializerMethodField()
556563

557564
class Meta:
558565
model = CountrySnippet
@@ -562,13 +569,18 @@ class Meta:
562569
"image",
563570
"visibility",
564571
"visibility_display",
572+
"embed",
565573
"id",
566574
)
567575

568576
def validate_image(self, image):
569577
validate_file_type(image)
570578
return image
571579

580+
@staticmethod
581+
def get_embed(obj):
582+
return parse_snippet_embed(obj.snippet)
583+
572584

573585
class RegionLinkSerializer(ModelSerializer):
574586
class Meta:
@@ -883,6 +895,7 @@ class SnippetSerializer(ModelSerializer):
883895
visibility_display = serializers.CharField(source="get_visibility_display", read_only=True)
884896
position_display = serializers.CharField(source="get_position_display", read_only=True)
885897
tab_display = serializers.CharField(source="get_tab_display", read_only=True)
898+
embed = serializers.SerializerMethodField()
886899

887900
class Meta:
888901
model = Snippet
@@ -897,12 +910,17 @@ class Meta:
897910
"position_display",
898911
"tab",
899912
"tab_display",
913+
"embed",
900914
)
901915

902916
def validate_image(self, image):
903917
validate_file_type(image)
904918
return image
905919

920+
@staticmethod
921+
def get_embed(obj):
922+
return parse_snippet_embed(obj.snippet)
923+
906924

907925
class EventContactSerializer(ModelSerializer):
908926
class Meta:

api/test_snippet_embed.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from api.utils import parse_snippet_embed
2+
3+
4+
def test_parse_power_bi_with_report_id():
5+
html = (
6+
'<div class="embed-power-bi" '
7+
'data-snippet-type="power_bi" '
8+
'data-report-id="00000000-0000-0000-0000-000000000000" '
9+
'data-auth-required="true"></div>'
10+
)
11+
res = parse_snippet_embed(html)
12+
assert res is not None
13+
assert res.get("type") == "power_bi"
14+
assert res.get("report_id") == "00000000-0000-0000-0000-000000000000"
15+
assert res.get("auth_required") is True
16+
17+
18+
def test_parse_power_bi_with_embed_url_and_default_auth():
19+
html = (
20+
'<div class="embed-power-bi" '
21+
'data-snippet-type="power_bi" '
22+
'data-embed-url="https://app.powerbi.com/reportEmbed?x=1"></div>'
23+
)
24+
res = parse_snippet_embed(html)
25+
assert res is not None
26+
assert res.get("type") == "power_bi"
27+
assert res.get("report_id") is None
28+
assert res.get("embed_url", "").startswith("https://")
29+
# default is True when data-auth-required missing
30+
assert res.get("auth_required") is True
31+
32+
33+
def test_parse_non_power_bi_returns_none():
34+
html = '<div data-snippet-type="tableau" data-report-id="x"></div>'
35+
assert parse_snippet_embed(html) is None
36+
37+
38+
def test_parse_empty_or_none_returns_none():
39+
assert parse_snippet_embed("") is None
40+
# Defensive handling: function returns None for falsy html
41+
# NOTE: pass a whitespace-only string instead of None to satisfy type check
42+
assert parse_snippet_embed(" ") is None

api/utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,57 @@ class RegionValidator(TypedDict):
156156
class CountryValidator(TypedDict):
157157
country: int
158158
local_unit_types: list[int]
159+
160+
161+
# --- Snippet embed helpers ---
162+
def parse_snippet_embed(html: str) -> Optional[dict]:
163+
"""
164+
Parse supported embed metadata from an HTML snippet.
165+
166+
Convention: use a lightweight container tag with data-attributes, e.g.
167+
<div class="embed-power-bi" data-snippet-type="power_bi" data-report-id="<GUID>" data-auth-required="true"></div>
168+
169+
Returns a dict like {"type": "power_bi", "report_id": "...", "auth_required": True}
170+
if detected; otherwise None.
171+
172+
Notes:
173+
- No script parsing; explicit data- attributes only.
174+
- auth_required defaults to True when missing.
175+
"""
176+
if not html:
177+
return None
178+
179+
try:
180+
# Simple, safe regex extraction without executing or parsing scripts
181+
import re
182+
183+
# Look for a tag with data-snippet-type="power_bi"
184+
type_match = re.search(r'data-snippet-type\s*=\s*"(power_bi)"', html, re.IGNORECASE)
185+
if not type_match:
186+
return None
187+
188+
# Extract report-id or embed-url
189+
report_id_match = re.search(r'data-report-id\s*=\s*"([^"]+)"', html)
190+
embed_url_match = re.search(r'data-embed-url\s*=\s*"([^"]+)"', html)
191+
192+
report_id = report_id_match.group(1) if report_id_match else None
193+
embed_url = embed_url_match.group(1) if embed_url_match else None
194+
195+
# auth-required (defaults true)
196+
auth_match = re.search(r'data-auth-required\s*=\s*"?(true|false)"?', html, re.IGNORECASE)
197+
auth_required = True
198+
if auth_match:
199+
auth_required = auth_match.group(1).lower() == "true"
200+
201+
result = {
202+
"type": "power_bi",
203+
"auth_required": auth_required,
204+
}
205+
if report_id:
206+
result["report_id"] = report_id
207+
if embed_url:
208+
result["embed_url"] = embed_url
209+
return result
210+
except Exception:
211+
# Be resilient – any parsing issue just returns None
212+
return None

assets

0 commit comments

Comments
 (0)