From 70dc14d4f6fa14b03bd23eda8e9671f7741ea60e Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:39:38 +0530 Subject: [PATCH] [WEB-1969] chore: migration for issue worklogs and issue property (#635) * dev: add issue properties * fix: migrations * dev: update issue prop migrations * dev: update issue props migration * dev: add validation on issue properties * chore: migration fixes * chore: changed the migration file * chore: minor changes --------- Co-authored-by: pablohashescobar Co-authored-by: sriram veeraghanta --- apiserver/plane/ee/migrations/0001_initial.py | 534 ++++++++++++++++++ apiserver/plane/ee/migrations/__init__.py | 0 apiserver/plane/ee/models/__init__.py | 7 + apiserver/plane/ee/models/issue.py | 28 + apiserver/plane/ee/models/issue_properties.py | 172 ++++++ 5 files changed, 741 insertions(+) create mode 100644 apiserver/plane/ee/migrations/0001_initial.py create mode 100644 apiserver/plane/ee/migrations/__init__.py create mode 100644 apiserver/plane/ee/models/__init__.py create mode 100644 apiserver/plane/ee/models/issue.py create mode 100644 apiserver/plane/ee/models/issue_properties.py diff --git a/apiserver/plane/ee/migrations/0001_initial.py b/apiserver/plane/ee/migrations/0001_initial.py new file mode 100644 index 0000000000..214a9b6d1f --- /dev/null +++ b/apiserver/plane/ee/migrations/0001_initial.py @@ -0,0 +1,534 @@ +# Generated by Django 4.2.11 on 2024-07-22 13:22 + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="IssueProperty", + 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" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("display_name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True, null=True)), + ("logo_props", models.JSONField(blank=True, default=dict)), + ("sort_order", models.FloatField(default=65535)), + ( + "property_type", + models.CharField( + choices=[ + ("TEXT", "Text"), + ("DATETIME", "Datetime"), + ("DECIMAL", "Decimal"), + ("BOOLEAN", "Boolean"), + ("OPTION", "Option"), + ("RELATION", "Relation"), + ("URL", "URL"), + ("EMAIL", "Email"), + ("FILE", "File"), + ], + max_length=255, + ), + ), + ( + "relation_type", + models.CharField( + blank=True, + null=True, + choices=[("ISSUE", "Issue"), ("USER", "User")], + max_length=255, + ), + ), + ("is_required", models.BooleanField(default=False)), + ( + "default_value", + django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(), + blank=True, + default=list, + size=None, + ), + ), + ("settings", models.JSONField(blank=True, default=dict)), + ("is_active", models.BooleanField(default=True)), + ("is_multi", models.BooleanField(default=False)), + ( + "validation_rules", + 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_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="properties", + to="db.issuetype", + ), + ), + ( + "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={ + "db_table": "issue_properties", + "ordering": ["sort_order"], + "unique_together": {("name", "issue_type")}, + }, + ), + migrations.CreateModel( + name="IssuePropertyOption", + 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" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("sort_order", models.FloatField(default=65535)), + ("description", models.TextField(blank=True)), + ("logo_props", models.JSONField(blank=True, default=dict)), + ("is_active", models.BooleanField(default=True)), + ("is_default", models.BooleanField(default=False)), + ( + "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", + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="ee.issuepropertyoption", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="options", + to="ee.issueproperty", + ), + ), + ( + "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={ + "db_table": "issue_property_options", + "ordering": ["sort_order"], + "unique_together": {("name", "property")}, + }, + ), + migrations.CreateModel( + name="IssueWorkLog", + 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" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("description", models.TextField(blank=True)), + ("duration", models.IntegerField(default=0)), + ( + "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="worklogs", + to="db.issue", + ), + ), + ( + "logged_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="worklogs", + 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 Work Log", + "verbose_name_plural": "Issue Work Logs", + "db_table": "issue_worklogs", + "ordering": ("-created_at",), + }, + ), + migrations.CreateModel( + name="IssuePropertyValue", + 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" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("value_text", models.TextField(blank=True)), + ("value_boolean", models.BooleanField(default=False)), + ("value_decimal", models.FloatField(default=0)), + ( + "value_datetime", + models.DateTimeField(blank=True, null=True), + ), + ("value_uuid", models.UUIDField(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", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="properties", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="values", + to="ee.issueproperty", + ), + ), + ( + "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", + ), + ), + ( + "value_option", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="property_values", + to="ee.issuepropertyoption", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "db_table": "issue_property_values", + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="IssuePropertyActivity", + 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" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("old_value", models.TextField(blank=True)), + ("new_value", models.TextField(blank=True)), + ("old_identifier", models.UUIDField(blank=True, null=True)), + ("new_identifier", models.UUIDField(blank=True, null=True)), + ("action", models.CharField(max_length=255)), + ("epoch", models.FloatField(null=True)), + ( + "comment", + models.TextField(blank=True, verbose_name="Comment"), + ), + ( + "actor", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_property_activities", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "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="activities", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="activities", + to="ee.issueproperty", + ), + ), + ( + "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={ + "db_table": "issue_property_activities", + "ordering": ["-created_at"], + }, + ), + ] diff --git a/apiserver/plane/ee/migrations/__init__.py b/apiserver/plane/ee/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apiserver/plane/ee/models/__init__.py b/apiserver/plane/ee/models/__init__.py new file mode 100644 index 0000000000..cea4190a69 --- /dev/null +++ b/apiserver/plane/ee/models/__init__.py @@ -0,0 +1,7 @@ +from .issue_properties import ( + IssueProperty, + IssuePropertyOption, + IssuePropertyValue, + IssuePropertyActivity, +) +from .issue import IssueWorkLog diff --git a/apiserver/plane/ee/models/issue.py b/apiserver/plane/ee/models/issue.py new file mode 100644 index 0000000000..0495a1cc4f --- /dev/null +++ b/apiserver/plane/ee/models/issue.py @@ -0,0 +1,28 @@ +# Django imports +from django.db import models +from django.conf import settings + +# Module imports +from plane.db.models import ProjectBaseModel, Issue + + +class IssueWorkLog(ProjectBaseModel): + issue = models.ForeignKey( + Issue, on_delete=models.CASCADE, related_name="worklogs" + ) + description = models.TextField(blank=True) + logged_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="worklogs", + ) + duration = models.IntegerField(default=0) + + class Meta: + verbose_name = "Issue Work Log" + verbose_name_plural = "Issue Work Logs" + db_table = "issue_worklogs" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.logged_by.email}" diff --git a/apiserver/plane/ee/models/issue_properties.py b/apiserver/plane/ee/models/issue_properties.py new file mode 100644 index 0000000000..c679c3fd91 --- /dev/null +++ b/apiserver/plane/ee/models/issue_properties.py @@ -0,0 +1,172 @@ +# Django imports +from django.db import models +from django.conf import settings +from django.template.defaultfilters import slugify +from django.contrib.postgres.fields import ArrayField + +# Module imports +from plane.db.models import WorkspaceBaseModel + + +class PropertyTypeEnum(models.TextChoices): + + TEXT = "TEXT", "Text" + DATETIME = "DATETIME", "Datetime" + DECIMAL = "DECIMAL", "Decimal" + BOOLEAN = "BOOLEAN", "Boolean" + OPTION = "OPTION", "Option" + RELATION = "RELATION", "Relation" + URL = "URL", "URL" + EMAIL = "EMAIL", "Email" + FILE = "FILE", "File" + + +class RelationTypeEnum(models.TextChoices): + ISSUE = "ISSUE", "Issue" + USER = "USER", "User" + + +class IssueProperty(WorkspaceBaseModel): + name = models.CharField(max_length=255) + display_name = models.CharField(max_length=255) + description = models.TextField(blank=True, null=True) + logo_props = models.JSONField(blank=True, default=dict) + sort_order = models.FloatField(default=65535) + property_type = models.CharField( + max_length=255, + choices=PropertyTypeEnum.choices, + ) + relation_type = models.CharField( + max_length=255, + blank=True, + null=True, + choices=RelationTypeEnum.choices, + ) + is_required = models.BooleanField(default=False) + default_value = ArrayField(models.TextField(), blank=True, default=list) + settings = models.JSONField(blank=True, default=dict) + is_active = models.BooleanField(default=True) + issue_type = models.ForeignKey( + "db.IssueType", on_delete=models.CASCADE, related_name="properties" + ) + is_multi = models.BooleanField(default=False) + validation_rules = models.JSONField(blank=True, default=dict) + + class Meta: + ordering = ["sort_order"] + unique_together = ["name", "issue_type"] + db_table = "issue_properties" + + def save(self, *args, **kwargs): + self.name = slugify(self.display_name) + if self._state.adding: + # Get the maximum sequence value from the database + last_id = IssueProperty.objects.filter( + project=self.project + ).aggregate(largest=models.Max("sort_order"))["largest"] + # if last_id is not None + if last_id is not None: + self.sort_order = last_id + 10000 + + super(IssueProperty, self).save(*args, **kwargs) + + def __str__(self): + return self.display_name + + +class IssuePropertyOption(WorkspaceBaseModel): + + name = models.CharField(max_length=255) + sort_order = models.FloatField(default=65535) + property = models.ForeignKey( + IssueProperty, on_delete=models.CASCADE, related_name="options" + ) + description = models.TextField(blank=True) + logo_props = models.JSONField(blank=True, default=dict) + is_active = models.BooleanField(default=True) + parent = models.ForeignKey( + "self", + on_delete=models.CASCADE, + related_name="children", + null=True, + blank=True, + ) + is_default = models.BooleanField(default=False) + + class Meta: + ordering = ["sort_order"] + unique_together = ["name", "property"] + db_table = "issue_property_options" + + def save(self, *args, **kwargs): + if self._state.adding: + # Get the maximum sequence value from the database + last_id = IssuePropertyOption.objects.filter( + project=self.project + ).aggregate(largest=models.Max("sort_order"))["largest"] + # if last_id is not None + if last_id is not None: + self.sort_order = last_id + 10000 + + if self.is_default: + self.is_active = True + + super(IssuePropertyOption, self).save(*args, **kwargs) + + +class IssuePropertyValue(WorkspaceBaseModel): + issue = models.ForeignKey( + "db.Issue", on_delete=models.CASCADE, related_name="properties" + ) + property = models.ForeignKey( + IssueProperty, on_delete=models.CASCADE, related_name="values" + ) + value_text = models.TextField(blank=True) + value_boolean = models.BooleanField(default=False) + value_decimal = models.FloatField(default=0) + value_datetime = models.DateTimeField(blank=True, null=True) + value_uuid = models.UUIDField(blank=True, null=True) + value_option = models.ForeignKey( + IssuePropertyOption, + on_delete=models.CASCADE, + related_name="property_values", + blank=True, + null=True, + ) + + class Meta: + ordering = ["-created_at"] + db_table = "issue_property_values" + + def __str__(self): + return f"{self.property.display_name}" + + +class IssuePropertyActivity(WorkspaceBaseModel): + + old_value = models.TextField(blank=True) + new_value = models.TextField(blank=True) + old_identifier = models.UUIDField(blank=True, null=True) + new_identifier = models.UUIDField(blank=True, null=True) + property = models.ForeignKey( + IssueProperty, on_delete=models.CASCADE, related_name="activities" + ) + issue = models.ForeignKey( + "db.Issue", on_delete=models.CASCADE, related_name="activities" + ) + actor = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name="issue_property_activities", + ) + action = models.CharField(max_length=255) + epoch = models.FloatField(null=True) + comment = models.TextField(verbose_name="Comment", blank=True) + + class Meta: + ordering = ["-created_at"] + db_table = "issue_property_activities" + + def __str__(self): + return f"{self.property.display_name} - {self.issue_id}"