diff --git a/apps/api/plane/db/migrations/0101_description_descriptionversion.py b/apps/api/plane/db/migrations/0101_description_descriptionversion.py new file mode 100644 index 00000000000..fca305c397b --- /dev/null +++ b/apps/api/plane/db/migrations/0101_description_descriptionversion.py @@ -0,0 +1,182 @@ +# Generated by Django 4.2.21 on 2025-08-19 11:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0100_profile_has_marketing_email_consent_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Description", + 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, + ), + ), + ("description_json", models.JSONField(blank=True, default=dict)), + ("description_html", models.TextField(blank=True, default="

")), + ("description_binary", models.BinaryField(null=True)), + ("description_stripped", models.TextField(blank=True, null=True)), + ( + "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", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + 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": "Description", + "verbose_name_plural": "Descriptions", + "db_table": "descriptions", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="DescriptionVersion", + 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, + ), + ), + ("description_json", models.JSONField(blank=True, default=dict)), + ("description_html", models.TextField(blank=True, default="

")), + ("description_binary", models.BinaryField(null=True)), + ("description_stripped", models.TextField(blank=True, null=True)), + ( + "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", + ), + ), + ( + "description", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="versions", + to="db.description", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + 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": "Description Version", + "verbose_name_plural": "Description Versions", + "db_table": "description_versions", + "ordering": ("-created_at",), + }, + ), + ] diff --git a/apps/api/plane/db/models/__init__.py b/apps/api/plane/db/models/__init__.py index 3cf46c91946..de8af54e427 100644 --- a/apps/api/plane/db/models/__init__.py +++ b/apps/api/plane/db/models/__init__.py @@ -83,3 +83,5 @@ from .device import Device, DeviceSession from .sticky import Sticky + +from .description import Description, DescriptionVersion \ No newline at end of file diff --git a/apps/api/plane/db/models/description.py b/apps/api/plane/db/models/description.py new file mode 100644 index 00000000000..24c15d395d4 --- /dev/null +++ b/apps/api/plane/db/models/description.py @@ -0,0 +1,56 @@ +from django.db import models +from django.utils.html import strip_tags +from .workspace import WorkspaceBaseModel + + +class Description(WorkspaceBaseModel): + + + description_json = models.JSONField(default=dict, blank=True) + description_html = models.TextField(blank=True, default="

") + description_binary = models.BinaryField(null=True) + description_stripped = models.TextField(blank=True, null=True) + + class Meta: + verbose_name = "Description" + verbose_name_plural = "Descriptions" + db_table = "descriptions" + 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(Description, self).save(*args, **kwargs) + + +class DescriptionVersion(WorkspaceBaseModel): + """ + DescriptionVersion is a model used to store historical versions of a Description. + """ + + description = models.ForeignKey( + "db.Description", on_delete=models.CASCADE, related_name="versions" + ) + description_json = models.JSONField(default=dict, blank=True) + description_html = models.TextField(blank=True, default="

") + description_binary = models.BinaryField(null=True) + description_stripped = models.TextField(blank=True, null=True) + + class Meta: + verbose_name = "Description Version" + verbose_name_plural = "Description Versions" + db_table = "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(DescriptionVersion, self).save(*args, **kwargs)