Skip to content

Commit

Permalink
Merge pull request #1902 from unicef/ch33421-sync-realms-from-etools
Browse files Browse the repository at this point in the history
[ch33421] Sync realms from etools
  • Loading branch information
robertavram authored Apr 25, 2023
2 parents 1f32a04 + 0471ace commit ba10f71
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 2 deletions.
2 changes: 2 additions & 0 deletions django_api/etools_prp/apps/core/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ class WorkspaceFactory(factory.django.DjangoModelFactory):
Ex) WorkspaceFactory(countries=[country1, country2, ...])
"""

external_id = factory.LazyFunction(lambda: faker.uuid4()[:32])
workspace_code = fuzzy.FuzzyChoice(COUNTRY_CODES_LIST)
title = factory.LazyAttribute(lambda o: COUNTRIES_ALPHA2_CODE_DICT[o.workspace_code])
business_area_code = factory.LazyFunction(lambda: faker.random_number(4, True))
Expand Down Expand Up @@ -367,6 +368,7 @@ class PartnerFactory(factory.django.DjangoModelFactory):
Ex) PartnerFactory(clusters=[cluster1, cluster2, ...])
"""

external_id = factory.LazyFunction(lambda: faker.uuid4()[:32])
title = factory.LazyFunction(faker.company)
short_title = factory.LazyAttribute(lambda o: o.title)
alternate_title = factory.LazyAttribute(lambda o: o.title)
Expand Down
105 changes: 104 additions & 1 deletion django_api/etools_prp/apps/unicef/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.utils.functional import cached_property

from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator

from etools_prp.apps.account.validators import EmailValidator
from etools_prp.apps.core.common import CURRENCIES, OVERALL_STATUS, PD_DOCUMENT_TYPE, PD_STATUS, PROGRESS_REPORT_STATUS
from etools_prp.apps.core.common import (
CURRENCIES,
OVERALL_STATUS,
PD_DOCUMENT_TYPE,
PD_STATUS,
PROGRESS_REPORT_STATUS,
PRP_IP_ROLE_TYPES,
)
from etools_prp.apps.core.models import Location, Workspace
from etools_prp.apps.core.serializers import ShortLocationSerializer
from etools_prp.apps.indicator.models import IndicatorBlueprint
Expand All @@ -14,6 +24,8 @@
)
from etools_prp.apps.partner.models import Partner

from ..core.models import Realm
from ..utils.serializers import OptionalElementsListSerializer
from .models import (
FinalReview,
LowerLevelOutput,
Expand Down Expand Up @@ -976,3 +988,94 @@ class Meta:
'file_name',
'type',
)


class ImportRealmSerializer(serializers.Serializer):
country = serializers.SlugRelatedField(queryset=Workspace.objects.all(), slug_field='external_id')
organization = serializers.SlugRelatedField(queryset=Partner.objects.all(), slug_field='vendor_number')
group = serializers.SlugRelatedField(queryset=Group.objects.all(), slug_field='name')

@cached_property
def allowed_groups(self):
return [t[0] for t in PRP_IP_ROLE_TYPES]

def map_group(self, value):
return {
"IP Authorized Officer": PRP_IP_ROLE_TYPES.ip_authorized_officer,
"IP Editor": PRP_IP_ROLE_TYPES.ip_editor,
"IP Viewer": PRP_IP_ROLE_TYPES.ip_viewer,
"IP Admin": PRP_IP_ROLE_TYPES.ip_admin,
}.get(value, value)

def run_validation(self, data=None):
if 'group' in data:
data['group'] = self.map_group(data['group'])

return super(ImportRealmSerializer, self).run_validation(data)


class ImportUserRealmsSerializer(serializers.ModelSerializer):
realms = OptionalElementsListSerializer(child=ImportRealmSerializer(), allow_empty=False)

class Meta:
model = get_user_model()
fields = (
'email',
'first_name',
'middle_name',
'last_name',
'realms',
)

def save_realms(self, user, realms):
realms_set = {
(realm['country'].id, realm['organization'].id, realm['group'].id)
for realm in realms
}
user_realms = user.realms.all()
user_realms_dict = {
(realm.workspace_id, realm.partner_id, realm.group_id): realm
for realm in user_realms
}
realms_to_create = []
realms_to_activate = []
realms_to_deactivate = []

for workspace_id, organization_id, group_id in realms_set:
realm_key = (workspace_id, organization_id, group_id)
if realm_key in user_realms_dict:
user_realm = user_realms_dict[realm_key]
if not user_realm.is_active:
realms_to_activate.append(user_realm)
else:
realms_to_create.append(Realm(
user=user,
workspace_id=workspace_id,
partner_id=organization_id,
group_id=group_id,
))

for realm_key, realm in user_realms_dict.items():
if realm_key not in realms_set:
realms_to_deactivate.append(realm)

Realm.objects.bulk_create(realms_to_create)
Realm.objects.filter(pk__in=[realm.id for realm in realms_to_activate]).update(is_active=True)
Realm.objects.filter(pk__in=[realm.id for realm in realms_to_deactivate]).update(is_active=False)

def create(self, validated_data):
realms = validated_data.pop('realms')

first_realm = realms[0]
validated_data['workspace_id'] = first_realm['country'].id
validated_data['partner_id'] = first_realm['organization'].id

instance = super().create(validated_data)
self.save_realms(instance, realms)
return instance

def update(self, instance, validated_data):
realms = validated_data.pop('realms')
instance = super().update(instance, validated_data)
self.save_realms(instance, realms)
return instance
145 changes: 144 additions & 1 deletion django_api/etools_prp/apps/unicef/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from unittest.mock import patch

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.files import File
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q
Expand All @@ -26,7 +28,14 @@
from etools_prp.apps.core.models import Location
from etools_prp.apps.core.tests import factories
from etools_prp.apps.core.tests.base import BaseAPITestCase
from etools_prp.apps.core.tests.factories import faker
from etools_prp.apps.core.tests.factories import (
faker,
GroupFactory,
PartnerFactory,
PartnerUserFactory,
RealmFactory,
WorkspaceFactory,
)
from etools_prp.apps.indicator.disaggregators import QuantityIndicatorDisaggregator
from etools_prp.apps.indicator.models import IndicatorBlueprint, IndicatorLocationData, IndicatorReport, Reportable
from etools_prp.apps.unicef.models import ProgrammeDocument, ProgressReport, ProgressReportAttachment
Expand Down Expand Up @@ -1730,3 +1739,137 @@ def test_export(self):
f"{url}?report_status={report_status}&export=pdf"
)
self.assertEquals(response.status_code, status.HTTP_200_OK)


class TestEToolsRolesSynchronization(BaseAPITestCase):
def test_sync(self):
user = PartnerUserFactory(realms__data=['IP_VIEWER'])
self.assertIsNotNone(user.workspace.external_id)
self.assertIsNotNone(user.partner.vendor_number)
group_to_activate = GroupFactory(name='IP_AUTHORIZED_OFFICER')
RealmFactory(
user=user,
workspace=user.workspace,
partner=user.partner,
group=group_to_activate,
is_active=False,
)
new_group = GroupFactory(name='IP_EDITOR')
input_data = {
'email': user.email,
'first_name': 'John',
'middle_name': '_',
'last_name': 'Doe',
'realms': [
{
'country': user.workspace.external_id,
'organization': user.partner.vendor_number,
'group': 'Partnership Manager',
},
{
'country': user.workspace.external_id,
'organization': user.partner.vendor_number,
'group': "IP Editor",
},
{
'country': "unknown country code",
'organization': user.partner.vendor_number,
'group': "IP Editor",
},
{
'country': user.workspace.external_id,
'organization': "unknown organization vendor number",
'group': "IP Editor",
},
{
'country': user.workspace.external_id,
'organization': user.partner.vendor_number,
'group': "IP_AUTHORIZED_OFFICER",
},
]
}
self.client.force_authenticate(factories.NonPartnerUserFactory())
response = self.client.post(reverse('user-realms-import'), data=input_data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
user.refresh_from_db()
self.assertEqual(user.first_name, 'John')
self.assertEqual(user.middle_name, '_')
self.assertEqual(user.last_name, 'Doe')
self.assertCountEqual(
list(user.realms.all().values_list('workspace', 'partner', 'group__name', 'is_active')),
[
(user.workspace.id, user.partner.id, Group.objects.get(name='IP_VIEWER').name, False),
(user.workspace.id, user.partner.id, new_group.name, True),
(user.workspace.id, user.partner.id, group_to_activate.name, True),
]
)

def test_create_user(self):
user = PartnerUserFactory.build(realms__data=[])
workspace = WorkspaceFactory()
partner = PartnerFactory()
self.assertFalse(get_user_model().objects.filter(email=user.email).exists())
GroupFactory(name='IP_VIEWER')
input_data = {
'email': user.email,
'first_name': user.first_name,
'middle_name': user.middle_name,
'last_name': user.last_name,
'realms': [
{
'country': "unknown country code",
'organization': "unknown organization vendor number",
'group': "IP Editor",
},
{
'country': workspace.external_id,
'organization': partner.vendor_number,
'group': "IP Viewer",
},
]
}
self.client.force_authenticate(factories.NonPartnerUserFactory())
response = self.client.post(reverse('user-realms-import'), data=input_data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK, response.data)
self.assertTrue(get_user_model().objects.filter(email=user.email).exists())
user = get_user_model().objects.get(email=user.email)
self.assertCountEqual(
list(user.realms.all().values_list('workspace', 'partner', 'group__name', 'is_active')),
[
(user.workspace.id, user.partner.id, Group.objects.get(name='IP_VIEWER').name, True),
]
)

def test_auth_required(self):
self.client.force_authenticate()
response = self.client.post(reverse('user-realms-import'), data={})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_empty_realms(self):
user = PartnerUserFactory(realms__data=['IP_VIEWER'])
input_data = {
'email': user.email,
'first_name': user.first_name,
'middle_name': user.middle_name,
'last_name': user.last_name,
'realms': [
{
'country': user.workspace.external_id,
'organization': user.partner.vendor_number,
'group': 'Partnership Manager',
},
{
'country': "unknown country code",
'organization': user.partner.vendor_number,
'group': "IP Editor",
},
{
'country': user.workspace.external_id,
'organization': "unknown organization vendor number",
'group': "IP Editor",
},
]
}
self.client.force_authenticate(factories.NonPartnerUserFactory())
response = self.client.post(reverse('user-realms-import'), data=input_data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data)
2 changes: 2 additions & 0 deletions django_api/etools_prp/apps/unicef/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ProgressReportReviewAPIView,
ProgressReportSRSubmitAPIView,
ProgressReportSubmitAPIView,
UserRealmsImportView,
)

urlpatterns = [
Expand Down Expand Up @@ -89,4 +90,5 @@
re_path(r'^(?P<workspace_id>\d+)/progress-reports/(?P<progress_report_id>\d+)/attachments/(?P<pk>\d+)/$',
ProgressReportAttachmentAPIView.as_view(),
name="progress-reports-attachment"),
re_path(r'^users/realms/import/$', UserRealmsImportView.as_view(), name="user-realms-import")
]
12 changes: 12 additions & 0 deletions django_api/etools_prp/apps/unicef/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
from .import_report import ProgressReportXLSXReader
from .models import LowerLevelOutput, ProgrammeDocument, ProgressReport, ProgressReportAttachment
from .serializers import (
ImportUserRealmsSerializer,
LLOutputSerializer,
ProgrammeDocumentCalculationMethodsSerializer,
ProgrammeDocumentDetailSerializer,
Expand Down Expand Up @@ -1393,3 +1394,14 @@ def post(self, request, workspace_id, pk):

else:
return Response({}, status=statuses.HTTP_200_OK)


class UserRealmsImportView(APIView):
permission_classes = (IsAuthenticated,)

def post(self, request):
user = get_user_model().objects.filter(email=request.data.get('email', None)).first()
serializer = ImportUserRealmsSerializer(data=request.data, instance=user)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response({}, status=statuses.HTTP_200_OK)
41 changes: 41 additions & 0 deletions django_api/etools_prp/apps/utils/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings
from rest_framework.utils import html


class CurrentWorkspaceDefault:

workspace = None
Expand All @@ -20,3 +26,38 @@ def serialize_choices(choices):
'label': label,
} for value, label in choices
]


class OptionalElementsListSerializer(serializers.ListSerializer):
"""
ignore elements having validation errors
"""
def to_internal_value(self, data):
if html.is_html_input(data):
data = html.parse_html_list(data, default=[])

if not isinstance(data, list):
message = self.error_messages['not_a_list'].format(
input_type=type(data).__name__
)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='not_a_list')

ret = []

for item in data:
try:
validated = self.child.run_validation(item)
except ValidationError:
continue
else:
ret.append(validated)

if not self.allow_empty and len(ret) == 0:
message = self.error_messages['empty']
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='empty')

return ret

0 comments on commit ba10f71

Please sign in to comment.