diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index e087a36597d..01f4068eeed 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -58,6 +58,7 @@ from plane.bgtasks.recent_visited_task import recent_visited_task from plane.utils.global_paginator import paginate from plane.bgtasks.webhook_task import model_activity +from plane.bgtasks.issue_description_version_task import issue_description_version_task class IssueListEndpoint(BaseAPIView): @@ -428,6 +429,13 @@ def create(self, request, slug, project_id): slug=slug, origin=request.META.get("HTTP_ORIGIN"), ) + # updated issue description version + issue_description_version_task.delay( + updated_issue=json.dumps(request.data, cls=DjangoJSONEncoder), + issue_id=str(serializer.data["id"]), + user_id=request.user.id, + is_creating=True, + ) return Response(issue, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -649,6 +657,12 @@ def partial_update(self, request, slug, project_id, pk=None): slug=slug, origin=request.META.get("HTTP_ORIGIN"), ) + # updated issue description version + issue_description_version_task.delay( + updated_issue=current_instance, + issue_id=str(serializer.data.get("id", None)), + user_id=request.user.id, + ) return Response(status=status.HTTP_204_NO_CONTENT) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/bgtasks/issue_description_version_task.py b/apiserver/plane/bgtasks/issue_description_version_task.py new file mode 100644 index 00000000000..a29fb6c5727 --- /dev/null +++ b/apiserver/plane/bgtasks/issue_description_version_task.py @@ -0,0 +1,84 @@ +from celery import shared_task +from django.db import transaction +from django.utils import timezone +from typing import Optional, Dict +import json + +from plane.db.models import Issue, IssueDescriptionVersion +from plane.utils.exception_logger import log_exception + + +def should_update_existing_version( + version: IssueDescriptionVersion, user_id: str, max_time_difference: int = 600 +) -> bool: + if not version: + return + + time_difference = (timezone.now() - version.last_saved_at).total_seconds() + return ( + str(version.owned_by_id) == str(user_id) + and time_difference <= max_time_difference + ) + + +def update_existing_version(version: IssueDescriptionVersion, issue) -> None: + version.description_json = issue.description + version.description_html = issue.description_html + version.description_binary = issue.description_binary + version.description_stripped = issue.description_stripped + version.last_saved_at = timezone.now() + + version.save( + update_fields=[ + "description_json", + "description_html", + "description_binary", + "description_stripped", + "last_saved_at", + ] + ) + + +@shared_task +def issue_description_version_task( + updated_issue, issue_id, user_id, is_creating=False +) -> Optional[bool]: + try: + # Parse updated issue data + current_issue: Dict = json.loads(updated_issue) if updated_issue else {} + + # Get current issue + issue = Issue.objects.get(id=issue_id) + + # Check if description has changed + if ( + current_issue.get("description_html") == issue.description_html + and not is_creating + ): + return + + with transaction.atomic(): + # Get latest version + latest_version = ( + IssueDescriptionVersion.objects.filter(issue_id=issue_id) + .order_by("-last_saved_at") + .first() + ) + + # Determine whether to update existing or create new version + if should_update_existing_version(version=latest_version, user_id=user_id): + update_existing_version(latest_version, issue) + else: + IssueDescriptionVersion.log_issue_description_version(issue, user_id) + + return + + except Issue.DoesNotExist: + # Issue no longer exists, skip processing + return + except json.JSONDecodeError as e: + log_exception(f"Invalid JSON for updated_issue: {e}") + return + except Exception as e: + log_exception(f"Error processing issue description version: {e}") + return diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 8d1a3f320d2..ca7347ad792 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -804,13 +804,17 @@ def log_issue_description_version(cls, issue, user): Log the issue description version """ cls.objects.create( - issue=issue, + workspace_id=issue.workspace_id, + project_id=issue.project_id, + created_by_id=issue.created_by_id, + updated_by_id=issue.updated_by_id, + owned_by_id=user, + last_saved_at=timezone.now(), + issue_id=issue.id, description_binary=issue.description_binary, description_html=issue.description_html, description_stripped=issue.description_stripped, description_json=issue.description, - last_saved_at=timezone.now(), - owned_by=user, ) return True except Exception as e: diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index ed42dfe19c5..db9a244537f 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -262,6 +262,9 @@ "plane.license.bgtasks.tracer", # management tasks "plane.bgtasks.dummy_data_task", + # issue version tasks + "plane.bgtasks.issue_version_sync", + "plane.bgtasks.issue_description_version_sync", ) # Sentry Settings