[WEB-1435] dev: conflict free issue descriptions (#5912)
* chore: new description binary endpoints * chore: conflict free issue description * chore: fix submitting status * chore: update yjs utils * chore: handle component re-mounting * chore: update buffer response type * chore: add try catch for issue description update * chore: update buffer response type * chore: description binary in retrieve * chore: update issue description hook * chore: decode description binary * chore: migrations fixes and cleanup * chore: migration fixes * fix: inbox issue description * chore: move update operations to the issue store * fix: merge conflicts * chore: reverted the commit * chore: removed the unwanted imports * chore: remove unnecessary props * chore: remove unused services * chore: update live server error handling --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
parent
229610513a
commit
e9680cab74
65 changed files with 1466 additions and 358 deletions
|
|
@ -284,9 +284,11 @@ class DraftIssueSerializer(BaseSerializer):
|
|||
|
||||
class DraftIssueDetailSerializer(DraftIssueSerializer):
|
||||
description_html = serializers.CharField()
|
||||
description_binary = serializers.CharField()
|
||||
|
||||
class Meta(DraftIssueSerializer.Meta):
|
||||
fields = DraftIssueSerializer.Meta.fields + [
|
||||
"description_html",
|
||||
"description_binary",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
# Python imports
|
||||
import base64
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.validators import URLValidator
|
||||
|
|
@ -732,14 +735,31 @@ class IssueLiteSerializer(DynamicBaseSerializer):
|
|||
read_only_fields = fields
|
||||
|
||||
|
||||
class Base64BinaryField(serializers.CharField):
|
||||
def to_representation(self, value):
|
||||
# Encode the binary data to base64 string for JSON response
|
||||
if value:
|
||||
return base64.b64encode(value).decode("utf-8")
|
||||
return None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# Decode the base64 string to binary data when saving
|
||||
try:
|
||||
return base64.b64decode(data)
|
||||
except (TypeError, ValueError):
|
||||
raise serializers.ValidationError("Invalid base64-encoded data")
|
||||
|
||||
|
||||
class IssueDetailSerializer(IssueSerializer):
|
||||
description_html = serializers.CharField()
|
||||
description_binary = Base64BinaryField()
|
||||
is_subscribed = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta(IssueSerializer.Meta):
|
||||
fields = IssueSerializer.Meta.fields + [
|
||||
"description_html",
|
||||
"is_subscribed",
|
||||
"description_binary",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
|||
|
||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||
draft_issue_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMember
|
||||
fields = "__all__"
|
||||
|
|
|
|||
|
|
@ -92,4 +92,14 @@ urlpatterns = [
|
|||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/description/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve_description",
|
||||
"post": "update_description",
|
||||
}
|
||||
),
|
||||
name="inbox-issue-description",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -66,6 +66,16 @@ urlpatterns = [
|
|||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/description/",
|
||||
IssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve_description",
|
||||
"post": "update_description",
|
||||
}
|
||||
),
|
||||
name="project-issue-description",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||
LabelViewSet.as_view(
|
||||
|
|
@ -288,6 +298,15 @@ urlpatterns = [
|
|||
),
|
||||
name="project-issue-archive-unarchive",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/description/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve_description",
|
||||
}
|
||||
),
|
||||
name="archive-issue-description",
|
||||
),
|
||||
## End Issue Archives
|
||||
## Issue Relation
|
||||
path(
|
||||
|
|
|
|||
|
|
@ -276,6 +276,16 @@ urlpatterns = [
|
|||
),
|
||||
name="workspace-drafts-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/draft-issues/<uuid:pk>/description/",
|
||||
WorkspaceDraftIssueViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve_description",
|
||||
"post": "update_description",
|
||||
}
|
||||
),
|
||||
name="workspace-drafts-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/draft-to-issue/<uuid:draft_id>/",
|
||||
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Python imports
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
|
||||
# Django import
|
||||
from django.utils import timezone
|
||||
|
|
@ -9,6 +11,9 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
|||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Value, UUIDField
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
|
|
@ -40,7 +45,6 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
|||
|
||||
|
||||
class IntakeViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = IntakeSerializer
|
||||
model = Intake
|
||||
|
||||
|
|
@ -89,7 +93,6 @@ class IntakeViewSet(BaseViewSet):
|
|||
|
||||
|
||||
class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = IntakeIssueSerializer
|
||||
model = IntakeIssue
|
||||
|
||||
|
|
@ -640,3 +643,82 @@ class IntakeIssueViewSet(BaseViewSet):
|
|||
|
||||
intake_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def retrieve_description(self, request, slug, project_id, pk):
|
||||
issue = Issue.objects.filter(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
if issue is None:
|
||||
return Response(
|
||||
{"error": "Issue not found"},
|
||||
status=404,
|
||||
)
|
||||
binary_data = issue.description_binary
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def update_description(self, request, slug, project_id, pk):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
base64_description = issue.description_binary
|
||||
# convert to base64 string
|
||||
if base64_description:
|
||||
base64_description = base64.b64encode(base64_description).decode(
|
||||
"utf-8"
|
||||
)
|
||||
data = {
|
||||
"original_document": base64_description,
|
||||
"updates": request.data.get("description_binary"),
|
||||
}
|
||||
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
|
||||
try:
|
||||
response = requests.post(base_url, json=data, headers=None)
|
||||
except requests.RequestException:
|
||||
return Response(
|
||||
{"error": "Failed to connect to the external service"},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
issue.description = response.json().get(
|
||||
"description", issue.description
|
||||
)
|
||||
issue.description_html = response.json().get("description_html")
|
||||
response_description_binary = response.json().get(
|
||||
"description_binary"
|
||||
)
|
||||
issue.description_binary = base64.b64decode(
|
||||
response_description_binary
|
||||
)
|
||||
issue.save()
|
||||
|
||||
def stream_data():
|
||||
if issue.description_binary:
|
||||
yield issue.description_binary
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery
|
|||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
|
|
@ -27,7 +29,7 @@ from plane.db.models import (
|
|||
IssueLink,
|
||||
IssueSubscriber,
|
||||
IssueReaction,
|
||||
CycleIssue
|
||||
CycleIssue,
|
||||
)
|
||||
from plane.utils.grouper import (
|
||||
issue_group_values,
|
||||
|
|
@ -327,6 +329,32 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def retrieve_description(self, request, slug, project_id, pk):
|
||||
issue = Issue.objects.filter(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
if issue is None:
|
||||
return Response(
|
||||
{"error": "Issue not found"},
|
||||
status=404,
|
||||
)
|
||||
binary_data = issue.description_binary
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Python imports
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
|
|
@ -20,8 +22,10 @@ from django.db.models import (
|
|||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.conf import settings
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
|
|
@ -725,6 +729,84 @@ class IssueViewSet(BaseViewSet):
|
|||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def retrieve_description(self, request, slug, project_id, pk):
|
||||
issue = Issue.issue_objects.filter(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
if issue is None:
|
||||
return Response(
|
||||
{"error": "Issue not found"},
|
||||
status=404,
|
||||
)
|
||||
binary_data = issue.description_binary
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
def update_description(self, request, slug, project_id, pk):
|
||||
issue = Issue.issue_objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
base64_description = issue.description_binary
|
||||
# convert to base64 string
|
||||
if base64_description:
|
||||
base64_description = base64.b64encode(base64_description).decode(
|
||||
"utf-8"
|
||||
)
|
||||
data = {
|
||||
"original_document": base64_description,
|
||||
"updates": request.data.get("description_binary"),
|
||||
}
|
||||
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
|
||||
try:
|
||||
response = requests.post(base_url, json=data, headers=None)
|
||||
except requests.RequestException:
|
||||
return Response(
|
||||
{"error": "Failed to connect to the external service"},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
issue.description = response.json().get(
|
||||
"description", issue.description
|
||||
)
|
||||
issue.description_html = response.json().get("description_html")
|
||||
response_description_binary = response.json().get(
|
||||
"description_binary"
|
||||
)
|
||||
issue.description_binary = base64.b64decode(
|
||||
response_description_binary
|
||||
)
|
||||
issue.save()
|
||||
|
||||
def stream_data():
|
||||
if issue.description_binary:
|
||||
yield issue.description_binary
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Python imports
|
||||
import json
|
||||
import requests
|
||||
import base64
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
|
@ -7,6 +9,7 @@ from django.core import serializers
|
|||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.db.models import (
|
||||
Q,
|
||||
UUIDField,
|
||||
|
|
@ -17,6 +20,7 @@ from django.db.models import (
|
|||
from django.db.models.functions import Coalesce
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.conf import settings
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework import status
|
||||
|
|
@ -350,3 +354,78 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
|||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def retrieve_description(self, request, slug, pk):
|
||||
issue = DraftIssue.objects.filter(pk=pk, workspace__slug=slug).first()
|
||||
if issue is None:
|
||||
return Response(
|
||||
{"error": "Issue not found"},
|
||||
status=404,
|
||||
)
|
||||
binary_data = issue.description_binary
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="draft_issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def update_description(self, request, slug, pk):
|
||||
issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk)
|
||||
base64_description = issue.description_binary
|
||||
# convert to base64 string
|
||||
if base64_description:
|
||||
base64_description = base64.b64encode(base64_description).decode(
|
||||
"utf-8"
|
||||
)
|
||||
data = {
|
||||
"original_document": base64_description,
|
||||
"updates": request.data.get("description_binary"),
|
||||
}
|
||||
base_url = f"{settings.LIVE_BASE_URL}/resolve-document-conflicts/"
|
||||
try:
|
||||
response = requests.post(base_url, json=data, headers=None)
|
||||
except requests.RequestException:
|
||||
return Response(
|
||||
{"error": "Failed to connect to the external service"},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
issue.description = response.json().get(
|
||||
"description", issue.description
|
||||
)
|
||||
issue.description_html = response.json().get("description_html")
|
||||
response_description_binary = response.json().get(
|
||||
"description_binary"
|
||||
)
|
||||
issue.description_binary = base64.b64decode(
|
||||
response_description_binary
|
||||
)
|
||||
issue.save()
|
||||
|
||||
def stream_data():
|
||||
if issue.description_binary:
|
||||
yield issue.description_binary
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="issue_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
# Generated by Django 4.2.15 on 2024-11-06 08:41
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
|||
|
|
@ -381,6 +381,7 @@ CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
|
|||
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
||||
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
|
||||
|
||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from django.urls import path
|
|||
|
||||
from plane.space.views import (
|
||||
IntakeIssuePublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue