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