Skip to content

Commit 5cfb953

Browse files
[WEB-5312] migration: work item comments (#8072)
* migration: description field on issue_comment sync: issue_comment and description * fix: update if description already exists for the IssueComment * feat: management command to copy IssueComment to Description * fix: description creation order * chore: add while loop * fix: move write outside loop * chore: change sync logic chore: test cases * chore: removed deleted_at filter and added order_by in management command * fix: description_id * migration: added parent_id for IssueComment * fix: update update_by_id * fix: use ChangeTrackerMixin in save * chore: add docstring fix: remove self.pk check chore: wrap the description creation logic in transaction.atomic() * fix: tests * fix: use super save method * fix: mulitple if conditions * fix: update updated_at
1 parent 6a26ce3 commit 5cfb953

File tree

4 files changed

+416
-2
lines changed

4 files changed

+416
-2
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Django imports
2+
from django.core.management.base import BaseCommand
3+
from django.db import transaction
4+
5+
# Module imports
6+
from plane.db.models import Description
7+
from plane.db.models import IssueComment
8+
9+
10+
class Command(BaseCommand):
11+
help = "Create Description records for existing IssueComment"
12+
13+
def handle(self, *args, **kwargs):
14+
batch_size = 500
15+
16+
while True:
17+
comments = list(
18+
IssueComment.objects.filter(description_id__isnull=True).order_by("created_at")[:batch_size]
19+
)
20+
21+
if not comments:
22+
break
23+
24+
with transaction.atomic():
25+
descriptions = [
26+
Description(
27+
created_at=comment.created_at,
28+
updated_at=comment.updated_at,
29+
description_json=comment.comment_json,
30+
description_html=comment.comment_html,
31+
description_stripped=comment.comment_stripped,
32+
project_id=comment.project_id,
33+
created_by_id=comment.created_by_id,
34+
updated_by_id=comment.updated_by_id,
35+
workspace_id=comment.workspace_id,
36+
)
37+
for comment in comments
38+
]
39+
40+
created_descriptions = Description.objects.bulk_create(descriptions)
41+
42+
comments_to_update = []
43+
for comment, description in zip(comments, created_descriptions):
44+
comment.description_id = description.id
45+
comments_to_update.append(comment)
46+
47+
IssueComment.objects.bulk_update(comments_to_update, ["description_id"])
48+
49+
self.stdout.write(self.style.SUCCESS("Successfully Copied IssueComment to Description"))
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.22 on 2025-11-06 08:28
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('db', '0108_alter_issueactivity_issue_comment'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='issuecomment',
16+
name='description',
17+
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue_comment_description', to='db.description'),
18+
),
19+
migrations.AddField(
20+
model_name='issuecomment',
21+
name='parent',
22+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_issue_comment', to='db.issuecomment'),
23+
),
24+
]

apps/api/plane/db/models/issue.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from plane.utils.exception_logger import log_exception
1818
from .project import ProjectBaseModel
1919
from plane.utils.uuid import convert_uuid_to_integer
20+
from .description import Description
21+
from plane.db.mixins import ChangeTrackerMixin
2022

2123

2224
def get_default_properties():
@@ -442,10 +444,13 @@ def __str__(self):
442444
return str(self.issue)
443445

444446

445-
class IssueComment(ProjectBaseModel):
447+
class IssueComment(ChangeTrackerMixin, ProjectBaseModel):
446448
comment_stripped = models.TextField(verbose_name="Comment", blank=True)
447449
comment_json = models.JSONField(blank=True, default=dict)
448450
comment_html = models.TextField(blank=True, default="<p></p>")
451+
description = models.OneToOneField(
452+
"db.Description", on_delete=models.CASCADE, related_name="issue_comment_description", null=True
453+
)
449454
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
450455
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
451456
# System can also create comment
@@ -463,10 +468,60 @@ class IssueComment(ProjectBaseModel):
463468
external_source = models.CharField(max_length=255, null=True, blank=True)
464469
external_id = models.CharField(max_length=255, blank=True, null=True)
465470
edited_at = models.DateTimeField(null=True, blank=True)
471+
parent = models.ForeignKey(
472+
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="parent_issue_comment"
473+
)
474+
475+
TRACKED_FIELDS = ["comment_stripped", "comment_json", "comment_html"]
466476

467477
def save(self, *args, **kwargs):
478+
"""
479+
Custom save method for IssueComment that manages the associated Description model.
480+
481+
This method handles creation and updates of both the comment and its description in a
482+
single atomic transaction to ensure data consistency.
483+
"""
484+
468485
self.comment_stripped = strip_tags(self.comment_html) if self.comment_html != "" else ""
469-
return super(IssueComment, self).save(*args, **kwargs)
486+
is_creating = self._state.adding
487+
488+
# Prepare description defaults
489+
description_defaults = {
490+
"workspace_id": self.workspace_id,
491+
"project_id": self.project_id,
492+
"created_by_id": self.created_by_id,
493+
"updated_by_id": self.updated_by_id,
494+
"description_stripped": self.comment_stripped,
495+
"description_json": self.comment_json,
496+
"description_html": self.comment_html,
497+
}
498+
499+
with transaction.atomic():
500+
super(IssueComment, self).save(*args, **kwargs)
501+
502+
if is_creating or not self.description_id:
503+
# Create new description for new comment
504+
description = Description.objects.create(**description_defaults)
505+
self.description_id = description.id
506+
super(IssueComment, self).save(update_fields=["description_id"])
507+
else:
508+
field_mapping = {
509+
"comment_html": "description_html",
510+
"comment_stripped": "description_stripped",
511+
"comment_json": "description_json",
512+
}
513+
514+
changed_fields = {
515+
desc_field: getattr(self, comment_field)
516+
for comment_field, desc_field in field_mapping.items()
517+
if self.has_changed(comment_field)
518+
}
519+
520+
# Update description only if comment fields changed
521+
if changed_fields and self.description_id:
522+
Description.objects.filter(pk=self.description_id).update(
523+
**changed_fields, updated_by_id=self.updated_by_id, updated_at=self.updated_at
524+
)
470525

471526
class Meta:
472527
verbose_name = "Issue Comment"

0 commit comments

Comments
 (0)