-
Notifications
You must be signed in to change notification settings - Fork 9
[POLIO-2027] Add Performance Dashboard Backend API #2557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
5ed99a0
d5fff36
02b5fda
22f1f50
2dfdcca
8b9ea87
a1dae55
f487f06
5a859f3
3148d68
2de235d
afba14d
46bc6af
6dc0329
d7a632c
06038d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a custom |
| 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 |
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
|
|
||
| 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()) | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "antigen", | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "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", | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ] | ||
| # read_only_fields = ["account"] # No longer needed here | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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: | ||
Crebert08 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| validated_data["updated_by"] = request.user | ||
| # Note: account is typically not changed during an update | ||
| return super().update(instance, validated_data) | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
|
|
||||||
| 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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should remove the non-relevant AI comments 😅
Suggested change
|
||||||
|
|
||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
There was a problem hiding this comment.
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?