Skip to content

Commit

Permalink
Merge pull request #3676 from unicef/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
robertavram authored May 27, 2024
2 parents aa91073 + e01eb8b commit 475ab48
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 10 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ flower = "==1.2.0" # issue when locking
GDAL = "==3.0.2"
gunicorn = "<20.0"
jsonschema = "*"
msal = "*"
newrelic = "*"
Pillow = "==9.3.0"
psycopg2-binary = "==2.9.1"
Expand Down
20 changes: 19 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions __pypackages__/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def assign_permissions(self):
self.add_permissions(
self.all_unicef_users, 'view',
self.follow_up_page,
condition=final_engagement_condition
condition=self.engagement_comments_received_by_unicef()
)
self.add_permissions(
self.all_unicef_users, 'view',
Expand All @@ -403,7 +403,7 @@ def assign_permissions(self):
self.add_permissions(
self.focal_point, 'edit',
self.follow_up_editable_page,
condition=final_engagement_condition
condition=self.engagement_comments_received_by_unicef()
)

# action points related permissions. editable by focal point, author, assignee and assigner
Expand Down
5 changes: 5 additions & 0 deletions src/etools/applications/last_mile/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
view=views_ext.VisionLMSMExport.as_view(http_method_names=['get'],),
name="vision-export-data"
),
path(
'pbi-data/',
view=views.PowerBIDataView.as_view(http_method_names=['get'],),
name="vision-export-data"
),
path(
'points-of-interest/<int:poi_pk>/items/',
view=views.InventoryItemListView.as_view(http_method_names=['get'],),
Expand Down
54 changes: 54 additions & 0 deletions src/etools/applications/last_mile/views.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
from functools import cache

from django.conf import settings
from django.db import connection
from django.db.models import CharField, OuterRef, Prefetch, Q, Subquery
from django.shortcuts import get_object_or_404
from django.utils.functional import cached_property

import requests
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import SearchFilter
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from unicef_restlib.pagination import DynamicPageNumberPagination

from etools.applications.last_mile import models, serializers
from etools.applications.last_mile.filters import TransferFilter
from etools.applications.last_mile.permissions import IsIPLMEditor
from etools.applications.last_mile.tasks import notify_upload_waybill
from etools.applications.utils.pbi_auth import get_access_token, get_embed_token, get_embed_url, TokenRetrieveException


class PointOfInterestTypeViewSet(ReadOnlyModelViewSet):
Expand Down Expand Up @@ -313,3 +318,52 @@ def get_queryset(self):
if not partner:
return super().get_queryset().none()
return super().get_queryset().filter(transfer__partner_organization=partner)


class PowerBIDataView(APIView):
permission_classes = [IsIPLMEditor]

@staticmethod
def get_pbi_access_token():
try:
return get_access_token()
except TokenRetrieveException:
raise PermissionDenied('Token cannot be retrieved')

@cached_property
def pbi_headers(self):
return {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + self.get_pbi_access_token()}

def get_embed_url(self, workspace_id, report_id):
url_to_call = f'https://api.powerbi.com/v1.0/myorg/groups/{workspace_id}/reports/{report_id}'
api_response = requests.get(url_to_call, headers=self.pbi_headers)
if api_response.status_code == 200:
r = api_response.json()
return r["embedUrl"], r["datasetId"]

def get_embed_token(self, dataset_id, workspace_id, report_id):
embed_token_api = 'https://api.powerbi.com/v1.0/myorg/GenerateToken'
request_body = {
"datasets": [{'id': dataset_id}],
"reports": [{'id': report_id}],
"targetWorkspaces": [{'id': workspace_id}]
}
api_response = requests.post(embed_token_api, data=request_body, headers=self.pbi_headers)
if api_response.status_code == 200:
return api_response.json()["token"]
return None

def get(self, request, *args, **kwargs):
try:
embed_url, dataset_id = get_embed_url(self.pbi_headers)
print(embed_url, 'embedurl')
embed_token = get_embed_token(dataset_id, self.pbi_headers)
print(embed_token)
except TokenRetrieveException:
raise PermissionDenied('Token cannot be retrieved')
resp_data = {
"report_id": settings.PBI_CONFIG["REPORT_ID"],
"embed_url": embed_url,
"access_token": embed_token
}
return Response(resp_data, status=status.HTTP_200_OK)
Original file line number Diff line number Diff line change
Expand Up @@ -1811,7 +1811,7 @@ def test_partner_details(self):
realms__data=['IP Viewer'],
profile__organization=intervention.agreement.partner.organization
)
with self.assertNumQueries(192):
with self.assertNumQueries(193):
response = self.forced_auth_req(
"patch",
reverse('pmp_v3:intervention-detail', args=[intervention.pk]),
Expand All @@ -1838,7 +1838,7 @@ def test_unicef_details(self):
budget_owner = UserFactory(is_staff=True)
office = OfficeFactory()
section = SectionFactory()
with self.assertNumQueries(203):
with self.assertNumQueries(204):
response = self.forced_auth_req(
"patch",
reverse('pmp_v3:intervention-detail', args=[intervention.pk]),
Expand Down Expand Up @@ -1887,7 +1887,7 @@ def test_unicef_extended_details(self):
site2 = LocationSiteFactory()
site3 = LocationSiteFactory()

with self.assertNumQueries(253):
with self.assertNumQueries(254):
response = self.forced_auth_req(
"patch",
reverse('pmp_v3:intervention-detail', args=[intervention.pk]),
Expand Down
9 changes: 5 additions & 4 deletions src/etools/applications/partners/validation/interventions.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,14 @@ def start_date_related_agreement_valid(i):
if i.number.find("-") > -1:
has_valid_dates = i.start and i.agreement.start
else:
has_valid_dates = i.start and i.agreement.start and i.start < i.agreement.start
has_valid_dates = i.start and i.agreement.start and i.start >= i.agreement.start

# Check if there is a signed document or attachment
has_signed_document = i.signed_pd_document or i.signed_pd_attachment
has_signed_document = i.signed_pd_document or i.signed_pd_attachment.exists()

if is_pd_or_spd and not_contingency_pd and has_valid_dates and has_signed_document:
return False
if is_pd_or_spd and not_contingency_pd and has_signed_document:
if not has_valid_dates:
return False
return True


Expand Down
82 changes: 82 additions & 0 deletions src/etools/applications/utils/pbi_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from django.conf import settings
from django.core.cache import cache

import msal
import requests


class TokenRetrieveException(BaseException):
"""Exception thrown when migration is failing due validation"""


pbi_config = settings.PBI_CONFIG


def get_access_token():
# try to get it from cache:
cache_key = 'lmsm_pbi_access_token'
access_token = cache.get(cache_key, None)
if not access_token:
# cache.add(cache_key, access_token, timeout=1800)

required_keys = ['AUTHENTICATION_MODE', 'WORKSPACE_ID',
'REPORT_ID', 'TENANT_ID',
'CLIENT_ID', 'CLIENT_SECRET', 'SCOPE_BASE', 'AUTHORITY_URL']

if not all(key in pbi_config and pbi_config[key] for key in required_keys):
raise TokenRetrieveException('Token required keys: {}'.format(required_keys))
try:

# Service Principal auth is the recommended by Microsoft to achieve App Owns Data Power BI embedding
if pbi_config['AUTHENTICATION_MODE'].lower() == 'serviceprincipal':
authority = pbi_config['AUTHORITY_URL'].replace('organizations', pbi_config['TENANT_ID'])
clientapp = msal.ConfidentialClientApplication(pbi_config['CLIENT_ID'],
client_credential=pbi_config['CLIENT_SECRET'],
authority=authority)

# Make a client call if Access token is not available in cache
response = clientapp.acquire_token_for_client(scopes=pbi_config['SCOPE_BASE'])
else:
raise TokenRetrieveException(f"Not supported authentication mode: {pbi_config['AUTHENTICATION_MODE']}")

try:
token_return = response['access_token']
except KeyError:
raise TokenRetrieveException(response['error_description'])
else:
cache.add(cache_key, token_return, timeout=1800)
return token_return
except Exception as ex:
raise TokenRetrieveException('Error retrieving Access token\n' + str(ex))

return access_token


def get_embed_url(pbi_headers):
workspace_id = pbi_config['WORKSPACE_ID']
report_id = pbi_config['REPORT_ID']

url_to_call = f'https://api.powerbi.com/v1.0/myorg/groups/{workspace_id}/reports/{report_id}'
api_response = requests.get(url_to_call, headers=pbi_headers)
if api_response.status_code == 200:
r = api_response.json()
return r["embedUrl"], r["datasetId"]
else:
raise TokenRetrieveException('Error retrieving Embed URL')


def get_embed_token(dataset_id, pbi_headers):
workspace_id = pbi_config['WORKSPACE_ID']
report_id = pbi_config['REPORT_ID']

embed_token_api = 'https://api.powerbi.com/v1.0/myorg/GenerateToken'
request_body = {
"datasets": [{'id': dataset_id}],
"reports": [{'id': report_id}],
"targetWorkspaces": [{'id': workspace_id}]
}
api_response = requests.post(embed_token_api, json=request_body, headers=pbi_headers)
if api_response.status_code == 200:
return api_response.json()["token"]
else:
raise TokenRetrieveException('Error retrieving Embed Token')
11 changes: 11 additions & 0 deletions src/etools/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,3 +682,14 @@ def before_send(event, hint):
"/api/pmp/v3",
"/api/v2/funds",
]

PBI_CONFIG = {
"AUTHENTICATION_MODE": 'ServicePrincipal',
"WORKSPACE_ID": get_from_secrets_or_env('PBI_LMSM_WORKSPACE_ID', ''),
"REPORT_ID": get_from_secrets_or_env('PBI_LMSM_REPORT_ID', ''),
"TENANT_ID": get_from_secrets_or_env('PBI_LMSM_TENANT_ID', ''),
"CLIENT_ID": get_from_secrets_or_env('PBI_LMSM_CLIENT_ID', ''),
"CLIENT_SECRET": get_from_secrets_or_env('PBI_LMSM_CLIENT_SECRET', ''),
"SCOPE_BASE": ['https://analysis.windows.net/powerbi/api/.default'],
"AUTHORITY_URL": f"https://login.microsoftonline.com/{get_from_secrets_or_env('PBI_LMSM_TENANT_ID', '')}"
}

0 comments on commit 475ab48

Please sign in to comment.