diff --git a/apiserver/plane/ee/migrations/0002_projectattribute_projectstate_workspacefeature_and_more.py b/apiserver/plane/ee/migrations/0002_projectattribute_projectstate_workspacefeature_and_more.py new file mode 100644 index 0000000000..00dce58658 --- /dev/null +++ b/apiserver/plane/ee/migrations/0002_projectattribute_projectstate_workspacefeature_and_more.py @@ -0,0 +1,375 @@ +# Generated by Django 4.2.14 on 2024-08-02 09:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("ee", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="ProjectAttribute", + 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, + ), + ), + ( + "priority", + models.CharField( + choices=[ + ("none", "None"), + ("low", "Low"), + ("medium", "Medium"), + ("high", "High"), + ("urgent", "Urgent"), + ], + default="none", + max_length=50, + ), + ), + ("start_date", models.DateTimeField(blank=True, null=True)), + ("target_date", models.DateTimeField(blank=True, null=True)), + ], + options={ + "verbose_name": "Project Attribute", + "verbose_name_plural": "Project Attributes", + "db_table": "project_attributes", + "ordering": ("created_at",), + }, + ), + migrations.CreateModel( + name="ProjectState", + 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, + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.TextField(blank=True)), + ("color", models.CharField(max_length=255)), + ("sequence", models.FloatField(default=65535)), + ( + "group", + models.CharField( + choices=[ + ("draft", "Draft"), + ("planning", "Planning"), + ("execution", "Execution"), + ("monitoring", "Monitoring"), + ("completed", "Completed"), + ("cancelled", "Cancelled"), + ], + default="draft", + max_length=20, + ), + ), + ("default", 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), + ), + ], + options={ + "verbose_name": "Project State", + "verbose_name_plural": "Project States", + "db_table": "project_states", + "ordering": ("sequence",), + }, + ), + migrations.CreateModel( + name="WorkspaceFeature", + 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, + ), + ), + ( + "is_project_grouping_enabled", + models.BooleanField(default=False), + ), + ], + options={ + "verbose_name": "Workspace Feature", + "verbose_name_plural": "Workspace Features", + "db_table": "workspace_features", + "ordering": ("-created_at",), + }, + ), + migrations.AlterUniqueTogether( + name="issueproperty", + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name="issuepropertyoption", + unique_together=set(), + ), + migrations.AddField( + model_name="issueproperty", + name="deleted_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + migrations.AddField( + model_name="issuepropertyactivity", + name="deleted_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + migrations.AddField( + model_name="issuepropertyoption", + name="deleted_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + migrations.AddField( + model_name="issuepropertyvalue", + name="deleted_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + migrations.AddField( + model_name="issueworklog", + name="deleted_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + migrations.AlterUniqueTogether( + name="issueproperty", + unique_together={("name", "issue_type", "deleted_at")}, + ), + migrations.AlterUniqueTogether( + name="issuepropertyoption", + unique_together={("name", "property", "deleted_at")}, + ), + migrations.AddConstraint( + model_name="issueproperty", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "issue_type"), + name="issue_property_unique_name_project_when_deleted_at_null", + ), + ), + migrations.AddConstraint( + model_name="issuepropertyoption", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "property"), + name="issue_property_option_unique_name_project_when_deleted_at_null", + ), + ), + migrations.AddField( + model_name="workspacefeature", + name="created_by", + field=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", + ), + ), + migrations.AddField( + model_name="workspacefeature", + name="updated_by", + field=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", + ), + ), + migrations.AddField( + model_name="workspacefeature", + name="workspace", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="features", + to="db.workspace", + ), + ), + migrations.AddField( + model_name="projectstate", + name="created_by", + field=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", + ), + ), + migrations.AddField( + model_name="projectstate", + name="updated_by", + field=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", + ), + ), + migrations.AddField( + model_name="projectstate", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_states", + to="db.workspace", + ), + ), + migrations.AddField( + model_name="projectattribute", + name="created_by", + field=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", + ), + ), + migrations.AddField( + model_name="projectattribute", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + migrations.AddField( + model_name="projectattribute", + name="state", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="project_attributes", + to="ee.projectstate", + ), + ), + migrations.AddField( + model_name="projectattribute", + name="updated_by", + field=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", + ), + ), + migrations.AddField( + model_name="projectattribute", + name="workspace", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + migrations.AddConstraint( + model_name="projectstate", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "workspace"), + name="project_state_unique_name_project_when_deleted_at_null", + ), + ), + migrations.AlterUniqueTogether( + name="projectstate", + unique_together={("name", "workspace", "deleted_at")}, + ), + ] diff --git a/apiserver/plane/ee/models/__init__.py b/apiserver/plane/ee/models/__init__.py index cea4190a69..ed9f3521f8 100644 --- a/apiserver/plane/ee/models/__init__.py +++ b/apiserver/plane/ee/models/__init__.py @@ -5,3 +5,5 @@ from .issue_properties import ( IssuePropertyActivity, ) from .issue import IssueWorkLog +from .project import ProjectState, ProjectAttribute +from .workspace import WorkspaceFeature diff --git a/apiserver/plane/ee/models/issue_properties.py b/apiserver/plane/ee/models/issue_properties.py index c679c3fd91..b4f46b4347 100644 --- a/apiserver/plane/ee/models/issue_properties.py +++ b/apiserver/plane/ee/models/issue_properties.py @@ -1,6 +1,7 @@ # Django imports from django.db import models from django.conf import settings +from django.db.models import Q from django.template.defaultfilters import slugify from django.contrib.postgres.fields import ArrayField @@ -54,7 +55,14 @@ class IssueProperty(WorkspaceBaseModel): class Meta: ordering = ["sort_order"] - unique_together = ["name", "issue_type"] + unique_together = ["name", "issue_type", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["name", "issue_type"], + condition=Q(deleted_at__isnull=True), + name="issue_property_unique_name_project_when_deleted_at_null", + ) + ] db_table = "issue_properties" def save(self, *args, **kwargs): @@ -95,7 +103,14 @@ class IssuePropertyOption(WorkspaceBaseModel): class Meta: ordering = ["sort_order"] - unique_together = ["name", "property"] + unique_together = ["name", "property", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["name", "property"], + condition=Q(deleted_at__isnull=True), + name="issue_property_option_unique_name_project_when_deleted_at_null", + ) + ] db_table = "issue_property_options" def save(self, *args, **kwargs): diff --git a/apiserver/plane/ee/models/project.py b/apiserver/plane/ee/models/project.py new file mode 100644 index 0000000000..17450763fd --- /dev/null +++ b/apiserver/plane/ee/models/project.py @@ -0,0 +1,91 @@ +# Django imports +from django.db import models + +# Module imports +from plane.db.models.base import BaseModel +from plane.db.models.project import ProjectBaseModel + + +class GroupChoices(models.TextChoices): + DRAFT = "draft", "Draft" + PLANNING = "planning", "Planning" + EXECUTION = "execution", "Execution" + MONITORING = "monitoring", "Monitoring" + COMPLETED = "completed", "Completed" + CANCELLED = "cancelled", "Cancelled" + + +class ProjectState(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", on_delete=models.CASCADE, related_name="project_states" + ) + name = models.CharField(max_length=50) + description = models.TextField(blank=True) + color = models.CharField(max_length=255) + sequence = models.FloatField(default=65535) + group = models.CharField( + max_length=20, choices=GroupChoices.choices, default=GroupChoices.DRAFT + ) + default = 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) + + def __str__(self): + """Return name of the state""" + return f"{self.name} <{self.workspace.name}>" + + class Meta: + unique_together = ["name", "workspace", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["name", "workspace"], + condition=models.Q(deleted_at__isnull=True), + name="project_state_unique_name_project_when_deleted_at_null", + ) + ] + verbose_name = "Project State" + verbose_name_plural = "Project States" + db_table = "project_states" + ordering = ("sequence",) + + def save(self, *args, **kwargs): + if self._state.adding: + # Get the maximum sequence value from the database + last_id = ProjectState.objects.filter( + workspace=self.workspace + ).aggregate(largest=models.Max("sequence"))["largest"] + # if last_id is not None + if last_id is not None: + self.sequence = last_id + 15000 + + return super().save(*args, **kwargs) + + +class PriorityChoices(models.TextChoices): + NONE = "none", "None" + LOW = "low", "Low" + MEDIUM = "medium", "Medium" + HIGH = "high", "High" + URGENT = "urgent", "Urgent" + + +class ProjectAttribute(ProjectBaseModel): + priority = models.CharField( + max_length=50, + choices=PriorityChoices.choices, + default=PriorityChoices.NONE, + ) + state = models.ForeignKey( + ProjectState, + on_delete=models.SET_NULL, + related_name="project_attributes", + null=True, + ) + start_date = models.DateTimeField(null=True, blank=True) + target_date = models.DateTimeField(null=True, blank=True) + + class Meta: + verbose_name = "Project Attribute" + verbose_name_plural = "Project Attributes" + db_table = "project_attributes" + ordering = ("created_at",) diff --git a/apiserver/plane/ee/models/workspace.py b/apiserver/plane/ee/models/workspace.py new file mode 100644 index 0000000000..9bc69d0855 --- /dev/null +++ b/apiserver/plane/ee/models/workspace.py @@ -0,0 +1,18 @@ +# Django imports +from django.db import models + +# Module imports +from plane.db.models.base import BaseModel + + +class WorkspaceFeature(BaseModel): + workspace = models.OneToOneField( + "db.Workspace", on_delete=models.CASCADE, related_name="features" + ) + is_project_grouping_enabled = models.BooleanField(default=False) + + class Meta: + verbose_name = "Workspace Feature" + verbose_name_plural = "Workspace Features" + db_table = "workspace_features" + ordering = ("-created_at",)