Skip to content
Open
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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ services:
WFP_AUTH_ACCOUNT:
WFP_AUTH_CLIENT_ID:
WFP_EMAIL_RECIPIENTS_NEW_ACCOUNT:
USE_CELERY:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add this?

logging: &iaso_logging
driver: 'json-file'
options:
Expand Down
Empty file.
16 changes: 16 additions & 0 deletions plugins/polio/api/perfomance_dashboard/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import django_filters

from plugins.polio.models.performance_dashboard import PerformanceDashboard


class PerformanceDashboardFilter(django_filters.FilterSet):
"""
Filter for the NationalLogisticsPlan model.
"""

country = django_filters.NumberFilter(field_name="country__id")
country_block = django_filters.NumberFilter(field_name="country__parent__id")

class Meta:
model = PerformanceDashboard
fields = ["status", "antigen"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget these 2 fields or did you forget to add frontend components to add them?

5 changes: 5 additions & 0 deletions plugins/polio/api/perfomance_dashboard/pagination.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a custom ModelViewset that you should use, and that includes pagination

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from iaso.api.common import Paginator


class PerformanceDashboardPagination(Paginator):
page_size = 20
43 changes: 43 additions & 0 deletions plugins/polio/api/perfomance_dashboard/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from rest_framework import permissions

from plugins.polio.permissions import (
POLIO_PERFORMANCE_ADMIN_PERMISSION,
POLIO_PERFORMANCE_NON_ADMIN_PERMISSION,
POLIO_PERFORMANCE_READ_ONLY_PERMISSION,
)


class HasPerformanceDashboardReadOnlyPermission(permissions.BasePermission):
"""
Allows access for users with any of the logistics permissions.
This is for read-only actions (list, retrieve).
"""

def has_permission(self, request, view):
return (
request.user.has_perm(POLIO_PERFORMANCE_READ_ONLY_PERMISSION.full_name())
or request.user.has_perm(POLIO_PERFORMANCE_NON_ADMIN_PERMISSION.full_name())
or request.user.has_perm(POLIO_PERFORMANCE_ADMIN_PERMISSION.full_name())
)


class HasPerformanceDashboardWritePermission(permissions.BasePermission):
"""
Allows access for users with non-admin or admin logistics permissions.
This is for write actions (create, update).
"""

def has_permission(self, request, view):
return request.user.has_perm(POLIO_PERFORMANCE_NON_ADMIN_PERMISSION.full_name()) or request.user.has_perm(
POLIO_PERFORMANCE_ADMIN_PERMISSION.full_name()
)
Comment on lines +30 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no distinction between the admin perm and the temporary write perm here, this file should probably be more similar to polio.api.vaccines.permissions.py, this means that you'll also probably need to change how these permissions are called. We'll discuss that later



class HasPerformanceDashboardAdminPermission(permissions.BasePermission):
"""
Allows access only for users with admin logistics permissions.
This is for destructive actions (delete).
"""

def has_permission(self, request, view):
return request.user.has_perm(POLIO_PERFORMANCE_ADMIN_PERMISSION.full_name())
98 changes: 98 additions & 0 deletions plugins/polio/api/perfomance_dashboard/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logging

from rest_framework import serializers

from iaso.models import OrgUnit, User
from plugins.polio.models.performance_dashboard import PerformanceDashboard


logger = logging.getLogger(__name__)


class UserNestedSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name"]
ref_name = "UserNestedSerializerForNationalLogisticsPlan"

Comment on lines +12 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a lot of these, you might be able to re-use one


class OrgUnitNestedSerializer(serializers.ModelSerializer):
class Meta:
model = OrgUnit
fields = ["id", "name"]
ref_name = "OrgUnitNestedSerializerForNationalLogisticsPlan"


class PerformanceDashboardListSerializer(serializers.ModelSerializer):
created_by = UserNestedSerializer(read_only=True)
updated_by = UserNestedSerializer(read_only=True)
created_at = serializers.DateTimeField(read_only=True)
updated_at = serializers.DateTimeField(read_only=True)

# For read operations, we want to display the country's name
country_name = serializers.CharField(source="country.name", read_only=True)
# For write operations (create/update), we expect the country ID
country_id = serializers.PrimaryKeyRelatedField(source="country", queryset=OrgUnit.objects.all(), write_only=True)

class Meta:
model = PerformanceDashboard
fields = [
"id",
"date",
"status",
"country_name", # For read operations (displaying nested country object)
"country_id", # For write operations (accepting country ID)
"antigen",
"account",
"created_at",
"created_by",
"updated_at",
"updated_by",
]
read_only_fields = ["account"] # Account will be set automatically by the view or create method


class PerformanceDashboardWriteSerializer(serializers.ModelSerializer):
# Expect the country ID for write operations
country_id = serializers.PrimaryKeyRelatedField(source="country", queryset=OrgUnit.objects.all(), write_only=True)

class Meta:
model = PerformanceDashboard
fields = [
"id",
"date",
"status",
"country_id", # Only country_id is needed for input
"antigen",
]
# read_only_fields = ["account"] # No longer needed here

def create(self, validated_data):
request = self.context.get("request")

if request and hasattr(request, "user") and request.user.is_authenticated:
try:
profile = request.user.iaso_profile
validated_data["created_by"] = request.user
validated_data["account"] = profile.account # Ensure account is set here
except AttributeError as e:
logger.error(f"User {request.user} does not have an iaso_profile or account: {e}")
raise serializers.ValidationError("User profile or account not found.")
except Exception as e:
logger.error(f"Unexpected error getting profile/account for {request.user}: {e}")
raise serializers.ValidationError("Unexpected error encountered while fetching profile/account.")
else:
# This should ideally not happen if permissions are checked correctly before the serializer
logger.error("Request or authenticated user not available in context during creation.")
raise serializers.ValidationError("Request context or authenticated user missing.")

# Call the parent create method with the updated validated_data
return super().create(validated_data)

def update(self, instance, validated_data):
# Set updated_by automatically from the request user
request = self.context.get("request")
if request and hasattr(request, "user") and request.user.is_authenticated:
validated_data["updated_by"] = request.user
# Note: account is typically not changed during an update
return super().update(instance, validated_data)
98 changes: 98 additions & 0 deletions plugins/polio/api/perfomance_dashboard/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import django_filters

from rest_framework import filters, permissions, viewsets

from plugins.polio.api.perfomance_dashboard.serializers import (
PerformanceDashboardListSerializer,
PerformanceDashboardWriteSerializer,
)
from plugins.polio.models.performance_dashboard import PerformanceDashboard

from .filters import PerformanceDashboardFilter
from .pagination import PerformanceDashboardPagination
from .permissions import (
HasPerformanceDashboardAdminPermission,
HasPerformanceDashboardReadOnlyPermission,
HasPerformanceDashboardWritePermission,
)


class PerformanceDashboardViewSet(viewsets.ModelViewSet):
"""
API endpoint for National Logistics Plans.
This endpoint supports filtering by:
- `country` (ID of the country OrgUnit)
- `country_block` (ID of the parent OrgUnit of the country, e.g., a region)
- `status` (draft, commented, final)
- `antigen` (bOPV, nOPV2, etc.)
The permissions are structured as follows:
- **Read-only**: Can only list and retrieve plans.
- **Non-admin**: Can create and update plans.
- **Admin**: Can delete plans.
"""

# The serializer_class is removed because 'get_serializer_class' is implemented
# DRF will call get_serializer_clas() instead of looking for this attribute
# serializer_class =
pagination_class = PerformanceDashboardPagination
filter_backends = [
filters.OrderingFilter,
django_filters.rest_framework.DjangoFilterBackend,
]
filterset_class = PerformanceDashboardFilter
ordering_fields = ["date", "country__name", "status", "antigen", "updated_at"]
http_method_names = ["get", "post", "patch", "delete"]

def get_queryset(self):
"""
Get the queryset for the view, filtered for the current user's account.
"""
return (
PerformanceDashboard.objects.filter_for_user(self.request.user)
.select_related("country", "created_by", "updated_by")
.order_by("-date")
)

def get_permissions(self):
"""
Instantiate and return the list of permissions that this view requires,
based on the action being performed.
"""
if self.action in ["list", "retrieve"]:
permission_classes = [HasPerformanceDashboardReadOnlyPermission]
elif self.action in ["create", "partial_update"]:
permission_classes = [HasPerformanceDashboardWritePermission]
elif self.action == "destroy":
permission_classes = [HasPerformanceDashboardAdminPermission]
else:
permission_classes = [permissions.IsAuthenticated]
Comment on lines +69 to +70
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All scenarios are covered here, if a new one is added and somebody forgets to add it here, let it crash

Suggested change
else:
permission_classes = [permissions.IsAuthenticated]


return [permission() for permission in permission_classes]

def get_serializer_class(self):
"""
Dynamically returns the appropriate serializer class based on the action.
"""
if self.action in ["list", "retrieve"]:
# For read-only actions (GET), use the serializer that shows detailed, nested data.
return PerformanceDashboardListSerializer

if self.action in ["create", "update", "partial_update"]:
# For write actions (POST, PATCH), use the serializer that accepts simple IDs.
return PerformanceDashboardWriteSerializer
# As a fallback, you can return the default serializer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should remove the non-relevant AI comments 😅

Suggested change
# As a fallback, you can return the default serializer


return super().get_serializer_class()

def get_serializer_context(self):
"""
Pass the request context to the serializer.
This is crucial for the serializer's create/update methods.
"""
# Call the parent implementation to get the default context
context = super().get_serializer_context()
# Explicitly add the request object to the context
context["request"] = self.request
return context
Comment on lines +89 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do that, DRF will do it for you

2 changes: 2 additions & 0 deletions plugins/polio/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from plugins.polio.api.lqas_im.lqasim_global_map import LQASIMGlobalMapViewSet
from plugins.polio.api.lqas_im.lqasim_zoom_in_map import LQASIMZoominMapBackgroundViewSet, LQASIMZoominMapViewSet
from plugins.polio.api.notifications.views import NotificationViewSet
from plugins.polio.api.perfomance_dashboard.views import PerformanceDashboardViewSet
from plugins.polio.api.polio_org_units import PolioOrgunitViewSet
from plugins.polio.api.rounds.reasons_for_delay import ReasonForDelayViewSet
from plugins.polio.api.rounds.round import RoundViewSet
Expand Down Expand Up @@ -68,6 +69,7 @@


router = routers.SimpleRouter()
router.register(r"polio/performance_dashboard", PerformanceDashboardViewSet, basename="performance_dashboard")
router.register(r"polio/orgunits", PolioOrgunitViewSet, basename="PolioOrgunit")
router.register(r"polio/campaigns", CampaignViewSet, basename="Campaign")
router.register(r"polio/campaigns_subactivities", SubActivityViewSet, basename="campaigns_subactivities")
Expand Down
8 changes: 8 additions & 0 deletions plugins/polio/js/src/constants/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import AssessmentIcon from '@mui/icons-material/Assessment';
import BarChartIcon from '@mui/icons-material/BarChart';
import CalendarToday from '@mui/icons-material/CalendarToday';
import DonutSmallIcon from '@mui/icons-material/DonutSmall';
import ExtensionIcon from '@mui/icons-material/Extension';
Expand Down Expand Up @@ -40,6 +41,7 @@ import {
stockManagementPath,
supplychainPath,
vaccineRepositoryPath,
performanceDashboardPath,
} from './routes';

export const menu: MenuItem[] = [
Expand Down Expand Up @@ -144,6 +146,12 @@ export const menu: MenuItem[] = [
permissions: stockManagementPath.permissions,
icon: props => <StorageIcon {...props} />,
},
{
label: MESSAGES.performanceDashboard,
key: 'performanceDashboard',
permissions: performanceDashboardPath.permissions,
icon: props => <BarChartIcon {...props} />,
},
{
label: MESSAGES.vaccineRepository,
key: 'repository',
Expand Down
4 changes: 4 additions & 0 deletions plugins/polio/js/src/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ const MESSAGES = defineMessages({
defaultMessage: 'Campaigns',
id: 'iaso.polio.label.campaigns',
},
performanceDashboard: {
id: 'iaso.polio.performanceDashboard',
defaultMessage: 'Performance Dashboard',
},
campaign: {
defaultMessage: 'Campaign',
id: 'iaso.polio.label.campaign',
Expand Down
9 changes: 9 additions & 0 deletions plugins/polio/js/src/constants/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const STOCK_MANAGEMENT_READ = 'iaso_polio_vaccine_stock_management_read';
const STOCK_MANAGEMENT_READ_ONLY =
'iaso_polio_vaccine_stock_management_read_only';
const STOCK_MANAGEMENT_WRITE = 'iaso_polio_vaccine_stock_management_write';

const POLIO_PERFORMANCE_READ_ONLY_PERMISSION =
'iaso_polio_performance_read_only';
const POLIO_PERFORMANCE_NON_ADMIN_PERMISSION =
'iaso_polio_performance_non_admin';
const POLIO_PERFORMANCE_ADMIN_PERMISSION = 'iaso_polio_performance_admin';
const NOTIFICATION = 'iaso_polio_notifications';
const CHRONOGRAM = 'iaso_polio_chronogram';
const CHRONOGRAM_RESTRICTED_WRITE = 'iaso_polio_chronogram_restricted_write';
Expand All @@ -30,6 +36,9 @@ export {
STOCK_MANAGEMENT_READ,
STOCK_MANAGEMENT_WRITE,
STOCK_MANAGEMENT_READ_ONLY,
POLIO_PERFORMANCE_READ_ONLY_PERMISSION,
POLIO_PERFORMANCE_NON_ADMIN_PERMISSION,
POLIO_PERFORMANCE_ADMIN_PERMISSION,
NOTIFICATION,
CHRONOGRAM,
CHRONOGRAM_RESTRICTED_WRITE,
Expand Down
15 changes: 15 additions & 0 deletions plugins/polio/js/src/constants/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { LqasAfroOverview } from '../domains/LQAS-IM/LQAS/LqasAfroOverview/LqasA
import { Notifications } from '../domains/Notifications';
import { Nopv2AuthorisationsDetails } from '../domains/VaccineModule/Nopv2Authorisations/Details/Nopv2AuthorisationsDetails';
import { Nopv2Authorisations } from '../domains/VaccineModule/Nopv2Authorisations/Nopv2Authorisations';
import { PerformanceDashboard } from '../domains/VaccineModule/PerformanceDashboard/PerformanceDashboard';
import { VaccineRepository } from '../domains/VaccineModule/Repository/VaccineRepository';
import { VaccineStockManagementDetails } from '../domains/VaccineModule/StockManagement/Details/VaccineStockManagementDetails';
import { PublicVaccineStock } from '../domains/VaccineModule/StockManagement/PublicPage/PublicVaccineStock';
Expand All @@ -41,6 +42,9 @@ import {
SUPPLYCHAIN_READ,
SUPPLYCHAIN_READ_ONLY,
SUPPLYCHAIN_WRITE,
POLIO_PERFORMANCE_READ_ONLY_PERMISSION,
POLIO_PERFORMANCE_NON_ADMIN_PERMISSION,
POLIO_PERFORMANCE_ADMIN_PERMISSION,
} from './permissions';
import {
EMBEDDED_CALENDAR_URL,
Expand Down Expand Up @@ -257,6 +261,16 @@ export const chronogramDetailsPath: RoutePath = {
element: <ChronogramDetails />,
permissions: [CHRONOGRAM, CHRONOGRAM_RESTRICTED_WRITE],
};
export const performanceDashboardPath: RoutePath = {
baseUrl: baseUrls.performanceDashboard,
routerUrl: `${baseUrls.performanceDashboard}/*`,
element: <PerformanceDashboard />,
permissions: [
POLIO_PERFORMANCE_READ_ONLY_PERMISSION,
POLIO_PERFORMANCE_NON_ADMIN_PERMISSION,
POLIO_PERFORMANCE_ADMIN_PERMISSION,
],
};

export const routes: (RoutePath | AnonymousRoutePath)[] = [
campaignsPath,
Expand Down Expand Up @@ -287,4 +301,5 @@ export const routes: (RoutePath | AnonymousRoutePath)[] = [
chronogramPath,
chronogramTemplateTaskPath,
chronogramDetailsPath,
performanceDashboardPath,
];
Loading
Loading