diff --git a/apiserver/plane/db/migrations/0086_alter_webhook.py b/apiserver/plane/db/migrations/0086_alter_webhook.py index e4ca2755c9..8c3f9cdbe7 100644 --- a/apiserver/plane/db/migrations/0086_alter_webhook.py +++ b/apiserver/plane/db/migrations/0086_alter_webhook.py @@ -2,6 +2,9 @@ from django.db import migrations, models import plane.db.models.webhook +from django.conf import settings +import django.db.models.deletion +import uuid class Migration(migrations.Migration): @@ -86,4 +89,178 @@ class Migration(migrations.Migration): migrations.DeleteModel( name="TeamPage", ), + migrations.CreateModel( + name="IssueVersion", + 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, + ), + ), + ("parent", models.UUIDField(blank=True, null=True)), + ("state", models.UUIDField(blank=True, null=True)), + ("estimate_point", models.UUIDField(blank=True, null=True)), + ( + "name", + models.CharField( + max_length=255, verbose_name="Issue Name" + ), + ), + ("description", models.JSONField(blank=True, default=dict)), + ( + "description_html", + models.TextField(blank=True, default="
"), + ), + ( + "description_stripped", + models.TextField(blank=True, null=True), + ), + ("description_binary", models.BinaryField(null=True)), + ( + "priority", + models.CharField( + choices=[ + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ], + default="none", + max_length=30, + verbose_name="Issue Priority", + ), + ), + ("start_date", models.DateField(blank=True, null=True)), + ("target_date", models.DateField(blank=True, null=True)), + ( + "sequence_id", + models.IntegerField( + default=1, verbose_name="Issue Sequence ID" + ), + ), + ("sort_order", models.FloatField(default=65535)), + ("completed_at", models.DateTimeField(null=True)), + ("archived_at", models.DateField(null=True)), + ("is_draft", models.BooleanField(default=False)), + ( + "external_source", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "external_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("type", models.UUIDField(blank=True, null=True)), + ( + "last_saved_at", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("owned_by", models.UUIDField()), + ( + "assignees", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "labels", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), + blank=True, + default=list, + size=None, + ), + ), + ("cycle", models.UUIDField(blank=True, null=True)), + ( + "modules", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), + blank=True, + default=list, + size=None, + ), + ), + ("properties", models.JSONField(default=dict)), + ("meta", models.JSONField(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="versions", + to="db.issue", + ), + ), + ( + "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 Version", + "verbose_name_plural": "Issue Versions", + "db_table": "issue_versions", + "ordering": ("-created_at",), + }, + ), ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index adede2982f..e21ff77cf3 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -9,11 +9,12 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.utils import timezone from django.db.models import Q +from django import apps # Module imports from plane.utils.html_processor import strip_tags from plane.db.mixins import SoftDeletionManager - +from plane.utils.exception_logger import log_exception from .project import ProjectBaseModel @@ -709,3 +710,126 @@ class IssueVote(ProjectBaseModel): def __str__(self): return f"{self.issue.name} {self.actor.email}" + + +class IssueVersion(ProjectBaseModel): + issue = models.ForeignKey( + "db.Issue", + on_delete=models.CASCADE, + related_name="versions", + ) + PRIORITY_CHOICES = ( + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ) + parent = models.UUIDField(blank=True, null=True) + state = models.UUIDField(blank=True, null=True) + estimate_point = models.UUIDField(blank=True, null=True) + name = models.CharField(max_length=255, verbose_name="Issue Name") + description = models.JSONField(blank=True, default=dict) + description_html = models.TextField(blank=True, default="") + description_stripped = models.TextField(blank=True, null=True) + description_binary = models.BinaryField(null=True) + priority = models.CharField( + max_length=30, + choices=PRIORITY_CHOICES, + verbose_name="Issue Priority", + default="none", + ) + start_date = models.DateField(null=True, blank=True) + target_date = models.DateField(null=True, blank=True) + sequence_id = models.IntegerField( + default=1, verbose_name="Issue Sequence ID" + ) + sort_order = models.FloatField(default=65535) + completed_at = models.DateTimeField(null=True) + archived_at = models.DateField(null=True) + is_draft = models.BooleanField(default=False) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) + type = models.UUIDField(blank=True, null=True) + last_saved_at = models.DateTimeField(default=timezone.now) + owned_by = models.UUIDField() + assignees = ArrayField( + models.UUIDField(), + blank=True, + default=list, + ) + labels = ArrayField( + models.UUIDField(), + blank=True, + default=list, + ) + cycle = models.UUIDField( + null=True, + blank=True, + ) + modules = ArrayField( + models.UUIDField(), + blank=True, + default=list, + ) + properties = models.JSONField(default=dict) + meta = models.JSONField(default=dict) + + class Meta: + verbose_name = "Issue Version" + verbose_name_plural = "Issue Versions" + db_table = "issue_versions" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.name} <{self.project.name}>" + + @classmethod + def log_issue_version(cls, issue, user): + try: + """ + Log the issue version + """ + + Module = apps.get_model("db.Module") + CycleIssue = apps.get_model("db.CycleIssue") + + cycle_issue = CycleIssue.objects.filter( + issue=issue, + ).first() + + cls.objects.create( + issue=issue, + parent=issue.parent, + state=issue.state, + point=issue.point, + estimate_point=issue.estimate_point, + name=issue.name, + description=issue.description, + description_html=issue.description_html, + description_stripped=issue.description_stripped, + description_binary=issue.description_binary, + priority=issue.priority, + start_date=issue.start_date, + target_date=issue.target_date, + sequence_id=issue.sequence_id, + sort_order=issue.sort_order, + completed_at=issue.completed_at, + archived_at=issue.archived_at, + is_draft=issue.is_draft, + external_source=issue.external_source, + external_id=issue.external_id, + type=issue.type, + last_saved_at=issue.last_saved_at, + assignees=issue.assignees, + labels=issue.labels, + cycle=cycle_issue.cycle if cycle_issue else None, + modules=Module.objects.filter(issue=issue).values_list( + "id", flat=True + ), + owned_by=user, + ) + return True + except Exception as e: + log_exception(e) + return False