Compare commits

...

1 Commits

Author SHA1 Message Date
NarayanBavisetti 5277788be4 chore: issue description version histroy 2024-11-14 18:17:02 +05:30
12 changed files with 414 additions and 60 deletions
@@ -71,6 +71,8 @@ from .issue import (
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
IssueDescriptionVersionSerializer,
IssueDescriptionVersionDetailSerializer,
)
from .module import (
+44
View File
@@ -33,6 +33,7 @@ from plane.db.models import (
IssueVote,
IssueRelation,
State,
IssueDescriptionVersion
)
@@ -781,3 +782,46 @@ class IssueSubscriberSerializer(BaseSerializer):
"project",
"issue",
]
class IssueDescriptionVersionSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"issue",
"last_saved_at",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"issue",
]
class IssueDescriptionVersionDetailSerializer(BaseSerializer):
class Meta:
model = IssueDescriptionVersion
fields = [
"id",
"workspace",
"issue",
"last_saved_at",
"description_binary",
"description_html",
"description_json",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = [
"workspace",
"issue",
]
+13
View File
@@ -24,6 +24,7 @@ from plane.app.views import (
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
IssueVersionEndpoint,
)
urlpatterns = [
@@ -320,4 +321,16 @@ urlpatterns = [
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
# Issue Description versions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/",
IssueVersionEndpoint.as_view(),
name="issue-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/versions/<uuid:pk>/",
IssueVersionEndpoint.as_view(),
name="issue-versions",
),
## End Issue Description versions
]
+2
View File
@@ -138,6 +138,8 @@ from .issue.activity import (
IssueActivityEndpoint,
)
from .issue.version import IssueVersionEndpoint
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
from .issue.attachment import (
@@ -0,0 +1,39 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import IssueDescriptionVersion
from ..base import BaseAPIView
from plane.app.serializers import (
IssueDescriptionVersionSerializer,
IssueDescriptionVersionDetailSerializer,
)
from plane.app.permissions import allow_permission, ROLE
class IssueVersionEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id, pk=None):
# Check if pk is provided
if pk:
# Return a single issue version
issue_version = IssueDescriptionVersion.objects.get(
workspace__slug=slug,
issue_id=issue_id,
pk=pk,
)
# Serialize the issue version
serializer = IssueDescriptionVersionDetailSerializer(issue_version)
return Response(serializer.data, status=status.HTTP_200_OK)
# Return all issue versions
issue_versions = IssueDescriptionVersion.objects.filter(
workspace__slug=slug,
issue_id=issue_id,
).order_by("-last_saved_at")[:20]
# Serialize the issue versions
serializer = IssueDescriptionVersionSerializer(
issue_versions, many=True
)
return Response(serializer.data, status=status.HTTP_200_OK)
+4 -3
View File
@@ -37,7 +37,7 @@ from plane.db.models import (
from plane.utils.error_codes import ERROR_CODES
from ..base import BaseAPIView, BaseViewSet
from plane.bgtasks.page_transaction_task import page_transaction
from plane.bgtasks.page_version_task import page_version
from plane.bgtasks.version_task import version_task
from plane.bgtasks.recent_visited_task import recent_visited_task
@@ -621,8 +621,9 @@ class PagesDescriptionViewSet(BaseViewSet):
page.description = request.data.get("description")
page.save()
# Return a success response
page_version.delay(
page_id=page.id,
version_task.delay(
entity_type="PAGE",
entity_identifier=page.id,
existing_instance=existing_instance,
user_id=request.user.id,
)
+2 -4
View File
@@ -14,9 +14,7 @@ from plane.app.permissions import allow_permission, ROLE
class PageVersionEndpoint(BaseAPIView):
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]
)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, page_id, pk=None):
# Check if pk is provided
if pk:
@@ -33,7 +31,7 @@ class PageVersionEndpoint(BaseAPIView):
page_versions = PageVersion.objects.filter(
workspace__slug=slug,
page_id=page_id,
)
).order_by("-last_saved_at")[:20]
# Serialize the page versions
serializer = PageVersionSerializer(page_versions, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -1,53 +0,0 @@
# Python imports
import json
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import Page, PageVersion
from plane.utils.exception_logger import log_exception
@shared_task
def page_version(
page_id,
existing_instance,
user_id,
):
try:
# Get the page
page = Page.objects.get(id=page_id)
# Get the current instance
current_instance = (
json.loads(existing_instance)
if existing_instance is not None
else {}
)
# Create a version if description_html is updated
if current_instance.get("description_html") != page.description_html:
# Create a new page version
PageVersion.objects.create(
page_id=page_id,
workspace_id=page.workspace_id,
description_html=page.description_html,
description_binary=page.description_binary,
owned_by_id=user_id,
last_saved_at=page.updated_at,
)
# If page versions are greater than 20 delete the oldest one
if PageVersion.objects.filter(page_id=page_id).count() > 20:
# Delete the old page version
PageVersion.objects.filter(page_id=page_id).order_by(
"last_saved_at"
).first().delete()
return
except Page.DoesNotExist:
return
except Exception as e:
log_exception(e)
return
+148
View File
@@ -0,0 +1,148 @@
# Python imports
import json
# Django imports
from django.utils import timezone
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import Page, PageVersion, Issue, IssueDescriptionVersion
from plane.utils.exception_logger import log_exception
@shared_task
def version_task(
entity_type,
entity_identifier,
existing_instance,
user_id,
):
try:
# Get the current instance
current_instance = (
json.loads(existing_instance)
if existing_instance is not None
else {}
)
if entity_type == "PAGE":
# Get the page
page = Page.objects.get(id=entity_identifier)
# Create a version if description_html is updated
if (
current_instance.get("description_html")
!= page.description_html
):
# Fetch the latest page version
page_version = (
PageVersion.objects.filter(page_id=entity_identifier)
.order_by("-last_saved_at")
.first()
)
# Get the latest page version if it exists and is owned by the user
if (
page_version
and str(page_version.owned_by_id) == str(user_id)
and (
timezone.now() - page_version.last_saved_at
).total_seconds()
<= 600
):
page_version.description_html = page.description_html
page_version.description_binary = page.description_binary
page_version.description_json = page.description
page_version.description_stripped = (
page.description_stripped
)
page_version.last_saved_at = timezone.now()
page_version.save(
update_fields=[
"description_html",
"description_binary",
"description_json",
"description_stripped",
"last_saved_at",
]
)
else:
# Create a new page version
PageVersion.objects.create(
page_id=entity_identifier,
workspace_id=page.workspace_id,
description_html=page.description_html,
description_binary=page.description_binary,
description_stripped=page.description_stripped,
owned_by_id=user_id,
last_saved_at=page.updated_at,
description_json=page.description,
)
if entity_type == "ISSUE":
# Get the issue
issue = Issue.objects.get(id=entity_identifier)
# Create a version if description_html is updated
if (
current_instance.get("description_html")
!= issue.description_html
):
# Fetch the latest issue version
issue_version = (
IssueDescriptionVersion.objects.filter(
issue_id=entity_identifier
)
.order_by("-last_saved_at")
.first()
)
# Get the latest issue version if it exists and is owned by the user
if (
issue_version
and str(issue_version.owned_by_id) == str(user_id)
and (
timezone.now() - issue_version.last_saved_at
).total_seconds()
<= 600
):
issue_version.description_html = issue.description_html
issue_version.description_binary = issue.description_binary
issue_version.description_json = issue.description
issue_version.description_stripped = (
issue.description_stripped
)
issue_version.last_saved_at = timezone.now()
issue_version.save(
update_fields=[
"description_html",
"description_binary",
"description_json",
"description_stripped",
"last_saved_at",
]
)
else:
# Create a new issue version
IssueDescriptionVersion.objects.create(
issue_id=entity_identifier,
workspace_id=issue.workspace_id,
description_html=issue.description_html,
description_binary=issue.description_binary,
description_stripped=issue.description_stripped,
owned_by_id=user_id,
last_saved_at=issue.updated_at,
description_json=issue.description,
)
return
except Issue.DoesNotExist:
return
except Page.DoesNotExist:
return
except Exception as e:
log_exception(e)
return
@@ -0,0 +1,126 @@
# Generated by Django 4.2.15 on 2024-11-09 12:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
dependencies = [
("db", "0086_alter_teammember_unique_together_and_more"),
]
operations = [
migrations.CreateModel(
name="IssueDescriptionVersion",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"deleted_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Deleted At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
(
"last_saved_at",
models.DateTimeField(default=django.utils.timezone.now),
),
("description_binary", models.BinaryField(null=True)),
(
"description_html",
models.TextField(blank=True, default="<p></p>"),
),
(
"description_stripped",
models.TextField(blank=True, null=True),
),
(
"description_json",
models.JSONField(blank=True, default=dict),
),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_description_versions",
to="db.issue",
),
),
(
"owned_by",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue_versions",
to=settings.AUTH_USER_MODEL,
),
),
(
"project",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Issue Description Version",
"verbose_name_plural": "Issue Description Versions",
"db_table": "issue_description_versions",
"ordering": ("-created_at",),
},
),
]
+1
View File
@@ -41,6 +41,7 @@ from .issue import (
IssueSequence,
IssueSubscriber,
IssueVote,
IssueDescriptionVersion
)
from .module import (
Module,
+33
View File
@@ -274,6 +274,39 @@ class IssueBlocker(ProjectBaseModel):
return f"{self.block.name} {self.blocked_by.name}"
class IssueDescriptionVersion(ProjectBaseModel):
issue = models.ForeignKey(
"db.Issue",
on_delete=models.CASCADE,
related_name="issue_description_versions",
)
last_saved_at = models.DateTimeField(default=timezone.now)
owned_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_versions",
)
description_binary = models.BinaryField(null=True)
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
description_json = models.JSONField(default=dict, blank=True)
class Meta:
verbose_name = "Issue Description Version"
verbose_name_plural = "Issue Description Versions"
db_table = "issue_description_versions"
ordering = ("-created_at",)
def save(self, *args, **kwargs):
# Strip the html tags using html parser
self.description_stripped = (
None
if (self.description_html == "" or self.description_html is None)
else strip_tags(self.description_html)
)
super(IssueDescriptionVersion, self).save(*args, **kwargs)
class IssueRelation(ProjectBaseModel):
RELATION_CHOICES = (
("duplicate", "Duplicate"),