From 6a09e51541982a77a26af5d7274a798840263bb0 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 14:47:59 +0530 Subject: [PATCH 1/8] refactor: rename IssueUserProperty to ProjectUserProperty and update related references across the codebase --- apps/api/plane/api/views/project.py | 6 +-- apps/api/plane/app/serializers/issue.py | 4 +- apps/api/plane/app/urls/issue.py | 4 +- apps/api/plane/app/views/issue/base.py | 6 +-- apps/api/plane/app/views/project/base.py | 6 +-- apps/api/plane/app/views/project/invite.py | 6 +-- apps/api/plane/app/views/project/member.py | 6 +-- apps/api/plane/bgtasks/workspace_seed_task.py | 6 +-- .../commands/create_project_member.py | 4 +- .../0113_alter_issueuserproperty_table.py | 38 +++++++++++++++++ .../db/migrations/0114_auto_20251201_0857.py | 42 +++++++++++++++++++ apps/api/plane/db/models/__init__.py | 2 +- apps/api/plane/db/models/api.py | 1 + apps/api/plane/db/models/issue.py | 30 ------------- apps/api/plane/db/models/project.py | 35 +++++++++++++++- .../tests/contract/app/test_project_app.py | 10 ++--- 16 files changed, 145 insertions(+), 61 deletions(-) create mode 100644 apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py create mode 100644 apps/api/plane/db/migrations/0114_auto_20251201_0857.py diff --git a/apps/api/plane/api/views/project.py b/apps/api/plane/api/views/project.py index fa735f557d..698343ded1 100644 --- a/apps/api/plane/api/views/project.py +++ b/apps/api/plane/api/views/project.py @@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest from plane.db.models import ( Cycle, Intake, - IssueUserProperty, + ProjectUserProperty, Module, Project, DeployBoard, @@ -217,7 +217,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView): # Add the user as Administrator to the project _ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20) # Also create the issue property for the user - _ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user) + _ = ProjectUserProperty.objects.create(project_id=serializer.instance.id, user=request.user) if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str( request.user.id @@ -228,7 +228,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView): role=20, ) # Also create the issue property for the user - IssueUserProperty.objects.create( + ProjectUserProperty.objects.create( project_id=serializer.instance.id, user_id=serializer.instance.project_lead, ) diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index 5e3b93ab67..c71bd8447f 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -18,7 +18,7 @@ from plane.db.models import ( Issue, IssueActivity, IssueComment, - IssueUserProperty, + ProjectUserProperty, IssueAssignee, IssueSubscriber, IssueLabel, @@ -348,7 +348,7 @@ class IssueActivitySerializer(BaseSerializer): class IssueUserPropertySerializer(BaseSerializer): class Meta: - model = IssueUserProperty + model = ProjectUserProperty fields = "__all__" read_only_fields = ["user", "workspace", "project"] diff --git a/apps/api/plane/app/urls/issue.py b/apps/api/plane/app/urls/issue.py index 1d809e248f..e39ce9f9c5 100644 --- a/apps/api/plane/app/urls/issue.py +++ b/apps/api/plane/app/urls/issue.py @@ -208,13 +208,13 @@ urlpatterns = [ name="project-issue-comment-reactions", ), ## End Comment Reactions - ## IssueUserProperty + ## ProjectUserProperty path( "workspaces//projects//user-properties/", IssueUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), - ## IssueUserProperty End + ## ProjectUserProperty End ## Issue Archives path( "workspaces//projects//archived-issues/", diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index c24db61698..9dace60e76 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -51,7 +51,7 @@ from plane.db.models import ( IssueReaction, IssueRelation, IssueSubscriber, - IssueUserProperty, + ProjectUserProperty, ModuleIssue, Project, ProjectMember, @@ -718,7 +718,7 @@ class IssueViewSet(BaseViewSet): class IssueUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): - issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id) + issue_property = ProjectUserProperty.objects.get(user=request.user, project_id=project_id) issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters) issue_property.filters = request.data.get("filters", issue_property.filters) @@ -730,7 +730,7 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): - issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id) + issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id) serializer = IssueUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 3aa3564916..f98fc684c1 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -26,7 +26,7 @@ from plane.db.models import ( UserFavorite, Intake, DeployBoard, - IssueUserProperty, + ProjectUserProperty, Project, ProjectIdentifier, ProjectMember, @@ -249,7 +249,7 @@ class ProjectViewSet(BaseViewSet): role=ROLE.ADMIN.value, ) # Also create the issue property for the user - _ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user) + _ = ProjectUserProperty.objects.create(project_id=serializer.data["id"], user=request.user) if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str( request.user.id @@ -260,7 +260,7 @@ class ProjectViewSet(BaseViewSet): role=ROLE.ADMIN.value, ) # Also create the issue property for the user - IssueUserProperty.objects.create( + ProjectUserProperty.objects.create( project_id=serializer.data["id"], user_id=serializer.data["project_lead"], ) diff --git a/apps/api/plane/app/views/project/invite.py b/apps/api/plane/app/views/project/invite.py index cc5b3f4b57..6ae5a5e11a 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -24,7 +24,7 @@ from plane.db.models import ( User, WorkspaceMember, Project, - IssueUserProperty, + ProjectUserProperty, ) from plane.db.models.project import ProjectNetwork from plane.utils.host import base_host @@ -160,9 +160,9 @@ class UserProjectInvitationsViewset(BaseViewSet): ignore_conflicts=True, ) - IssueUserProperty.objects.bulk_create( + ProjectUserProperty.objects.bulk_create( [ - IssueUserProperty( + ProjectUserProperty( project_id=project_id, user=request.user, workspace=workspace, diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py index 69d45226ce..664355a17a 100644 --- a/apps/api/plane/app/views/project/member.py +++ b/apps/api/plane/app/views/project/member.py @@ -13,7 +13,7 @@ from plane.app.serializers import ( from plane.app.permissions import WorkspaceUserPermission -from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember +from plane.db.models import Project, ProjectMember, ProjectUserProperty, WorkspaceMember from plane.bgtasks.project_add_user_email_task import project_add_user_email from plane.utils.host import base_host from plane.app.permissions.base import allow_permission, ROLE @@ -119,7 +119,7 @@ class ProjectMemberViewSet(BaseViewSet): ) # Create a new issue property bulk_issue_props.append( - IssueUserProperty( + ProjectUserProperty( user_id=member.get("member_id"), project_id=project_id, workspace_id=project.workspace_id, @@ -129,7 +129,7 @@ class ProjectMemberViewSet(BaseViewSet): # Bulk create the project members and issue properties project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True) - _ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True) + _ = ProjectUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True) project_members = ProjectMember.objects.filter( project_id=project_id, diff --git a/apps/api/plane/bgtasks/workspace_seed_task.py b/apps/api/plane/bgtasks/workspace_seed_task.py index fb9980c3fd..6565ed6466 100644 --- a/apps/api/plane/bgtasks/workspace_seed_task.py +++ b/apps/api/plane/bgtasks/workspace_seed_task.py @@ -20,7 +20,7 @@ from plane.db.models import ( WorkspaceMember, Project, ProjectMember, - IssueUserProperty, + ProjectUserProperty, State, Label, Issue, @@ -118,9 +118,9 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]: ) # Create issue user properties - IssueUserProperty.objects.bulk_create( + ProjectUserProperty.objects.bulk_create( [ - IssueUserProperty( + ProjectUserProperty( project=project, user_id=workspace_member["member_id"], workspace_id=workspace.id, diff --git a/apps/api/plane/db/management/commands/create_project_member.py b/apps/api/plane/db/management/commands/create_project_member.py index d9b46524c2..a097056b99 100644 --- a/apps/api/plane/db/management/commands/create_project_member.py +++ b/apps/api/plane/db/management/commands/create_project_member.py @@ -8,7 +8,7 @@ from plane.db.models import ( WorkspaceMember, ProjectMember, Project, - IssueUserProperty, + ProjectUserProperty, ) @@ -67,7 +67,7 @@ class Command(BaseCommand): ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order) # Issue Property - IssueUserProperty.objects.get_or_create(user=user, project=project) + ProjectUserProperty.objects.get_or_create(user=user, project=project) # Success message self.stdout.write(self.style.SUCCESS(f"User {user_email} added to project {project_id}")) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py new file mode 100644 index 0000000000..3083f96654 --- /dev/null +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.25 on 2025-11-28 14:36 + +from django.db import migrations, models +import plane.db.models.project + + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0112_auto_20251124_0603'), + ] + + operations = [ + migrations.AlterModelTable( + name='issueuserproperty', + table='project_user_properties', + ), + migrations.RenameModel( + old_name='IssueUserProperty', + new_name='ProjectUserProperty', + ), + migrations.AddField( + model_name='apitoken', + name='allowed_rate_limit', + field=models.CharField(default='60/min', max_length=255), + ), + migrations.AddField( + model_name='projectuserproperty', + name='preferences', + field=models.JSONField(default=plane.db.models.project.get_default_preferences), + ), + migrations.AddField( + model_name='projectuserproperty', + name='sort_order', + field=models.FloatField(default=65535), + ), + ] diff --git a/apps/api/plane/db/migrations/0114_auto_20251201_0857.py b/apps/api/plane/db/migrations/0114_auto_20251201_0857.py new file mode 100644 index 0000000000..0ca5961d2d --- /dev/null +++ b/apps/api/plane/db/migrations/0114_auto_20251201_0857.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.25 on 2025-12-01 08:57 + +from django.db import migrations + +def move_issue_user_properties_to_project_user_properties(apps, schema_editor): + ProjectMember = apps.get_model('db', 'ProjectMember') + ProjectUserProperty = apps.get_model('db', 'ProjectUserProperty') + + # Get all project members + project_members = ProjectMember.objects.filter(deleted_at__isnull=True).values('member_id', 'project_id', 'preferences', 'sort_order') + + # create a mapping with consistent ordering + pm_dict = { + (pm['member_id'], pm['project_id']): pm + for pm in project_members + } + + # Get all project user properties + properties_to_update = [] + for projectuserproperty in ProjectUserProperty.objects.filter(deleted_at__isnull=True): + pm = pm_dict.get((projectuserproperty.user_id, projectuserproperty.project_id)) + if pm: + projectuserproperty.preferences = pm['preferences'] + projectuserproperty.sort_order = pm['sort_order'] + properties_to_update.append(projectuserproperty) + + ProjectUserProperty.objects.bulk_update(properties_to_update, ['preferences', 'sort_order'], batch_size=2000) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0113_alter_issueuserproperty_table'), + ] + + operations = [ + migrations.RunPython( + move_issue_user_properties_to_project_user_properties, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/apps/api/plane/db/models/__init__.py b/apps/api/plane/db/models/__init__.py index d24a145644..e668e2556b 100644 --- a/apps/api/plane/db/models/__init__.py +++ b/apps/api/plane/db/models/__init__.py @@ -34,7 +34,6 @@ from .issue import ( IssueLabel, IssueLink, IssueMention, - IssueUserProperty, IssueReaction, IssueRelation, IssueSequence, @@ -53,6 +52,7 @@ from .project import ( ProjectMember, ProjectMemberInvite, ProjectPublicMember, + ProjectUserProperty, ) from .session import Session from .social_connection import SocialLoginConnection diff --git a/apps/api/plane/db/models/api.py b/apps/api/plane/db/models/api.py index 7d040ebc28..75449a7428 100644 --- a/apps/api/plane/db/models/api.py +++ b/apps/api/plane/db/models/api.py @@ -32,6 +32,7 @@ class APIToken(BaseModel): workspace = models.ForeignKey("db.Workspace", related_name="api_tokens", on_delete=models.CASCADE, null=True) expired_at = models.DateTimeField(blank=True, null=True) is_service = models.BooleanField(default=False) + allowed_rate_limit = models.CharField(max_length=255, default="60/min") class Meta: verbose_name = "API Token" diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index d3377f0ad3..277b4a174a 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -536,36 +536,6 @@ class IssueComment(ChangeTrackerMixin, ProjectBaseModel): return str(self.issue) -class IssueUserProperty(ProjectBaseModel): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="issue_property_user", - ) - filters = models.JSONField(default=get_default_filters) - display_filters = models.JSONField(default=get_default_display_filters) - display_properties = models.JSONField(default=get_default_display_properties) - rich_filters = models.JSONField(default=dict) - - class Meta: - verbose_name = "Issue User Property" - verbose_name_plural = "Issue User Properties" - db_table = "issue_user_properties" - ordering = ("-created_at",) - unique_together = ["user", "project", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["user", "project"], - condition=Q(deleted_at__isnull=True), - name="issue_user_property_unique_user_project_when_deleted_at_null", - ) - ] - - def __str__(self): - """Return properties status of the issue""" - return str(self.user) - - class IssueLabel(ProjectBaseModel): issue = models.ForeignKey("db.Issue", on_delete=models.CASCADE, related_name="label_issue") label = models.ForeignKey("db.Label", on_delete=models.CASCADE, related_name="label_issue") diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 8495ac9df4..166fb55b02 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -12,7 +12,6 @@ from django.db.models import Q # Module imports from plane.db.mixins import AuditModel -# Module imports from .base import BaseModel ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) @@ -313,3 +312,37 @@ class ProjectPublicMember(ProjectBaseModel): verbose_name_plural = "Project Public Members" db_table = "project_public_members" ordering = ("-created_at",) + + +class ProjectUserProperty(ProjectBaseModel): + + from .issue import get_default_filters, get_default_display_filters, get_default_display_properties + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="issue_property_user", + ) + filters = models.JSONField(default=get_default_filters) + display_filters = models.JSONField(default=get_default_display_filters) + display_properties = models.JSONField(default=get_default_display_properties) + rich_filters = models.JSONField(default=dict) + preferences = models.JSONField(default=get_default_preferences) + sort_order = models.FloatField(default=65535) + + class Meta: + verbose_name = "Issue User Property" + verbose_name_plural = "Issue User Properties" + db_table = "project_user_properties" + ordering = ("-created_at",) + unique_together = ["user", "project", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["user", "project"], + condition=Q(deleted_at__isnull=True), + name="issue_user_property_unique_user_project_when_deleted_at_null", + ) + ] + + def __str__(self): + """Return properties status of the issue""" + return str(self.user) diff --git a/apps/api/plane/tests/contract/app/test_project_app.py b/apps/api/plane/tests/contract/app/test_project_app.py index 38b0f51f3b..9f05314cfe 100644 --- a/apps/api/plane/tests/contract/app/test_project_app.py +++ b/apps/api/plane/tests/contract/app/test_project_app.py @@ -6,7 +6,7 @@ from django.utils import timezone from plane.db.models import ( Project, ProjectMember, - IssueUserProperty, + ProjectUserProperty, State, WorkspaceMember, User, @@ -82,8 +82,8 @@ class TestProjectAPIPost(TestProjectBase): assert project_member.role == 20 # Administrator assert project_member.is_active is True - # Verify IssueUserProperty was created - assert IssueUserProperty.objects.filter(project=project, user=user).exists() + # Verify ProjectUserProperty was created + assert ProjectUserProperty.objects.filter(project=project, user=user).exists() # Verify default states were created states = State.objects.filter(project=project) @@ -116,8 +116,8 @@ class TestProjectAPIPost(TestProjectBase): project = Project.objects.get(name=project_data["name"]) assert ProjectMember.objects.filter(project=project, role=20).count() == 2 - # Verify both have IssueUserProperty - assert IssueUserProperty.objects.filter(project=project).count() == 2 + # Verify both have ProjectUserProperty + assert ProjectUserProperty.objects.filter(project=project).count() == 2 @pytest.mark.django_db def test_create_project_guest_forbidden(self, session_client, workspace): From e43d9b65ed9bb5c0ad07beea93c6622af5dcaaee Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 19:06:08 +0530 Subject: [PATCH 2/8] migrate: move issue user properties to project user properties and update related fields and constraints --- .../0113_alter_issueuserproperty_table.py | 20 +++++++++++++++++-- ...r_projectuserproperty_options_and_more.py} | 7 ++----- apps/api/plane/db/models/project.py | 10 +++++----- 3 files changed, 25 insertions(+), 12 deletions(-) rename apps/api/plane/db/migrations/{0114_auto_20251201_0857.py => 0114_alter_projectuserproperty_options_and_more.py} (86%) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py index 3083f96654..645633eb05 100644 --- a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -2,8 +2,8 @@ from django.db import migrations, models import plane.db.models.project - - +import django.db.models.deletion +from django.conf import settings class Migration(migrations.Migration): @@ -34,5 +34,21 @@ class Migration(migrations.Migration): model_name='projectuserproperty', name='sort_order', field=models.FloatField(default=65535), + ), migrations.AlterModelOptions( + name='projectuserproperty', + options={'ordering': ('-created_at',), 'verbose_name': 'Project User Property', 'verbose_name_plural': 'Project User Properties'}, + ), + migrations.RemoveConstraint( + model_name='projectuserproperty', + name='issue_user_property_unique_user_project_when_deleted_at_null', + ), + migrations.AlterField( + model_name='projectuserproperty', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_property_user', to=settings.AUTH_USER_MODEL), + ), + migrations.AddConstraint( + model_name='projectuserproperty', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('user', 'project'), name='project_user_property_unique_user_project_when_deleted_at_null'), ), ] diff --git a/apps/api/plane/db/migrations/0114_auto_20251201_0857.py b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py similarity index 86% rename from apps/api/plane/db/migrations/0114_auto_20251201_0857.py rename to apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py index 0ca5961d2d..4fd35a9397 100644 --- a/apps/api/plane/db/migrations/0114_auto_20251201_0857.py +++ b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.25 on 2025-12-01 08:57 +# Generated by Django 4.2.25 on 2025-12-01 13:33 from django.db import migrations @@ -35,8 +35,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython( - move_issue_user_properties_to_project_user_properties, - reverse_code=migrations.RunPython.noop - ), + migrations.RunPython(move_issue_user_properties_to_project_user_properties, reverse_code=migrations.RunPython.noop), ] diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 166fb55b02..580c8db211 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -320,7 +320,7 @@ class ProjectUserProperty(ProjectBaseModel): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - related_name="issue_property_user", + related_name="project_property_user", ) filters = models.JSONField(default=get_default_filters) display_filters = models.JSONField(default=get_default_display_filters) @@ -330,8 +330,8 @@ class ProjectUserProperty(ProjectBaseModel): sort_order = models.FloatField(default=65535) class Meta: - verbose_name = "Issue User Property" - verbose_name_plural = "Issue User Properties" + verbose_name = "Project User Property" + verbose_name_plural = "Project User Properties" db_table = "project_user_properties" ordering = ("-created_at",) unique_together = ["user", "project", "deleted_at"] @@ -339,10 +339,10 @@ class ProjectUserProperty(ProjectBaseModel): models.UniqueConstraint( fields=["user", "project"], condition=Q(deleted_at__isnull=True), - name="issue_user_property_unique_user_project_when_deleted_at_null", + name="project_user_property_unique_user_project_when_deleted_at_null", ) ] def __str__(self): - """Return properties status of the issue""" + """Return properties status of the project""" return str(self.user) From d874227de30d59ee6023b4b12dfa9dae295a27ac Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 19:30:39 +0530 Subject: [PATCH 3/8] refactor: rename IssueUserPropertySerializer and IssueUserDisplayPropertyEndpoint to ProjectUserPropertySerializer and ProjectUserDisplayPropertyEndpoint, updating all related references --- apps/api/plane/app/serializers/__init__.py | 2 +- apps/api/plane/app/serializers/issue.py | 2 +- apps/api/plane/app/urls/issue.py | 4 ++-- apps/api/plane/app/views/__init__.py | 2 +- apps/api/plane/app/views/issue/base.py | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/api/plane/app/serializers/__init__.py b/apps/api/plane/app/serializers/__init__.py index 759f27ed6e..96046187f9 100644 --- a/apps/api/plane/app/serializers/__init__.py +++ b/apps/api/plane/app/serializers/__init__.py @@ -52,7 +52,7 @@ from .issue import ( IssueCreateSerializer, IssueActivitySerializer, IssueCommentSerializer, - IssueUserPropertySerializer, + ProjectUserPropertySerializer, IssueAssigneeSerializer, LabelSerializer, IssueSerializer, diff --git a/apps/api/plane/app/serializers/issue.py b/apps/api/plane/app/serializers/issue.py index c71bd8447f..c8f530ba18 100644 --- a/apps/api/plane/app/serializers/issue.py +++ b/apps/api/plane/app/serializers/issue.py @@ -346,7 +346,7 @@ class IssueActivitySerializer(BaseSerializer): fields = "__all__" -class IssueUserPropertySerializer(BaseSerializer): +class ProjectUserPropertySerializer(BaseSerializer): class Meta: model = ProjectUserProperty fields = "__all__" diff --git a/apps/api/plane/app/urls/issue.py b/apps/api/plane/app/urls/issue.py index e39ce9f9c5..1fd7741206 100644 --- a/apps/api/plane/app/urls/issue.py +++ b/apps/api/plane/app/urls/issue.py @@ -14,7 +14,7 @@ from plane.app.views import ( IssueReactionViewSet, IssueRelationViewSet, IssueSubscriberViewSet, - IssueUserDisplayPropertyEndpoint, + ProjectUserDisplayPropertyEndpoint, IssueViewSet, LabelViewSet, BulkArchiveIssuesEndpoint, @@ -211,7 +211,7 @@ urlpatterns = [ ## ProjectUserProperty path( "workspaces//projects//user-properties/", - IssueUserDisplayPropertyEndpoint.as_view(), + ProjectUserDisplayPropertyEndpoint.as_view(), name="project-issue-display-properties", ), ## ProjectUserProperty End diff --git a/apps/api/plane/app/views/__init__.py b/apps/api/plane/app/views/__init__.py index 5f848a5ba0..6982b2bcfa 100644 --- a/apps/api/plane/app/views/__init__.py +++ b/apps/api/plane/app/views/__init__.py @@ -115,7 +115,7 @@ from .asset.v2 import ( from .issue.base import ( IssueListEndpoint, IssueViewSet, - IssueUserDisplayPropertyEndpoint, + ProjectUserDisplayPropertyEndpoint, BulkDeleteIssuesEndpoint, DeletedIssuesListViewSet, IssuePaginatedViewSet, diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index 9dace60e76..b938dc7ea7 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -34,7 +34,7 @@ from plane.app.serializers import ( IssueDetailSerializer, IssueListDetailSerializer, IssueSerializer, - IssueUserPropertySerializer, + ProjectUserPropertySerializer, ) from plane.bgtasks.issue_activities_task import issue_activity from plane.bgtasks.issue_description_version_task import issue_description_version_task @@ -715,7 +715,7 @@ class IssueViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class IssueUserDisplayPropertyEndpoint(BaseAPIView): +class ProjectUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): issue_property = ProjectUserProperty.objects.get(user=request.user, project_id=project_id) @@ -725,13 +725,13 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters) issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties) issue_property.save() - serializer = IssueUserPropertySerializer(issue_property) + serializer = ProjectUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_201_CREATED) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id) - serializer = IssueUserPropertySerializer(issue_property) + serializer = ProjectUserPropertySerializer(issue_property) return Response(serializer.data, status=status.HTTP_200_OK) From 280c3cec3d01483b4dc583d006b921723521334d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 1 Dec 2025 19:54:53 +0530 Subject: [PATCH 4/8] fix: enhance ProjectUserDisplayPropertyEndpoint to handle missing properties by creating new entries and improve response handling --- apps/api/plane/app/views/issue/base.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index b938dc7ea7..79d34501b6 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -718,15 +718,25 @@ class IssueViewSet(BaseViewSet): class ProjectUserDisplayPropertyEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id): - issue_property = ProjectUserProperty.objects.get(user=request.user, project_id=project_id) + try: + issue_property = ProjectUserProperty.objects.get( + user=request.user, + project_id=project_id + ) + except ProjectUserProperty.DoesNotExist: + issue_property = ProjectUserProperty.objects.create( + user=request.user, + project_id=project_id + ) - issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters) - issue_property.filters = request.data.get("filters", issue_property.filters) - issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters) - issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties) - issue_property.save() - serializer = ProjectUserPropertySerializer(issue_property) - return Response(serializer.data, status=status.HTTP_201_CREATED) + serializer = ProjectUserPropertySerializer( + issue_property, + data=request.data, + partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id): From e7cf8fa9254939c06a0eecb9421d073b4c528d3c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 3 Dec 2025 14:36:37 +0530 Subject: [PATCH 5/8] fix: correct formatting in migration for ProjectUserProperty model options --- .../plane/db/migrations/0113_alter_issueuserproperty_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py index 645633eb05..33365c3c25 100644 --- a/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py +++ b/apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py @@ -34,7 +34,8 @@ class Migration(migrations.Migration): model_name='projectuserproperty', name='sort_order', field=models.FloatField(default=65535), - ), migrations.AlterModelOptions( + ), + migrations.AlterModelOptions( name='projectuserproperty', options={'ordering': ('-created_at',), 'verbose_name': 'Project User Property', 'verbose_name_plural': 'Project User Properties'}, ), From 2f0ea09f1537f098ff806daf882f5514d9ebb79e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 17:42:51 +0530 Subject: [PATCH 6/8] migrate: add migration to update existing non-service API tokens to remove workspace association --- ...114_alter_projectuserproperty_options_and_more.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py index 4fd35a9397..2352020118 100644 --- a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py +++ b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py @@ -28,6 +28,17 @@ def move_issue_user_properties_to_project_user_properties(apps, schema_editor): +def migrate_existing_api_tokens(apps, schema_editor): + APIToken = apps.get_model('db', 'APIToken') + + # Update all the existing non-service api tokens to not have a workspace + APIToken.objects.filter(is_service=False).update( + workspace_id=None, + user__is_bot=False + ) + return + + class Migration(migrations.Migration): dependencies = [ @@ -36,4 +47,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(move_issue_user_properties_to_project_user_properties, reverse_code=migrations.RunPython.noop), + migrations.RunPython(migrate_existing_api_tokens, reverse_code=migrations.RunPython.noop), ] From ab9a6b4466b1cd0384271257f310f74f5dc1c094 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 8 Dec 2025 17:50:59 +0530 Subject: [PATCH 7/8] migrate: refine migration to update existing non-service API tokens by excluding bot users from workspace removal --- .../0114_alter_projectuserproperty_options_and_more.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py index 2352020118..75714243fc 100644 --- a/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py +++ b/apps/api/plane/db/migrations/0114_alter_projectuserproperty_options_and_more.py @@ -32,9 +32,9 @@ def migrate_existing_api_tokens(apps, schema_editor): APIToken = apps.get_model('db', 'APIToken') # Update all the existing non-service api tokens to not have a workspace - APIToken.objects.filter(is_service=False).update( + APIToken.objects.filter(is_service=False, user__is_bot=False).update( workspace_id=None, - user__is_bot=False + ) return From d75efb3cfd1c896c1c81b113ff7c1da851c6d8b3 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 18 Dec 2025 18:11:53 +0530 Subject: [PATCH 8/8] chore: changed the project sort order in project user property --- apps/api/plane/api/views/project.py | 7 ----- apps/api/plane/app/views/project/base.py | 5 ---- apps/api/plane/app/views/project/invite.py | 2 +- apps/api/plane/app/views/project/member.py | 26 +++++++++---------- .../commands/create_project_member.py | 13 ++-------- apps/api/plane/db/models/project.py | 22 ++++++++++------ 6 files changed, 30 insertions(+), 45 deletions(-) diff --git a/apps/api/plane/api/views/project.py b/apps/api/plane/api/views/project.py index 698343ded1..5db5ae1350 100644 --- a/apps/api/plane/api/views/project.py +++ b/apps/api/plane/api/views/project.py @@ -216,8 +216,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView): # Add the user as Administrator to the project _ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20) - # Also create the issue property for the user - _ = ProjectUserProperty.objects.create(project_id=serializer.instance.id, user=request.user) if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str( request.user.id @@ -227,11 +225,6 @@ class ProjectListCreateAPIEndpoint(BaseAPIView): member_id=serializer.instance.project_lead, role=20, ) - # Also create the issue property for the user - ProjectUserProperty.objects.create( - project_id=serializer.instance.id, - user_id=serializer.instance.project_lead, - ) State.objects.bulk_create( [ diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 504211760d..d2ed934212 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -266,11 +266,6 @@ class ProjectViewSet(BaseViewSet): member_id=serializer.data["project_lead"], role=ROLE.ADMIN.value, ) - # Also create the issue property for the user - ProjectUserProperty.objects.create( - project_id=serializer.data["id"], - user_id=serializer.data["project_lead"], - ) State.objects.bulk_create( [ diff --git a/apps/api/plane/app/views/project/invite.py b/apps/api/plane/app/views/project/invite.py index 6ae5a5e11a..f4ea9ba41a 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -220,7 +220,7 @@ class ProjectJoinEndpoint(BaseAPIView): if project_member is None: # Create a Project Member _ = ProjectMember.objects.create( - workspace_id=project_invite.workspace_id, + project_id=project_id, member=user, role=project_invite.role, ) diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py index 664355a17a..ebb2a830f6 100644 --- a/apps/api/plane/app/views/project/member.py +++ b/apps/api/plane/app/views/project/member.py @@ -1,6 +1,7 @@ # Third Party imports from rest_framework.response import Response from rest_framework import status +from django.db.models import Min # Module imports from .base import BaseViewSet, BaseAPIView @@ -89,24 +90,23 @@ class ProjectMemberViewSet(BaseViewSet): # Update the roles of the existing members ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100) - # Get the list of project members of the requested workspace with the given slug - project_members = ( - ProjectMember.objects.filter( + # Get the minimum sort_order for each member in the workspace + member_sort_orders = ( + ProjectUserProperty.objects.filter( workspace__slug=slug, - member_id__in=[member.get("member_id") for member in members], + user_id__in=[member.get("member_id") for member in members], ) - .values("member_id", "sort_order") - .order_by("sort_order") + .values("user_id") + .annotate(min_sort_order=Min("sort_order")) ) + # Convert to dictionary for easy lookup: {user_id: min_sort_order} + sort_order_map = {str(item["user_id"]): item["min_sort_order"] for item in member_sort_orders} # Loop through requested members for member in members: - # Get the sort orders of the member - sort_order = [ - project_member.get("sort_order") - for project_member in project_members - if str(project_member.get("member_id")) == str(member.get("member_id")) - ] + member_id = str(member.get("member_id")) + # Get the minimum sort_order for this member, or use default + min_sort_order = sort_order_map.get(member_id) # Create a new project member bulk_project_members.append( ProjectMember( @@ -114,7 +114,6 @@ class ProjectMemberViewSet(BaseViewSet): role=member.get("role", 5), project_id=project_id, workspace_id=project.workspace_id, - sort_order=(sort_order[0] - 10000 if len(sort_order) else 65535), ) ) # Create a new issue property @@ -123,6 +122,7 @@ class ProjectMemberViewSet(BaseViewSet): user_id=member.get("member_id"), project_id=project_id, workspace_id=project.workspace_id, + sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535), ) ) diff --git a/apps/api/plane/db/management/commands/create_project_member.py b/apps/api/plane/db/management/commands/create_project_member.py index a097056b99..c04dbda7e7 100644 --- a/apps/api/plane/db/management/commands/create_project_member.py +++ b/apps/api/plane/db/management/commands/create_project_member.py @@ -47,24 +47,15 @@ class Command(BaseCommand): if not WorkspaceMember.objects.filter(workspace=project.workspace, member=user, is_active=True).exists(): raise CommandError("User not member in workspace") - # Get the smallest sort order - smallest_sort_order = ( - ProjectMember.objects.filter(workspace_id=project.workspace_id).order_by("sort_order").first() - ) - - if smallest_sort_order: - sort_order = smallest_sort_order.sort_order - 1000 - else: - sort_order = 65535 if ProjectMember.objects.filter(project=project, member=user).exists(): # Update the project member ProjectMember.objects.filter(project=project, member=user).update( - is_active=True, sort_order=sort_order, role=role + is_active=True, role=role ) else: # Create the project member - ProjectMember.objects.create(project=project, member=user, role=role, sort_order=sort_order) + ProjectMember.objects.create(project=project, member=user, role=role) # Issue Property ProjectUserProperty.objects.get_or_create(user=user, project=project) diff --git a/apps/api/plane/db/models/project.py b/apps/api/plane/db/models/project.py index 580c8db211..6cd8050178 100644 --- a/apps/api/plane/db/models/project.py +++ b/apps/api/plane/db/models/project.py @@ -205,14 +205,20 @@ class ProjectMember(ProjectBaseModel): is_active = models.BooleanField(default=True) def save(self, *args, **kwargs): - if self._state.adding: - smallest_sort_order = ProjectMember.objects.filter( - workspace_id=self.project.workspace_id, member=self.member - ).aggregate(smallest=models.Min("sort_order"))["smallest"] + if self._state.adding and self.member: + # Get the minimum sort_order for this member in the workspace + min_sort_order_result = ProjectUserProperty.objects.filter( + workspace_id=self.project.workspace_id, user=self.member + ).aggregate(min_sort_order=models.Min("sort_order")) + min_sort_order = min_sort_order_result.get("min_sort_order") - # Project ordering - if smallest_sort_order is not None: - self.sort_order = smallest_sort_order - 10000 + # create project user property with project sort order + ProjectUserProperty.objects.create( + workspace_id=self.project.workspace_id, + project=self.project, + user=self.member, + sort_order=(min_sort_order - 10000 if min_sort_order is not None else 65535), + ) super(ProjectMember, self).save(*args, **kwargs) @@ -315,8 +321,8 @@ class ProjectPublicMember(ProjectBaseModel): class ProjectUserProperty(ProjectBaseModel): - from .issue import get_default_filters, get_default_display_filters, get_default_display_properties + user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE,