Compare commits

...

3 Commits

Author SHA1 Message Date
pablohashescobar
6592223ad2 dev: update the presigned url function 2024-06-20 20:06:58 +05:30
pablohashescobar
f7c67e19e0 dev: update external apis 2024-06-20 19:48:22 +05:30
pablohashescobar
675a6d6eea feat: save metadata on issue link creation 2024-06-20 18:37:52 +05:30
6 changed files with 178 additions and 10 deletions

View File

@@ -21,6 +21,7 @@ from plane.db.models import (
ProjectMember, ProjectMember,
State, State,
User, User,
Project,
) )
from .base import BaseSerializer from .base import BaseSerializer
@@ -28,6 +29,8 @@ from .cycle import CycleLiteSerializer, CycleSerializer
from .module import ModuleLiteSerializer, ModuleSerializer from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer from .state import StateLiteSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from plane.utils.metadata import get_metadata
from plane.utils.presigned_url_generator import generate_download_presigned_url
class IssueSerializer(BaseSerializer): class IssueSerializer(BaseSerializer):
@@ -272,6 +275,8 @@ class LabelSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer): class IssueLinkSerializer(BaseSerializer):
metadata = serializers.SerializerMethodField()
class Meta: class Meta:
model = IssueLink model = IssueLink
fields = "__all__" fields = "__all__"
@@ -286,6 +291,12 @@ class IssueLinkSerializer(BaseSerializer):
"updated_at", "updated_at",
] ]
def get_metadata(self, obj):
logo = obj.metadata.get("logo", None)
if logo:
obj.metadata["logo"] = generate_download_presigned_url(logo)
return obj.metadata
def validate_url(self, value): def validate_url(self, value):
# Check URL format # Check URL format
validate_url = URLValidator() validate_url = URLValidator()
@@ -309,17 +320,33 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )
# Workspace
project = Project.objects.get(pk=validated_data.get("project_id"))
# Fetch metadata from URL
validated_data["metadata"] = get_metadata(
validated_data.get("url"), project.workspace_id
)
return IssueLink.objects.create(**validated_data) return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
if IssueLink.objects.filter( if (
url=validated_data.get("url"), IssueLink.objects.filter(
issue_id=instance.issue_id, url=validated_data.get("url"),
).exclude(pk=instance.id).exists(): issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
):
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )
validated_data["metadata"] = get_metadata(
validated_data.get("url"), instance.workspace_id
)
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@@ -33,7 +33,10 @@ from plane.db.models import (
IssueVote, IssueVote,
IssueRelation, IssueRelation,
State, State,
Project,
) )
from plane.utils.metadata import get_metadata
from plane.utils.presigned_url_generator import generate_download_presigned_url
class IssueFlatSerializer(BaseSerializer): class IssueFlatSerializer(BaseSerializer):
@@ -419,6 +422,7 @@ class IssueModuleDetailSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer): class IssueLinkSerializer(BaseSerializer):
created_by_detail = UserLiteSerializer(read_only=True, source="created_by") created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
metadata = serializers.SerializerMethodField()
class Meta: class Meta:
model = IssueLink model = IssueLink
@@ -433,6 +437,12 @@ class IssueLinkSerializer(BaseSerializer):
"issue", "issue",
] ]
def get_metadata(self, obj):
logo = obj.metadata.get("logo", None)
if logo:
obj.metadata["logo"] = generate_download_presigned_url(logo)
return obj.metadata
def validate_url(self, value): def validate_url(self, value):
# Check URL format # Check URL format
validate_url = URLValidator() validate_url = URLValidator()
@@ -449,6 +459,7 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
# Check if URL already exists for this Issue
if IssueLink.objects.filter( if IssueLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"), issue_id=validated_data.get("issue_id"),
@@ -456,17 +467,32 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )
# Workspace
project = Project.objects.get(pk=validated_data.get("project_id"))
# Fetch metadata from URL
validated_data["metadata"] = get_metadata(
validated_data.get("url"), project.workspace_id
)
return IssueLink.objects.create(**validated_data) return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
if IssueLink.objects.filter( if (
url=validated_data.get("url"), IssueLink.objects.filter(
issue_id=instance.issue_id, url=validated_data.get("url"),
).exclude(pk=instance.id).exists(): issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
):
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )
validated_data["metadata"] = get_metadata(
validated_data.get("url"), instance.workspace_id
)
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@@ -90,7 +90,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
# Generate presigned url for the uploaded file with different base # Generate presigned url for the uploaded file with different base
presign_s3 = boto3.client( presign_s3 = boto3.client(
"s3", "s3",
endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace('/uploads', '')}/", endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace(settings.AWS_STORAGE_BUCKET_NAME, '')}/",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"), config=Config(signature_version="s3v4"),

View File

@@ -0,0 +1,67 @@
# Python imports
import requests
import uuid
# Django imports
from django.core.files.base import ContentFile
# Third party imports
from bs4 import BeautifulSoup
import favicon
# Module imports
from plane.db.models import FileAsset
def get_metadata(url, workspace_id):
try:
# Send a GET request to the URL
response = requests.get(url)
response.raise_for_status() # Raise an HTTPError for bad responses
# Parse the HTML content
soup = BeautifulSoup(response.content, "html.parser")
# Extract metadata
metadata = {
"title": soup.title.string if soup.title else "N/A",
"description": "",
"logo": "",
}
# Extract meta tags
meta_tags = soup.find_all("meta")
for tag in meta_tags:
if "name" in tag.attrs:
if tag.attrs["name"].lower() == "description":
metadata["description"] = tag.attrs["content"]
elif tag.attrs["name"].lower() == "keywords":
metadata["keywords"] = tag.attrs["content"]
elif (
"property" in tag.attrs
and tag.attrs["property"].lower() == "og:description"
):
metadata["description"] = tag.attrs["content"]
# Extract favicon
icons = favicon.get(url, timeout=3)
# Download the favicon
if icons:
favicon_response = requests.get(icons[0].url)
content = ContentFile(
favicon_response.content,
name=uuid.uuid4().hex,
)
# Save the favicon as an asset
asset = FileAsset.objects.create(
asset=content,
attributes={"type": "favicon"},
workspace_id=workspace_id,
)
metadata["logo"] = str(asset.asset)
return metadata
except requests.exceptions.RequestException as e:
return {}

View File

@@ -0,0 +1,47 @@
import boto3
from django.conf import settings
from botocore.client import Config
def generate_download_presigned_url(
key,
expiration=3600,
content_type="image/jpeg",
):
"""
Generate a presigned URL to download an object from S3, dynamically setting
the Content-Disposition based on the file metadata.
"""
# Create a new S3 client
if settings.USE_MINIO:
s3_client = boto3.client(
"s3",
endpoint_url=f"{settings.AWS_S3_URL_PROTOCOL}//{str(settings.AWS_S3_CUSTOM_DOMAIN).replace(settings.AWS_STORAGE_BUCKET_NAME, '')}/",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
else:
s3_client = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
region_name=settings.AWS_REGION,
)
try:
# Generate a presigned URL for the object
url = s3_client.generate_presigned_url(
"get_object",
Params={
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Key": key,
"ResponseContentType": content_type,
},
ExpiresIn=expiration,
)
# Return the presigned URL
return url
except Exception:
return ""

View File

@@ -61,4 +61,5 @@ zxcvbn==4.4.28
pytz==2024.1 pytz==2024.1
# jwt # jwt
PyJWT==2.8.0 PyJWT==2.8.0
# favicon
favicon==0.7.0