This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new e3e30ffbc refactor(portal): make experiment-summary + statistics 
serializers proto-native (Track D) (#196)
e3e30ffbc is described below

commit e3e30ffbce10796f818be5604745a2d36e2dc901
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 03:42:38 2026 -0400

    refactor(portal): make experiment-summary + statistics serializers 
proto-native (Track D) (#196)
    
    Rewrite BaseExperimentSummarySerializer, ExperimentSummarySerializer, and
    ExperimentStatisticsSerializer to read the gRPC ExperimentSummaryModel /
    ExperimentStatistics protobuf directly, emitting the same Thrift-named JSON.
    
    - Preserve a pre-existing quirk byte-for-byte: the experiment-search 
subclass
      (ExperimentSummarySerializer) renders creationTime/statusUpdateTime as raw
      epoch-millis ints (the old Thrift metaclass regenerated them as 
IntegerField on
      the subclass, shadowing the base's ISO field), while the base used by the
      statistics summary lists renders them as ISO. New ProtoIntOrNoneField 
(raw int,
      proto-0 -> None) backs the subclass.
    - Repoint ExperimentSearchViewSet + ExperimentStatisticsView to pass 
protobuf
      through directly, dropping grpc_adapters.experiment_summary /
      experiment_statistics + the Thrift 
ExperimentSummaryModel/ExperimentStatistics
      imports.
    
    Validated byte-for-byte (search full/min, base, statistics) vs the old
    adapter+serializer path. manage.py check green; api test failures unchanged.
---
 .../django_airavata/apps/api/grpc_adapters.py      | 39 ---------
 .../django_airavata/apps/api/serializers.py        | 95 ++++++++++++++++++----
 .../django_airavata/apps/api/views.py              | 16 ++--
 3 files changed, 84 insertions(+), 66 deletions(-)

diff --git a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py 
b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
index febcb2f20..47c5774ac 100644
--- a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
@@ -239,45 +239,6 @@ def compute_resource(pb):
     )
 
 
-def experiment_summary(pb):
-    """gRPC ``ExperimentSummaryModel`` protobuf -> 
``ExperimentSummarySerializer`` shape."""
-    return SimpleNamespace(
-        experimentId=pb.experiment_id,
-        projectId=pb.project_id,
-        gatewayId=pb.gateway_id,
-        creationTime=pb.creation_time or None,
-        userName=pb.user_name,
-        name=pb.name,
-        description=pb.description,
-        executionId=pb.execution_id,
-        resourceHostId=pb.resource_host_id,
-        experimentStatus=pb.experiment_status,
-        statusUpdateTime=pb.status_update_time or None,
-    )
-
-
-def experiment_statistics(pb):
-    """gRPC ``ExperimentStatistics`` -> ``ExperimentStatisticsSerializer`` 
shape.
-
-    A wrapper of per-state counts plus per-state experiment-summary lists, each
-    list adapted with :func:`experiment_summary`.
-    """
-    return SimpleNamespace(
-        allExperimentCount=pb.all_experiment_count,
-        completedExperimentCount=pb.completed_experiment_count,
-        cancelledExperimentCount=pb.cancelled_experiment_count,
-        failedExperimentCount=pb.failed_experiment_count,
-        createdExperimentCount=pb.created_experiment_count,
-        runningExperimentCount=pb.running_experiment_count,
-        allExperiments=[experiment_summary(s) for s in pb.all_experiments],
-        completedExperiments=[experiment_summary(s) for s in 
pb.completed_experiments],
-        failedExperiments=[experiment_summary(s) for s in 
pb.failed_experiments],
-        cancelledExperiments=[experiment_summary(s) for s in 
pb.cancelled_experiments],
-        createdExperiments=[experiment_summary(s) for s in 
pb.created_experiments],
-        runningExperiments=[experiment_summary(s) for s in 
pb.running_experiments],
-    )
-
-
 def _input_data_object(pb):
     """gRPC ``InputDataObjectType`` -> ``InputDataObjectTypeSerializer`` 
shape."""
     return SimpleNamespace(
diff --git a/airavata-django-portal/django_airavata/apps/api/serializers.py 
b/airavata-django-portal/django_airavata/apps/api/serializers.py
index 9754e7edc..661d7a69d 100644
--- a/airavata-django-portal/django_airavata/apps/api/serializers.py
+++ b/airavata-django-portal/django_airavata/apps/api/serializers.py
@@ -44,9 +44,7 @@ from airavata.model.data.replica.ttypes import (
     DataReplicaLocationModel
 )
 from airavata.model.experiment.ttypes import (
-    ExperimentModel,
-    ExperimentStatistics,
-    ExperimentSummaryModel
+    ExperimentModel
 )
 from airavata.model.group.ttypes import GroupModel, ResourcePermissionType
 from airavata.model.job.ttypes import JobModel
@@ -214,6 +212,23 @@ def proto_enum_name_field(enum_descriptor, 
proto_prefix='', **kwargs):
         proto_prefix=proto_prefix, **kwargs)
 
 
+class ProtoIntOrNoneField(serializers.IntegerField):
+    """Renders a protobuf int field as a raw integer, mapping the proto-0 
default
+    to ``None`` to match the old auto-generated ``IntegerField`` whose adapter 
fed
+    it ``pb.<field> or None``.
+    """
+
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('allow_null', True)
+        kwargs.setdefault('read_only', True)
+        super().__init__(*args, **kwargs)
+
+    def to_representation(self, value):
+        if not value:
+            return None
+        return super().to_representation(value)
+
+
 class StoredJSONField(serializers.JSONField):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -852,25 +867,50 @@ class FullExperimentSerializer(serializers.Serializer):
         raise Exception("Not implemented")
 
 
-class BaseExperimentSummarySerializer(
-        thrift_utils.create_serializer_class(ExperimentSummaryModel)):
-    creationTime = UTCPosixTimestampDateTimeField()
-    statusUpdateTime = UTCPosixTimestampDateTimeField()
+class BaseExperimentSummarySerializer(serializers.Serializer):
+    """Proto-native serializer for the gRPC ``ExperimentSummaryModel`` message.
+
+    Read-only; used directly for the experiment-statistics summary lists (where
+    the timestamps render as ISO strings — see 
:class:`ExperimentSummarySerializer`
+    for the experiment-search variant's int rendering).
+    """
+
+    experimentId = serializers.CharField(source='experiment_id', 
read_only=True)
+    projectId = serializers.CharField(source='project_id', read_only=True)
+    gatewayId = serializers.CharField(source='gateway_id', read_only=True)
+    creationTime = ProtoTimestampField(
+        source='creation_time', null_if_zero=True, read_only=True)
+    userName = serializers.CharField(source='user_name', read_only=True)
+    name = serializers.CharField(read_only=True)
+    description = serializers.CharField(read_only=True)
+    executionId = serializers.CharField(source='execution_id', read_only=True)
+    resourceHostId = serializers.CharField(
+        source='resource_host_id', read_only=True)
+    experimentStatus = serializers.CharField(
+        source='experiment_status', read_only=True)
+    statusUpdateTime = ProtoTimestampField(
+        source='status_update_time', null_if_zero=True, read_only=True)
     url = FullyEncodedHyperlinkedIdentityField(
         view_name='django_airavata_api:experiment-detail',
-        lookup_field='experimentId',
+        lookup_field='experiment_id',
         lookup_url_kwarg='experiment_id')
     project = FullyEncodedHyperlinkedIdentityField(
         view_name='django_airavata_api:project-detail',
-        lookup_field='projectId',
+        lookup_field='project_id',
         lookup_url_kwarg='project_id')
 
 
 class ExperimentSummarySerializer(BaseExperimentSummarySerializer):
+    # The experiment-search list historically rendered these timestamps as raw
+    # epoch-millis ints (the Thrift metaclass regenerated them as IntegerField 
on
+    # this subclass, shadowing the base's ISO field); preserve that exactly.
+    creationTime = ProtoIntOrNoneField(source='creation_time')
+    statusUpdateTime = ProtoIntOrNoneField(source='status_update_time')
     userHasWriteAccess = serializers.SerializerMethodField()
 
     def get_userHasWriteAccess(self, experiment):
-        return user_has_access(self.context['request'], 
experiment.experimentId)
+        return user_has_access(
+            self.context['request'], experiment.experiment_id)
 
 
 class UserProfileSerializer(
@@ -2231,14 +2271,33 @@ class 
NotificationSerializer(thrift_utils.create_serializer_class(Notification))
                 )
 
 
-class ExperimentStatisticsSerializer(
-        thrift_utils.create_serializer_class(ExperimentStatistics)):
-    allExperiments = BaseExperimentSummarySerializer(many=True)
-    completedExperiments = BaseExperimentSummarySerializer(many=True)
-    failedExperiments = BaseExperimentSummarySerializer(many=True)
-    cancelledExperiments = BaseExperimentSummarySerializer(many=True)
-    createdExperiments = BaseExperimentSummarySerializer(many=True)
-    runningExperiments = BaseExperimentSummarySerializer(many=True)
+class ExperimentStatisticsSerializer(serializers.Serializer):
+    """Proto-native serializer for the gRPC ``ExperimentStatistics`` 
message."""
+
+    allExperimentCount = serializers.IntegerField(
+        source='all_experiment_count', read_only=True)
+    completedExperimentCount = serializers.IntegerField(
+        source='completed_experiment_count', read_only=True)
+    cancelledExperimentCount = serializers.IntegerField(
+        source='cancelled_experiment_count', read_only=True)
+    failedExperimentCount = serializers.IntegerField(
+        source='failed_experiment_count', read_only=True)
+    createdExperimentCount = serializers.IntegerField(
+        source='created_experiment_count', read_only=True)
+    runningExperimentCount = serializers.IntegerField(
+        source='running_experiment_count', read_only=True)
+    allExperiments = BaseExperimentSummarySerializer(
+        source='all_experiments', many=True, read_only=True)
+    completedExperiments = BaseExperimentSummarySerializer(
+        source='completed_experiments', many=True, read_only=True)
+    failedExperiments = BaseExperimentSummarySerializer(
+        source='failed_experiments', many=True, read_only=True)
+    cancelledExperiments = BaseExperimentSummarySerializer(
+        source='cancelled_experiments', many=True, read_only=True)
+    createdExperiments = BaseExperimentSummarySerializer(
+        source='created_experiments', many=True, read_only=True)
+    runningExperiments = BaseExperimentSummarySerializer(
+        source='running_experiments', many=True, read_only=True)
 
 
 class UnverifiedEmailUserProfile(serializers.Serializer):
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py 
b/airavata-django-portal/django_airavata/apps/api/views.py
index 3a82a67f6..99be9e76e 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -363,10 +363,9 @@ class ExperimentSearchViewSet(mixins.ListModelMixin, 
GenericAPIBackedViewSet):
 
         class ExperimentSearchResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
-                summaries = view.request.airavata.research.search_experiments(
+                return list(view.request.airavata.research.search_experiments(
                     gateway_id=view.gateway_id, user_name=view.username,
-                    filters=filters, limit=limit, offset=offset)
-                return [grpc_adapters.experiment_summary(s) for s in summaries]
+                    filters=filters, limit=limit, offset=offset))
 
         # Preserve query parameters when moving to next and previous links
         return 
ExperimentSearchResultIterator(query_params=self.request.query_params.copy())
@@ -1996,15 +1995,14 @@ class ExperimentStatisticsView(APIView):
         limit = int(request.GET.get('limit', '50'))
         offset = int(request.GET.get('offset', '0'))
 
-        statistics = grpc_adapters.experiment_statistics(
-            request.airavata.research.get_experiment_statistics(
-                settings.GATEWAY_ID, from_time, to_time,
-                username or "", application_name or "", resource_hostname or 
"",
-                limit, offset))
+        statistics = request.airavata.research.get_experiment_statistics(
+            settings.GATEWAY_ID, from_time, to_time,
+            username or "", application_name or "", resource_hostname or "",
+            limit, offset)
         serializer = self.serializer_class(statistics, context={'request': 
request})
 
         paginator = pagination.LimitOffsetPagination()
-        paginator.count = statistics.allExperimentCount
+        paginator.count = statistics.all_experiment_count
         paginator.limit = limit
         paginator.offset = offset
         paginator.request = request

Reply via email to