Skip to content

Commit

Permalink
fix: restrict payments list by user location
Browse files Browse the repository at this point in the history
Refs:IA-3540
  • Loading branch information
quang-le authored Dec 2, 2024
2 parents 978d0b1 + 3b62041 commit eb1f622
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 43 deletions.
4 changes: 0 additions & 4 deletions hat/assets/js/apps/Iaso/domains/orgUnits/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ const useStyles = makeStyles(theme => ({
},
}));

// type Props = {
// params: OrgUnitParams;
// };

const baseUrl = baseUrls.orgUnits;
export const OrgUnits: FunctionComponent = () => {
// HOOKS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ export const PaymentLotActionCell = ({
].join(',');
return (
<>
<IconButton
icon="remove-red-eye"
url={`/${baseUrls.orgUnitsChangeRequest}/userIds/${userIds}/paymentIds/${paymentIds}`}
tooltipMessage={MESSAGES.viewChangeRequestforLot}
disabled={disableButtons}
/>
{paymentLot.can_see_change_requests && (
<IconButton
icon="remove-red-eye"
url={`/${baseUrls.orgUnitsChangeRequest}/userIds/${userIds}/paymentIds/${paymentIds}`}
tooltipMessage={MESSAGES.viewChangeRequestforLot}
disabled={disableButtons}
/>
)}

{paymentLot.status === 'new' && (
<IconButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,15 @@ export const usePaymentColumns = ({
accessor: 'action',
sortable: false,
Cell: settings => {
if (!settings.row.original.can_see_change_requests) {
return '';
}
return (
<IconButton
icon="remove-red-eye"
url={`/${baseUrls.orgUnitsChangeRequest}/userIds/${settings.row.original.user.id}/potentialPaymentIds/${settings.row.original.id}`}
tooltipMessage={
MESSAGES.viewChangeRequestsForPotentialPayment // change text to payment lot
MESSAGES.viewChangeRequestsForPotentialPayment
}
/>
);
Expand All @@ -127,13 +130,15 @@ export const usePaymentColumns = ({
const payment = settings.row.original;
return (
<>
<IconButton
icon="remove-red-eye"
url={`/${baseUrls.orgUnitsChangeRequest}/userIds/${settings.row.original.user.id}/paymentIds/${payment.id}`}
tooltipMessage={
MESSAGES.viewChangeRequestsForPayment
}
/>
{payment.can_see_change_request && (
<IconButton
icon="remove-red-eye"
url={`/${baseUrls.orgUnitsChangeRequest}/userIds/${settings.row.original.user.id}/paymentIds/${payment.id}`}
tooltipMessage={
MESSAGES.viewChangeRequestsForPayment
}
/>
)}
<EditPaymentDialog
status={payment.status}
id={payment.id}
Expand Down
1 change: 1 addition & 0 deletions hat/assets/js/apps/Iaso/domains/payments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type PaymentLot = {
status: PaymentLotStatus;
payments: NestedPayment[];
task?: { id: number; status: TaskStatus; ended_at: string };
can_see_change_requests: boolean;
};
export interface PaymentLotPaginated extends Pagination {
results: PaymentLot[];
Expand Down
88 changes: 64 additions & 24 deletions iaso/api/payments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,35 @@ def get_phone_number(self, obj):


class OrgChangeRequestNestedSerializer(serializers.ModelSerializer):
can_see_change_request = serializers.SerializerMethodField()

class Meta:
model = OrgUnitChangeRequest
fields = ["id", "uuid", "org_unit_id"]
fields = ["id", "uuid", "org_unit_id", "can_see_change_request"]
read_only_fields = ["id", "updated_at"]

def get_can_see_change_request(self, obj):
user = self.context.get("request").user
if user.is_superuser:
return True
user_org_units = list(user.iaso_profile.get_hierarchy_for_user().values_list("id", flat=True))
return obj.org_unit.id in user_org_units

class PaymentSerializer(serializers.ModelSerializer):
change_requests = serializers.SerializerMethodField()

class AuditOrgChangeRequestNestedSerializer(serializers.ModelSerializer):
class Meta:
model = Payment
fields = ["id", "status", "created_at", "updated_at", "created_by", "updated_by", "user", "change_requests"]
read_only_fields = ["id", "created_at", "updated_at"]

pagination_class = PaymentPagination
created_by = UserNestedSerializer()
updated_by = UserNestedSerializer()
user = UserNestedSerializer()
created_at = TimestampField(read_only=True)
updated_at = TimestampField(read_only=True)

def get_change_requests(self, obj):
change_requests = OrgUnitChangeRequest.objects.filter(payment=obj)
return OrgChangeRequestNestedSerializer(change_requests, many=True, context=self.context).data
model = OrgUnitChangeRequest
fields = ["id", "uuid", "org_unit_id"]
read_only_fields = ["id", "updated_at"]


class NestedPaymentSerializer(serializers.ModelSerializer):
change_requests = serializers.SerializerMethodField()
can_see_change_requests = serializers.SerializerMethodField()

class Meta:
model = Payment
fields = ["id", "change_requests", "user", "status"]
fields = ["id", "change_requests", "user", "status", "can_see_change_requests"]
read_only_fields = ["id"]

user = UserNestedSerializer()
Expand All @@ -66,6 +63,16 @@ def get_change_requests(self, obj):
change_requests = OrgUnitChangeRequest.objects.filter(payment=obj)
return OrgChangeRequestNestedSerializer(change_requests, many=True, context=self.context).data

def get_can_see_change_requests(self, obj):
change_requests = self.get_change_requests(obj)

blocked_change_requests = [
change_request for change_request in change_requests if change_request["can_see_change_request"] == False
]
if blocked_change_requests:
return False
return True


class NestedTaskSerializer(serializers.ModelSerializer):
class Meta:
Expand All @@ -76,28 +83,52 @@ class Meta:
class PaymentLotSerializer(serializers.ModelSerializer):
payments = serializers.SerializerMethodField()
task = NestedTaskSerializer(read_only=True)
can_see_change_requests = serializers.SerializerMethodField()

class Meta:
model = PaymentLot
fields = ["id", "name", "status", "created_at", "created_by", "payments", "comment", "task"]
fields = [
"id",
"name",
"status",
"created_at",
"created_by",
"payments",
"comment",
"task",
"can_see_change_requests",
]
read_only_fields = ["id", "created_at"]

pagination_class = PaymentPagination
created_by = UserNestedSerializer()
created_at = TimestampField(read_only=True)

def get_can_see_change_requests(self, obj):
user = self.context.get("request").user
if user.is_superuser:
return True
user_org_units = set(user.iaso_profile.get_hierarchy_for_user().values_list("id", flat=True))
change_requests_org_units_for_lot = (
OrgUnitChangeRequest.objects.filter(payment__in=obj.payments.all())
.values_list("org_unit__id", flat=True)
.distinct()
)
return set(change_requests_org_units_for_lot).issubset(user_org_units)

def get_payments(self, obj):
payments = obj.payments.all()
return NestedPaymentSerializer(payments, many=True, context=self.context).data


class PotentialPaymentSerializer(serializers.ModelSerializer):
change_requests = serializers.SerializerMethodField()
can_see_change_requests = serializers.SerializerMethodField()

class Meta:
model = PotentialPayment
fields = ["id", "user", "change_requests", "payment_lot"]
read_only_fields = ["id", "created_at", "updated_at", "payment_lot"]
fields = ["id", "user", "change_requests", "payment_lot", "can_see_change_requests"]
read_only_fields = ["id", "created_at", "updated_at", "payment_lot", "can_see_change_requests"]

pagination_class = PaymentPagination
user = UserNestedSerializer()
Expand All @@ -111,7 +142,16 @@ def get_change_requests(self, obj):
start_date = request.GET.get("change_requests__created_at_after", None)
end_date = request.GET.get("change_requests__created_at_before", None)
change_requests = filter_by_dates(request, change_requests, start_date, end_date)
return OrgChangeRequestNestedSerializer(change_requests, many=True).data
return OrgChangeRequestNestedSerializer(change_requests, many=True, context=self.context).data

def get_can_see_change_requests(self, obj):
change_requests = self.get_change_requests(obj)
blocked_change_requests = [
change_request for change_request in change_requests if change_request["can_see_change_request"] == False
]
if blocked_change_requests:
return False
return True


class PaymentLotCreateSerializer(serializers.Serializer):
Expand Down Expand Up @@ -150,7 +190,7 @@ def validate_status(self, status):

def get_change_requests(self, obj):
change_requests = OrgUnitChangeRequest.objects.filter(payment=obj)
return OrgChangeRequestNestedSerializer(change_requests, many=True).data
return OrgChangeRequestNestedSerializer(change_requests, many=True, context=self.context).data

def update(self, obj, validated_data):
payment = super().update(obj, validated_data)
Expand All @@ -166,7 +206,7 @@ class Meta:
model = Payment
fields = "__all__"

change_requests = OrgChangeRequestNestedSerializer(many=True)
change_requests = AuditOrgChangeRequestNestedSerializer(many=True)
user = UserNestedSerializer()


Expand Down
13 changes: 12 additions & 1 deletion iaso/api/payments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from iaso.api.payments.filters import potential_payments as potential_payments_filters
from iaso.api.tasks import TaskSerializer
from iaso.models import OrgUnitChangeRequest, Payment, PaymentLot, PotentialPayment
from iaso.models.org_unit import OrgUnit
from iaso.models.payments import PaymentStatuses
from iaso.tasks.create_payment_lot import create_payment_lot
from rest_framework.exceptions import ValidationError
Expand Down Expand Up @@ -449,11 +450,13 @@ class PotentialPaymentsViewSet(ModelViewSet, AuditMixin):
def get_queryset(self):
queryset = (
PotentialPayment.objects.prefetch_related("change_requests")
.prefetch_related("change_requests__org_unit")
.filter(change_requests__created_by__iaso_profile__account=self.request.user.iaso_profile.account)
# Filter out potential payments already linked to a task as this means there's a task running converting them into Payment
.filter(task__isnull=True)
.distinct()
)

queryset = queryset.annotate(change_requests_count=Count("change_requests"))

return queryset
Expand Down Expand Up @@ -572,7 +575,15 @@ class PaymentsViewSet(ModelViewSet):
permission_classes = [permissions.IsAuthenticated, HasPermission(permission.PAYMENTS)]

def get_queryset(self) -> models.QuerySet:
return Payment.objects.filter(created_by__iaso_profile__account=self.request.user.iaso_profile.account)
user = self.request.user
localisation = user.iaso_profile.org_units
queryset = Payment.objects.filter(
created_by__iaso_profile__account=self.request.user.iaso_profile.account
).prefetch_related("change_requests__org_unit")
if localisation:
authorized_org_units = OrgUnit.objects.filter_for_user(user)
queryset = queryset.filter(change_requests__org_unit__in=authorized_org_units)
return queryset

def update(self, request, *args, **kwargs):
with transaction.atomic():
Expand Down
3 changes: 3 additions & 0 deletions iaso/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,9 @@ class Meta:
def __str__(self):
return "%s -- %s" % (self.user, self.account)

def get_hierarchy_for_user(self):
return OrgUnit.objects.filter_for_user_and_app_id(self.user)

def get_user_roles_editable_org_unit_type_ids(self):
try:
return self.annotated_user_roles_editable_org_unit_type_ids
Expand Down
29 changes: 29 additions & 0 deletions iaso/tests/api/payments/test_payment_lots.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def setUpTestData(cls):
cls.user = cls.create_user_with_profile(
username="user", permissions=["iaso_payments", "iaso_sources", "iaso_data_tasks"], account=account
)
cls.geo_limited_user = cls.create_user_with_profile(
username="other_user", permissions=["iaso_payments", "iaso_sources", "iaso_data_tasks"], account=account
)
cls.payment_beneficiary = cls.create_user_with_profile(
username="payment_beneficiary", first_name="John", last_name="Doe", account=account
)
Expand All @@ -35,6 +38,13 @@ def setUpTestData(cls):
version=version,
validation_status=m.OrgUnit.VALIDATION_VALID,
)
cls.other_org_unit = m.OrgUnit.objects.create(
name="Some other place",
org_unit_type=org_unit_type,
version=version,
validation_status=m.OrgUnit.VALIDATION_VALID,
)
cls.geo_limited_user.iaso_profile.org_units.set([cls.other_org_unit])
cls.payment_lot = m.PaymentLot.objects.create(name="Test Payment Lot", created_by=cls.user, updated_by=cls.user)
cls.payment = m.Payment.objects.create(
user=cls.payment_beneficiary,
Expand Down Expand Up @@ -302,3 +312,22 @@ def test_payment_lot_not_created_if_potential_payment_not_found(self):
self.runAndValidateTask(task, "ERRORED")
# No new payment lot created, we find only the one from setup
self.assertEqual(m.PaymentLot.objects.count(), 1)

def test_geo_limited_user_cannot_see_change_requests_not_in_org_units(self):
self.client.force_authenticate(self.geo_limited_user)
response = self.client.get(f"/api/payments/lots/")
self.assertJSONResponse(response, 200)
data = response.json()
results = data["results"]
result = results[0]
self.assertEqual(len(results), 1)
self.assertFalse(result["can_see_change_requests"])
self.assertEqual(len(result["payments"]), 2)
change_requests = result["payments"][0]["change_requests"]
self.assertEqual(len(change_requests), 1)
for change_request in change_requests:
self.assertFalse(change_request["can_see_change_request"])
change_requests = result["payments"][1]["change_requests"]
self.assertEqual(len(change_requests), 1)
for change_request in change_requests:
self.assertFalse(change_request["can_see_change_request"])
32 changes: 32 additions & 0 deletions iaso/tests/api/payments/test_potential_payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ def setUpTestData(cls):
org_unit = m.OrgUnit.objects.create(
org_unit_type=org_unit_type, version=version, uuid="1539f174-4c53-499c-85de-7a58458c49ef"
)
other_org_unit = m.OrgUnit.objects.create(
org_unit_type=org_unit_type, version=version, uuid="1539f174-4c53-499c-85de-7a58458c21gh"
)

account = m.Account.objects.create(name="Account", default_version=version)
another_account = m.Account.objects.create(name="another_account", default_version=version)
Expand All @@ -31,15 +34,23 @@ def setUpTestData(cls):
account=account,
permissions=["iaso_org_unit_change_request_review", "iaso_payments"],
)
geo_limited_user = cls.create_user_with_profile(
username="geo_limited_user",
account=account,
permissions=["iaso_org_unit_change_request_review", "iaso_payments"],
)
geo_limited_user.iaso_profile.org_units.set([other_org_unit])

data_source.projects.set([project])
org_unit_type.projects.set([project])
user.iaso_profile.org_units.set([org_unit])

cls.org_unit = org_unit
cls.other_org_unit = other_org_unit
cls.org_unit_type = org_unit_type
cls.project = project
cls.user = user
cls.geo_limited_user = geo_limited_user
cls.user_with_perm = user_with_perm
cls.user_from_another_account = user_from_another_account
cls.version = version
Expand Down Expand Up @@ -126,3 +137,24 @@ def test_list_does_not_create_potential_payments_for_existing_payments(self):
response = self.client.get("/api/potential_payments/")
self.assertJSONResponse(response, 200)
self.assertEqual(0, len(response.data["results"]))

def test_geo_limited_user_cannot_see_change_requests_not_in_org_units(self):
m.OrgUnitChangeRequest.objects.create(
org_unit=self.org_unit, status=m.OrgUnitChangeRequest.Statuses.APPROVED, created_by=self.user
)
m.OrgUnitChangeRequest.objects.create(
org_unit=self.org_unit,
status=m.OrgUnitChangeRequest.Statuses.APPROVED,
created_by=self.user_with_perm,
)
self.client.force_authenticate(self.geo_limited_user)
response = self.client.get(f"/api/potential_payments/")
self.assertJSONResponse(response, 200)
data = response.json()
results = data["results"]
print("results", results)
self.assertEqual(len(results), 2)
for result in results:
self.assertFalse(result["can_see_change_requests"])
self.assertEqual(len(result["change_requests"]), 1)
self.assertFalse(result["change_requests"][0]["can_see_change_request"])

0 comments on commit eb1f622

Please sign in to comment.