Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/api/plane/api/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from plane.db.models import (
Cycle,
Intake,
IssueUserProperty,
ProjectUserProperty,
Module,
Project,
DeployBoard,
Expand Down Expand Up @@ -217,7 +217,7 @@ def post(self, request, slug):
# 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
Expand All @@ -228,7 +228,7 @@ def post(self, request, slug):
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,
)
Expand Down
2 changes: 1 addition & 1 deletion apps/api/plane/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
IssueUserPropertySerializer,
ProjectUserPropertySerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
Expand Down
6 changes: 3 additions & 3 deletions apps/api/plane/app/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Issue,
IssueActivity,
IssueComment,
IssueUserProperty,
ProjectUserProperty,
IssueAssignee,
IssueSubscriber,
IssueLabel,
Expand Down Expand Up @@ -346,9 +346,9 @@ class Meta:
fields = "__all__"


class IssueUserPropertySerializer(BaseSerializer):
class ProjectUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
model = ProjectUserProperty
fields = "__all__"
read_only_fields = ["user", "workspace", "project"]

Expand Down
8 changes: 4 additions & 4 deletions apps/api/plane/app/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
IssueReactionViewSet,
IssueRelationViewSet,
IssueSubscriberViewSet,
IssueUserDisplayPropertyEndpoint,
ProjectUserDisplayPropertyEndpoint,
IssueViewSet,
LabelViewSet,
BulkArchiveIssuesEndpoint,
Expand Down Expand Up @@ -208,13 +208,13 @@
name="project-issue-comment-reactions",
),
## End Comment Reactions
## IssueUserProperty
## ProjectUserProperty
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(),
ProjectUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties",
),
## IssueUserProperty End
## ProjectUserProperty End
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
from .issue.base import (
IssueListEndpoint,
IssueViewSet,
IssueUserDisplayPropertyEndpoint,
ProjectUserDisplayPropertyEndpoint,
BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
Expand Down
36 changes: 23 additions & 13 deletions apps/api/plane/app/views/issue/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
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
Expand All @@ -51,7 +51,7 @@
IssueReaction,
IssueRelation,
IssueSubscriber,
IssueUserProperty,
ProjectUserProperty,
ModuleIssue,
Project,
ProjectMember,
Expand Down Expand Up @@ -715,23 +715,33 @@ def destroy(self, request, slug, project_id, pk=None):
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 = IssueUserProperty.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 = IssueUserPropertySerializer(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):
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
serializer = IssueUserPropertySerializer(issue_property)
issue_property, _ = ProjectUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
serializer = ProjectUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK)


Expand Down
7 changes: 4 additions & 3 deletions apps/api/plane/app/views/project/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
from plane.bgtasks.webhook_task import model_activity, webhook_activity
from plane.db.models import (
DeployBoard,
ProjectUserProperty,
Intake,
IssueUserProperty,
Project,
ProjectIdentifier,
ProjectMember,
ProjectNetwork,
State,
DEFAULT_STATES,
UserFavorite,
Workspace,
WorkspaceMember,
)
Expand Down Expand Up @@ -255,7 +256,7 @@ def create(self, request, slug):
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate ProjectUserProperty creation causes IntegrityError

The ProjectMember.save() method now automatically creates a ProjectUserProperty when a new member is added. However, line 259 still explicitly calls ProjectUserProperty.objects.create() after ProjectMember.objects.create(). This results in attempting to create two ProjectUserProperty records for the same user and project, which violates the unique constraint and will raise an IntegrityError. The explicit creation at line 259 needs to be removed, similar to how it was correctly removed in apps/api/plane/api/views/project.py.

Additional Locations (1)

Fix in Cursor Fix in Web


if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
request.user.id
Expand All @@ -266,7 +267,7 @@ def create(self, request, slug):
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"],
)
Expand Down
6 changes: 3 additions & 3 deletions apps/api/plane/app/views/project/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
User,
WorkspaceMember,
Project,
IssueUserProperty,
ProjectUserProperty,
)
from plane.db.models.project import ProjectNetwork
from plane.utils.host import base_host
Expand Down Expand Up @@ -160,9 +160,9 @@ def create(self, request, slug):
ignore_conflicts=True,
)

IssueUserProperty.objects.bulk_create(
ProjectUserProperty.objects.bulk_create(
[
IssueUserProperty(
ProjectUserProperty(
project_id=project_id,
user=request.user,
workspace=workspace,
Expand Down
6 changes: 3 additions & 3 deletions apps/api/plane/app/views/project/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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
Expand Down Expand Up @@ -119,7 +119,7 @@ def create(self, request, slug, project_id):
)
# 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,
Expand All @@ -129,7 +129,7 @@ def create(self, request, slug, project_id):
# 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,
Expand Down
6 changes: 3 additions & 3 deletions apps/api/plane/bgtasks/workspace_seed_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
WorkspaceMember,
Project,
ProjectMember,
IssueUserProperty,
ProjectUserProperty,
State,
Label,
Issue,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
WorkspaceMember,
ProjectMember,
Project,
IssueUserProperty,
ProjectUserProperty,
)


Expand Down Expand Up @@ -67,7 +67,7 @@ def handle(self, *args: Any, **options: Any):
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}"))
Expand Down
55 changes: 55 additions & 0 deletions apps/api/plane/db/migrations/0113_alter_issueuserproperty_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 4.2.25 on 2025-11-28 14:36

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):

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),
),
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'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generated by Django 4.2.25 on 2025-12-01 13:33

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)



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 = [
('db', '0113_alter_issueuserproperty_table'),
]

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),
]
Loading