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 2e614e6ed refactor(portal): make ProjectSerializer proto-native (Track 
D) (#193)
2e614e6ed is described below

commit 2e614e6ed4e639790f1f6583ea2e5464dbd03fd4
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Jun 9 03:28:15 2026 -0400

    refactor(portal): make ProjectSerializer proto-native (Track D) (#193)
    
    Rewrite ProjectSerializer to read the gRPC Project protobuf directly (proto
    field names: project_id, creation_time, ...) and emit the exact same JSON 
the
    Thrift-generated serializer produced (projectID, creationTime, ...), so the 
REST
    contract with the Vue frontend is byte-for-byte unchanged.
    
    - ProjectSerializer is now a hand-written proto-native DRF serializer; its
      save() returns a proto Project the view passes straight to the gRPC 
facade.
    - Repoint ProjectViewSet (list/instance/create/update/list_all) and
      FullExperimentViewSet's nested project to pass protobuf through directly,
      dropping the grpc_adapters.project / grpc_requests.project roundtrip.
    - Add ProtoTimestampField (proto int64 epoch-millis -> the same ISO 
timestamp,
      with proto-0 -> null where the old field was nullable).
    - Remove the now-unused grpc_adapters.project / grpc_requests.project and 
the
      Thrift Project import.
    
    Validated byte-for-byte: the new serializer's .data equals the old
    (grpc_adapters.project -> Thrift serializer) .data across 
full/empty/no-timestamp/
    non-owner cases; write path produces an equivalent proto. manage.py check 
green;
    api test failures unchanged vs origin/main (6 pre-existing).
---
 .../django_airavata/apps/api/grpc_adapters.py      | 16 -----
 .../django_airavata/apps/api/grpc_requests.py      | 14 ----
 .../django_airavata/apps/api/serializers.py        | 77 +++++++++++++++++-----
 .../django_airavata/apps/api/views.py              | 30 ++++-----
 4 files changed, 74 insertions(+), 63 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 29b08f85c..0c54f8570 100644
--- a/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_adapters.py
@@ -256,22 +256,6 @@ def proto_summary_type(thrift_summary_type):
         _ThriftSummaryType(thrift_summary_type).name)
 
 
-def project(pb):
-    """gRPC ``Project`` protobuf -> ``ProjectSerializer`` (Thrift ``Project``) 
shape."""
-    return SimpleNamespace(
-        projectID=pb.project_id,
-        owner=pb.owner,
-        gatewayId=pb.gateway_id,
-        name=pb.name,
-        description=pb.description,
-        # int64 epoch millis, like the Thrift field; 0 (unset) -> None for the
-        # serializer's allow_null creationTime.
-        creationTime=pb.creation_time or None,
-        sharedUsers=list(pb.shared_users),
-        sharedGroups=list(pb.shared_groups),
-    )
-
-
 def application_module(pb):
     """gRPC ``ApplicationModule`` protobuf -> ``ApplicationModuleSerializer`` 
shape."""
     return SimpleNamespace(
diff --git a/airavata-django-portal/django_airavata/apps/api/grpc_requests.py 
b/airavata-django-portal/django_airavata/apps/api/grpc_requests.py
index 3a5704d0c..292ed88b3 100644
--- a/airavata-django-portal/django_airavata/apps/api/grpc_requests.py
+++ b/airavata-django-portal/django_airavata/apps/api/grpc_requests.py
@@ -68,20 +68,6 @@ def _proto_enum(proto_enum, thrift_enum, value, prefix=''):
     return proto_enum.Value(name)
 
 
-def project(t):
-    """Thrift ``Project`` -> proto ``Project`` request message."""
-    return _workspace_pb2().Project(
-        project_id=t.projectID or '',
-        owner=t.owner or '',
-        gateway_id=t.gatewayId or '',
-        name=t.name or '',
-        description=t.description or '',
-        creation_time=t.creationTime or 0,
-        shared_users=list(t.sharedUsers or []),
-        shared_groups=list(t.sharedGroups or []),
-    )
-
-
 def password_credential(gateway_id, portal_user_name, login_user_name,
                          password, description):
     """Build a proto ``PasswordCredential`` from the create-password 
request."""
diff --git a/airavata-django-portal/django_airavata/apps/api/serializers.py 
b/airavata-django-portal/django_airavata/apps/api/serializers.py
index 257e7ce35..ddc270c00 100644
--- a/airavata-django-portal/django_airavata/apps/api/serializers.py
+++ b/airavata-django-portal/django_airavata/apps/api/serializers.py
@@ -63,8 +63,7 @@ from airavata.model.status.ttypes import (
 from airavata.model.user.ttypes import UserProfile
 from airavata.model.workspace.ttypes import (
     Notification,
-    NotificationPriority,
-    Project
+    NotificationPriority
 )
 from airavata_django_portal_sdk import (
     experiment_util
@@ -145,6 +144,29 @@ class 
UTCPosixTimestampDateTimeField(serializers.DateTimeField):
         return int(datetime.datetime.utcnow().timestamp() * 1000)
 
 
+class ProtoTimestampField(UTCPosixTimestampDateTimeField):
+    """Renders a protobuf int64 epoch-millis field as the same ISO timestamp 
the
+    Thrift-generated serializers produced.
+
+    proto3 scalar int fields default to ``0`` (never None), so an unset 
timestamp
+    reads as ``0``. When ``null_if_zero`` is set the field treats ``0`` as the
+    Thrift ``None`` and renders ``null`` (matching the old ``allow_null`` 
fields
+    whose adapters mapped ``pb.<time> or None``); otherwise ``0`` renders as 
the
+    epoch like the old non-nullable fields did.
+    """
+
+    def __init__(self, *args, null_if_zero=False, **kwargs):
+        self.null_if_zero = null_if_zero
+        if null_if_zero:
+            kwargs.setdefault('allow_null', True)
+        super().__init__(*args, **kwargs)
+
+    def to_representation(self, obj):
+        if self.null_if_zero and not obj:
+            return None
+        return super().to_representation(obj)
+
+
 class StoredJSONField(serializers.JSONField):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -273,35 +295,60 @@ class 
GroupSerializer(thrift_utils.create_serializer_class(GroupModel)):
             }
 
 
-class ProjectSerializer(
-        thrift_utils.create_serializer_class(Project)):
-    class Meta:
-        required = ('name',)
-        read_only = ('owner', 'gatewayId')
+class ProjectSerializer(serializers.Serializer):
+    """Proto-native serializer for the gRPC ``Project`` message.
+
+    Reads protobuf fields directly (``project_id``, ``creation_time``, ...) and
+    emits the historical Thrift-named JSON keys (``projectID``, 
``creationTime``,
+    ...) so the REST contract with the Vue frontend is unchanged. ``save()``
+    returns a proto ``Project`` the view passes straight to the gRPC facade.
+    """
 
+    projectID = serializers.CharField(source='project_id', read_only=True)
+    owner = serializers.CharField(read_only=True)
+    gatewayId = serializers.CharField(source='gateway_id', read_only=True)
+    name = serializers.CharField()
+    description = serializers.CharField(
+        allow_blank=True, allow_null=True, required=False)
+    creationTime = ProtoTimestampField(
+        source='creation_time', null_if_zero=True, read_only=True)
+    sharedUsers = serializers.ListField(
+        source='shared_users', child=serializers.CharField(),
+        read_only=True)
+    sharedGroups = serializers.ListField(
+        source='shared_groups', child=serializers.CharField(),
+        read_only=True)
     url = FullyEncodedHyperlinkedIdentityField(
         view_name='django_airavata_api:project-detail',
-        lookup_field='projectID',
+        lookup_field='project_id',
         lookup_url_kwarg='project_id')
     experiments = FullyEncodedHyperlinkedIdentityField(
         view_name='django_airavata_api:project-experiments',
-        lookup_field='projectID',
+        lookup_field='project_id',
         lookup_url_kwarg='project_id')
-    creationTime = UTCPosixTimestampDateTimeField(allow_null=True)
     userHasWriteAccess = serializers.SerializerMethodField()
     isOwner = serializers.SerializerMethodField()
 
     def create(self, validated_data):
-        return Project(**validated_data)
+        from airavata_sdk.generated.org.apache.airavata.model.workspace import 
(
+            workspace_pb2,
+        )
+        return workspace_pb2.Project(
+            owner=validated_data.get('owner', '') or '',
+            gateway_id=validated_data.get('gateway_id', '') or '',
+            name=validated_data.get('name', '') or '',
+            description=validated_data.get('description', '') or '',
+        )
 
     def update(self, instance, validated_data):
-        instance.name = validated_data.get('name', instance.name)
-        instance.description = validated_data.get(
-            'description', instance.description)
+        if 'name' in validated_data:
+            instance.name = validated_data['name'] or ''
+        if 'description' in validated_data:
+            instance.description = validated_data['description'] or ''
         return instance
 
     def get_userHasWriteAccess(self, project):
-        return user_has_access(self.context['request'], project.projectID)
+        return user_has_access(self.context['request'], project.project_id)
 
     def get_isOwner(self, project):
         request = self.context['request']
diff --git a/airavata-django-portal/django_airavata/apps/api/views.py 
b/airavata-django-portal/django_airavata/apps/api/views.py
index 2de39dd25..5df6fc950 100644
--- a/airavata-django-portal/django_airavata/apps/api/views.py
+++ b/airavata-django-portal/django_airavata/apps/api/views.py
@@ -195,40 +195,35 @@ class ProjectViewSet(APIBackedViewSet):
 
         class ProjectResultIterator(APIResultIterator):
             def get_results(self, limit=-1, offset=0):
-                projects = view.request.airavata.research.get_user_projects(
+                return view.request.airavata.research.get_user_projects(
                     gateway_id=view.gateway_id, user_name=view.username,
                     limit=limit, offset=offset)
-                return [grpc_adapters.project(p) for p in projects]
 
         return ProjectResultIterator()
 
     def get_instance(self, lookup_value):
-        return grpc_adapters.project(
-            self.request.airavata.research.get_project(lookup_value))
+        return self.request.airavata.research.get_project(lookup_value)
 
     def perform_create(self, serializer):
         project = serializer.save(
             owner=self.username,
-            gatewayId=self.gateway_id)
+            gateway_id=self.gateway_id)
         project_id = self.request.airavata.research.create_project(
-            self.gateway_id, grpc_requests.project(project))
-        project.projectID = project_id
+            self.gateway_id, project)
+        project.project_id = project_id
         self._update_most_recent_project(project_id)
 
     def perform_update(self, serializer):
         project = serializer.save()
         self.request.airavata.research.update_project(
-            project.projectID, grpc_requests.project(project))
-        self._update_most_recent_project(project.projectID)
+            project.project_id, project)
+        self._update_most_recent_project(project.project_id)
 
     @action(detail=False)
     def list_all(self, request):
-        projects = [
-            grpc_adapters.project(p)
-            for p in self.request.airavata.research.get_user_projects(
-                gateway_id=self.gateway_id, user_name=self.username,
-                limit=-1, offset=0)
-        ]
+        projects = self.request.airavata.research.get_user_projects(
+            gateway_id=self.gateway_id, user_name=self.username,
+            limit=-1, offset=0)
         serializer = serializers.ProjectSerializer(
             projects, many=True, context={'request': request})
         return Response(serializer.data)
@@ -464,9 +459,8 @@ class FullExperimentViewSet(mixins.RetrieveModelMixin,
             compute_resource = None
         if serializers.user_has_access(
                 self.request, experimentModel.projectId, 'READ'):
-            project = grpc_adapters.project(
-                self.request.airavata.research.get_project(
-                    experimentModel.projectId))
+            project = self.request.airavata.research.get_project(
+                experimentModel.projectId)
         else:
             # User may not have access to project, only experiment
             project = None

Reply via email to