From 406f4b4efd3b142497c7db0c66a315eb392daabc Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 15 Oct 2024 14:47:32 +0530 Subject: [PATCH] [MOBIL-382] chore: add Unsplash images and project cover APIs (#1496) * fix: implemented mobile unsplash project covers * fix: updated error handle * fix: project covers --- .../graphql/queries/external/__init__.py | 2 + .../queries/external/project_covers.py | 57 ++++++++ .../graphql/queries/external/unsplash.py | 129 ++++++++++++++++++ apiserver/plane/graphql/schema.py | 3 + apiserver/plane/graphql/types/external.py | 40 ++++++ 5 files changed, 231 insertions(+) create mode 100644 apiserver/plane/graphql/queries/external/__init__.py create mode 100644 apiserver/plane/graphql/queries/external/project_covers.py create mode 100644 apiserver/plane/graphql/queries/external/unsplash.py create mode 100644 apiserver/plane/graphql/types/external.py diff --git a/apiserver/plane/graphql/queries/external/__init__.py b/apiserver/plane/graphql/queries/external/__init__.py new file mode 100644 index 0000000000..9c25a01910 --- /dev/null +++ b/apiserver/plane/graphql/queries/external/__init__.py @@ -0,0 +1,2 @@ +from .unsplash import UnsplashImagesQuery +from .project_covers import ProjectCoversQuery \ No newline at end of file diff --git a/apiserver/plane/graphql/queries/external/project_covers.py b/apiserver/plane/graphql/queries/external/project_covers.py new file mode 100644 index 0000000000..a80ef7ce5c --- /dev/null +++ b/apiserver/plane/graphql/queries/external/project_covers.py @@ -0,0 +1,57 @@ +# Third-Party Imports +import strawberry + +# Python Standard Library Imports +import boto3 +from django.conf import settings + +# Strawberry Imports +from strawberry.types import Info +from strawberry.permission import PermissionExtension + +# Module Imports +from plane.graphql.permissions.workspace import WorkspaceBasePermission +from plane.graphql.types.external import ProjectCovers + + +@strawberry.type +class ProjectCoversQuery: + @strawberry.field( + extensions=[ + PermissionExtension(permissions=[WorkspaceBasePermission()]) + ] + ) + async def project_covers( + self, + slug: str, + info: Info, + ) -> ProjectCovers: + project_cover_files = [] + + if settings.USE_MINIO: + s3 = boto3.client( + "s3", + endpoint_url=settings.AWS_S3_ENDPOINT_URL, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + else: + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + params = { + "Bucket": settings.AWS_STORAGE_BUCKET_NAME, + "Prefix": "static/project-cover/", + } + + response = s3.list_objects_v2(**params) + if "Contents" in response: + for content in response["Contents"]: + if not content["Key"].endswith("/"): + project_cover_files.append( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) + + return ProjectCovers(urls=project_cover_files) diff --git a/apiserver/plane/graphql/queries/external/unsplash.py b/apiserver/plane/graphql/queries/external/unsplash.py new file mode 100644 index 0000000000..d20d594313 --- /dev/null +++ b/apiserver/plane/graphql/queries/external/unsplash.py @@ -0,0 +1,129 @@ +# Third-Party Imports +import strawberry +import requests +import os + +# Python Standard Library Imports +from asgiref.sync import sync_to_async + +# Strawberry Imports +from strawberry.types import Info +from strawberry.permission import PermissionExtension + +# Django Imports +from typing import Optional +from django.conf import settings + +# Module Imports +from plane.graphql.permissions.workspace import WorkspaceBasePermission +from plane.graphql.types.external import UnsplashImagesInfo, UnsplashImages +from plane.license.models import InstanceConfiguration +from plane.license.utils.encryption import decrypt_data + + +@sync_to_async +def get_instance_configuration(): + return list( + InstanceConfiguration.objects.values("key", "value", "is_encrypted") + ) + + +async def get_configuration_value(keys): + environment_list = [] + if settings.SKIP_ENV_VAR: + # Get the configurations + instance_configuration = await get_instance_configuration() + for key in keys: + for item in instance_configuration: + if key.get("key") == item.get("key"): + if item.get("is_encrypted", False): + environment_list.append( + await sync_to_async( + decrypt_data(item.get("value")) + ) + ) + else: + environment_list.append(item.get("value")) + break + else: + environment_list.append(key.get("default")) + else: + # Get the configuration from os + for key in keys: + environment_list.append( + os.environ.get(key.get("key"), key.get("default")) + ) + + return tuple(environment_list) + + +@sync_to_async +def unsplash_request(url, headers): + return requests.get(url, headers=headers) + + +@strawberry.type +class UnsplashImagesQuery: + @strawberry.field( + extensions=[ + PermissionExtension(permissions=[WorkspaceBasePermission()]) + ] + ) + async def unsplash_images( + self, + slug: str, + info: Info, + page: Optional[int] = 1, + per_page: Optional[int] = 20, + query: Optional[str] = None, + ) -> UnsplashImages: + (UNSPLASH_ACCESS_KEY,) = await get_configuration_value( + [ + { + "key": "UNSPLASH_ACCESS_KEY", + "default": os.environ.get("UNSPLASH_ACCESS_KEY"), + } + ] + ) + + # Check unsplash access key + if not UNSPLASH_ACCESS_KEY: + return UnsplashImages(total=None, total_pages=None, urls=[]) + + # construct the unsplash url and headers + unsplash_url = ( + f"https://api.unsplash.com/search/photos/?client_id={UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}" + if query + else f"https://api.unsplash.com/photos/?client_id={UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}" + ) + unsplash_headers = {"Content-Type": "application/json"} + + # make a request to unsplash api + unsplash_response = await unsplash_request( + unsplash_url, unsplash_headers + ) + unsplash_response_data = unsplash_response.json() + + total = None + total_pages = None + if query is not None: + total = unsplash_response_data.get("total") + total_pages = unsplash_response_data.get("total_pages") + unsplash_response_data = unsplash_response_data.get("results") + + unsplash_data = list() + for data in unsplash_response_data: + unsplash_data.append( + UnsplashImagesInfo( + raw=data.get("urls").get("raw"), + full=data.get("urls").get("full"), + regular=data.get("urls").get("regular"), + small=data.get("urls").get("small"), + thumb=data.get("urls").get("thumb"), + small_s3=data.get("urls").get("small_s3"), + ) + ) + + return UnsplashImages( + total=total, total_pages=total_pages, urls=unsplash_data + ) diff --git a/apiserver/plane/graphql/schema.py b/apiserver/plane/graphql/schema.py index f70fd5e905..5fd23d3cf2 100644 --- a/apiserver/plane/graphql/schema.py +++ b/apiserver/plane/graphql/schema.py @@ -47,6 +47,7 @@ from .queries.issues import ( SubIssuesQuery, ) from .queries.dashboard import userInformationQuery +from .queries.external import UnsplashImagesQuery, ProjectCoversQuery # mutations from .mutations.workspace import WorkspaceMutation, WorkspaceInviteMutation @@ -127,6 +128,8 @@ class Query( IssueRelationQuery, IssuesSearchQuery, userInformationQuery, + UnsplashImagesQuery, + ProjectCoversQuery, ): pass diff --git a/apiserver/plane/graphql/types/external.py b/apiserver/plane/graphql/types/external.py new file mode 100644 index 0000000000..f82945f29d --- /dev/null +++ b/apiserver/plane/graphql/types/external.py @@ -0,0 +1,40 @@ +# Python imports +from typing import Optional + +# Strawberry imports +import strawberry + + +@strawberry.type +class ProjectCovers: + urls: list[str] + + +@strawberry.type +class UnsplashPaginatorInfo: + prev_cursor: Optional[int] + cursor: str + next_cursor: Optional[int] + prev_page_results: bool + next_page_results: bool + count: int + total_count: int + + +@strawberry.type +# unsplash images info +class UnsplashImagesInfo: + raw: str + full: str + regular: str + small: str + thumb: str + small_s3: str + + +@strawberry.type +# unsplash images +class UnsplashImages: + total: Optional[int] + total_pages: Optional[int] + urls: list[UnsplashImagesInfo]