Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5277788be4 |
@@ -71,6 +71,8 @@ from .issue import (
|
||||
IssueReactionLiteSerializer,
|
||||
IssueAttachmentLiteSerializer,
|
||||
IssueLinkLiteSerializer,
|
||||
IssueDescriptionVersionSerializer,
|
||||
IssueDescriptionVersionDetailSerializer,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -41,6 +41,7 @@ from .issue import (
|
||||
IssueSequence,
|
||||
IssueSubscriber,
|
||||
IssueVote,
|
||||
IssueDescriptionVersion
|
||||
)
|
||||
from .module import (
|
||||
Module,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user