From fc486df312aec804bdd8dc0ca31bfef6db218d60 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 11 Feb 2025 13:51:35 +0530 Subject: [PATCH] [MOBIL-766] fix: convert created_at and updated_at to user timezone in response (#2334) * fix: conveted the created-at and updated_at based on the user timezone in the response * chore: created_at and updated_at user timezone converter --- apiserver/plane/graphql/types/asset/base.py | 19 ++++++ apiserver/plane/graphql/types/cycle.py | 4 +- apiserver/plane/graphql/types/issue.py | 59 +++++++++++++++++-- apiserver/plane/graphql/types/issues/link.py | 15 ++++- apiserver/plane/graphql/types/module.py | 23 +++++++- apiserver/plane/graphql/types/notification.py | 15 ++++- apiserver/plane/graphql/types/page.py | 15 ++++- apiserver/plane/graphql/types/users.py | 25 +++++++- .../plane/graphql/utils/timezone/__init__.py | 1 + .../plane/graphql/utils/timezone/user.py | 36 +++++++++++ 10 files changed, 194 insertions(+), 18 deletions(-) create mode 100644 apiserver/plane/graphql/utils/timezone/__init__.py create mode 100644 apiserver/plane/graphql/utils/timezone/user.py diff --git a/apiserver/plane/graphql/types/asset/base.py b/apiserver/plane/graphql/types/asset/base.py index 0f856fffa2..3098cf2f5a 100644 --- a/apiserver/plane/graphql/types/asset/base.py +++ b/apiserver/plane/graphql/types/asset/base.py @@ -9,6 +9,7 @@ import strawberry_django from strawberry.scalars import JSON # module imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import FileAsset @@ -83,6 +84,24 @@ class FileAssetType: # model properties asset_url: Optional[str] + @strawberry.field + def created_by(self) -> Optional[strawberry.ID]: + return self.created_by_id + + @strawberry.field + def updated_by(self) -> Optional[strawberry.ID]: + return self.updated_by_id + + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry.type class AssetPresignedUrlResponseType: diff --git a/apiserver/plane/graphql/types/cycle.py b/apiserver/plane/graphql/types/cycle.py index eda341bc47..1a25a2a5fe 100644 --- a/apiserver/plane/graphql/types/cycle.py +++ b/apiserver/plane/graphql/types/cycle.py @@ -36,8 +36,8 @@ class CycleType: workspace: strawberry.ID created_by: Optional[strawberry.ID] updated_by: Optional[strawberry.ID] - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] total_issues: int completed_issues: int is_favorite: bool diff --git a/apiserver/plane/graphql/types/issue.py b/apiserver/plane/graphql/types/issue.py index 93f7a977e7..5fad5fef1a 100644 --- a/apiserver/plane/graphql/types/issue.py +++ b/apiserver/plane/graphql/types/issue.py @@ -12,6 +12,7 @@ from strawberry.scalars import JSON from strawberry.types import Info # Module Imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import ( Issue, IssueLabel, @@ -64,8 +65,8 @@ class IssuesType: external_id: Optional[str] created_by: Optional[strawberry.ID] updated_by: Optional[strawberry.ID] - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] cycle: Optional[strawberry.ID] modules: Optional[list[strawberry.ID]] type: Optional[strawberry.ID] @@ -169,6 +170,24 @@ class IssuesType: def parent_project_identifier(self) -> Optional[str]: return self.parent.project.identifier if self.parent else None + @strawberry.field + def created_by(self) -> Optional[strawberry.ID]: + return self.created_by_id + + @strawberry.field + def updated_by(self) -> Optional[strawberry.ID]: + return self.updated_by_id + + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry_django.type(IssueUserProperty) class IssueUserPropertyType: @@ -210,8 +229,8 @@ class IssuePropertyActivityType: epoch: float workspace: strawberry.ID project: strawberry.ID - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] @strawberry.field def workspace(self) -> int: @@ -233,6 +252,16 @@ class IssuePropertyActivityType: def issue(self) -> int: return self.issue_id + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry_django.type(IssueComment) class IssueCommentActivityType: @@ -248,8 +277,8 @@ class IssueCommentActivityType: external_id: Optional[str] workspace: strawberry.ID project: strawberry.ID - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] @strawberry.field def workspace(self) -> int: @@ -267,6 +296,24 @@ class IssueCommentActivityType: def issue(self) -> int: return self.issue_id + @strawberry.field + def created_by(self) -> Optional[strawberry.ID]: + return self.created_by_id + + @strawberry.field + def updated_by(self) -> Optional[strawberry.ID]: + return self.updated_by_id + + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry_django.type(Issue) class IssueLiteType: diff --git a/apiserver/plane/graphql/types/issues/link.py b/apiserver/plane/graphql/types/issues/link.py index 4c9f85f638..f83e1e61bd 100644 --- a/apiserver/plane/graphql/types/issues/link.py +++ b/apiserver/plane/graphql/types/issues/link.py @@ -7,6 +7,7 @@ import strawberry import strawberry_django # Module Imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import IssueLink @@ -15,8 +16,8 @@ class IssueLinkType: id: strawberry.ID title: str url: str - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] @strawberry.field def workspace(self) -> strawberry.ID: @@ -37,3 +38,13 @@ class IssueLinkType: @strawberry.field def updated_by(self) -> Optional[strawberry.ID]: return self.updated_by_id if self.created_by_id else None + + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date diff --git a/apiserver/plane/graphql/types/module.py b/apiserver/plane/graphql/types/module.py index 69d6f9cea8..e50b683c2d 100644 --- a/apiserver/plane/graphql/types/module.py +++ b/apiserver/plane/graphql/types/module.py @@ -9,6 +9,7 @@ from strawberry.types import Info from strawberry.scalars import JSON # Module Imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import Module, Issue, ModuleUserProperties from plane.graphql.types.users import UserType @@ -25,8 +26,8 @@ class ModuleType: workspace: strawberry.ID created_by: Optional[strawberry.ID] updated_by: Optional[strawberry.ID] - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] description: Optional[str] description_text: Optional[str] description_html: Optional[str] @@ -86,6 +87,24 @@ class ModuleType: )() return issue_assignees_count + @strawberry.field + def created_by(self) -> Optional[strawberry.ID]: + return self.created_by_id + + @strawberry.field + def updated_by(self) -> Optional[strawberry.ID]: + return self.updated_by_id + + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry_django.type(Module) class ModuleLiteType: diff --git a/apiserver/plane/graphql/types/notification.py b/apiserver/plane/graphql/types/notification.py index b25d94e034..ed98cb18ce 100644 --- a/apiserver/plane/graphql/types/notification.py +++ b/apiserver/plane/graphql/types/notification.py @@ -8,6 +8,7 @@ import strawberry_django from strawberry.scalars import JSON # Module imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import Notification from plane.graphql.types.users import UserType from plane.graphql.types.project import ProjectType @@ -31,8 +32,8 @@ class NotificationType: data: JSON workspace: strawberry.ID project: Optional[ProjectType] - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] @strawberry.field def workspace(self) -> int: @@ -41,3 +42,13 @@ class NotificationType: @strawberry.field def receiver(self) -> int: return self.receiver_id + + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date diff --git a/apiserver/plane/graphql/types/page.py b/apiserver/plane/graphql/types/page.py index ecab1ae49f..43d3962d21 100644 --- a/apiserver/plane/graphql/types/page.py +++ b/apiserver/plane/graphql/types/page.py @@ -11,6 +11,7 @@ import strawberry_django from strawberry.scalars import JSON # Module imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import Page from plane.graphql.types.project import ProjectLiteType @@ -35,8 +36,8 @@ class PageType: logo_props: JSON is_global: bool is_favorite: bool - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] projects: list[strawberry.ID] # teams: list[strawberry.ID] # labels: list[strawberry.ID] @@ -62,6 +63,16 @@ class PageType: projects = await sync_to_async(list)(self.projects.all()) return [project.id for project in projects] + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + # @strawberry.field # async def labels(self) -> list[strawberry.ID]: # labels = await sync_to_async(list)(self.labels.all()) diff --git a/apiserver/plane/graphql/types/users.py b/apiserver/plane/graphql/types/users.py index 9b395fd29e..cce481b16e 100644 --- a/apiserver/plane/graphql/types/users.py +++ b/apiserver/plane/graphql/types/users.py @@ -10,6 +10,7 @@ from strawberry.types import Info from strawberry.scalars import JSON # Module imports +from plane.graphql.utils.timezone import user_timezone_converter from plane.db.models import ( User, Profile, @@ -119,8 +120,8 @@ class UserFavoriteType: is_folder: bool sequence: float parent: Optional[strawberry.ID] - created_at: datetime - updated_at: datetime + created_at: Optional[datetime] + updated_at: Optional[datetime] deleted_at: Optional[datetime] project: Optional[strawberry.ID] @@ -128,6 +129,16 @@ class UserFavoriteType: def project(self) -> int: return self.project_id + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry.field async def entity_data(self) -> Optional[UserFavoriteEntityData]: # where entity_identifier is project_id and entity_type is project @@ -216,6 +227,16 @@ class UserRecentVisitType: def user(self) -> int: return self.user_id + @strawberry.field + def created_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.created_at) + return converted_date + + @strawberry.field + def updated_at(self, info) -> Optional[datetime]: + converted_date = user_timezone_converter(info.context.user, self.updated_at) + return converted_date + @strawberry.field async def entity_data(self) -> Optional[UserFavoriteEntityData]: # where entity_identifier is project_id and entity_name is project diff --git a/apiserver/plane/graphql/utils/timezone/__init__.py b/apiserver/plane/graphql/utils/timezone/__init__.py new file mode 100644 index 0000000000..68767b27aa --- /dev/null +++ b/apiserver/plane/graphql/utils/timezone/__init__.py @@ -0,0 +1 @@ +from .user import user_timezone_converter diff --git a/apiserver/plane/graphql/utils/timezone/user.py b/apiserver/plane/graphql/utils/timezone/user.py new file mode 100644 index 0000000000..1236c6472c --- /dev/null +++ b/apiserver/plane/graphql/utils/timezone/user.py @@ -0,0 +1,36 @@ +# Python imports +import pytz +from datetime import datetime, date + +# Third-party imports +from asgiref.sync import sync_to_async +from django.core.exceptions import ObjectDoesNotExist + +# Module imports +from plane.db.models import User + + +async def user_timezone_converter(user, input_date=None): + if user is None or input_date is None: + return None + + try: + current_user = await sync_to_async(User.objects.get)(id=user.id) + except ObjectDoesNotExist: + return input_date + + user_timezone = current_user.user_timezone + if not user_timezone: + return input_date + + try: + tz = pytz.timezone(user_timezone) + except pytz.UnknownTimeZoneError: + return input_date + + if isinstance(input_date, datetime): + return input_date.astimezone(tz) + elif isinstance(input_date, date): + return datetime.combine(input_date, datetime.min.time()).astimezone(tz) + else: + return input_date