mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
Merge branch 'chore-user-property-migrations' of github.com:makeplane/plane into workspace-api-tokens
This commit is contained in:
@@ -18,7 +18,7 @@ from drf_spectacular.utils import OpenApiResponse, OpenApiRequest
|
||||
from plane.db.models import (
|
||||
Cycle,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
Module,
|
||||
Project,
|
||||
DeployBoard,
|
||||
@@ -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
|
||||
_ = IssueUserProperty.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
|
||||
IssueUserProperty.objects.create(
|
||||
project_id=serializer.instance.id,
|
||||
user_id=serializer.instance.project_lead,
|
||||
)
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
|
||||
@@ -52,7 +52,7 @@ from .issue import (
|
||||
IssueCreateSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueUserPropertySerializer,
|
||||
ProjectUserPropertySerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.db.models import (
|
||||
Issue,
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
@@ -346,9 +346,9 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IssueUserPropertySerializer(BaseSerializer):
|
||||
class ProjectUserPropertySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueUserProperty
|
||||
model = ProjectUserProperty
|
||||
fields = "__all__"
|
||||
read_only_fields = ["user", "workspace", "project"]
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from plane.app.views import (
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
IssueViewSet,
|
||||
LabelViewSet,
|
||||
BulkArchiveIssuesEndpoint,
|
||||
@@ -208,13 +208,13 @@ urlpatterns = [
|
||||
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/",
|
||||
|
||||
@@ -115,7 +115,7 @@ from .asset.v2 import (
|
||||
from .issue.base import (
|
||||
IssueListEndpoint,
|
||||
IssueViewSet,
|
||||
IssueUserDisplayPropertyEndpoint,
|
||||
ProjectUserDisplayPropertyEndpoint,
|
||||
BulkDeleteIssuesEndpoint,
|
||||
DeletedIssuesListViewSet,
|
||||
IssuePaginatedViewSet,
|
||||
|
||||
@@ -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
|
||||
@@ -51,7 +51,7 @@ from plane.db.models import (
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSubscriber,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
ModuleIssue,
|
||||
Project,
|
||||
ProjectMember,
|
||||
@@ -715,23 +715,33 @@ 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 = 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)
|
||||
|
||||
|
||||
|
||||
@@ -27,14 +27,15 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.db.models import (
|
||||
UserFavorite,
|
||||
DeployBoard,
|
||||
ProjectUserProperty,
|
||||
Intake,
|
||||
IssueUserProperty,
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
ProjectNetwork,
|
||||
State,
|
||||
DEFAULT_STATES,
|
||||
UserFavorite,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
)
|
||||
@@ -256,7 +257,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
|
||||
@@ -266,11 +267,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
member_id=serializer.data["project_lead"],
|
||||
role=ROLE.ADMIN.value,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
user_id=serializer.data["project_lead"],
|
||||
)
|
||||
|
||||
State.objects.bulk_create(
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -13,7 +14,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
|
||||
@@ -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,22 +114,22 @@ 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
|
||||
bulk_issue_props.append(
|
||||
IssueUserProperty(
|
||||
ProjectUserProperty(
|
||||
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),
|
||||
)
|
||||
)
|
||||
|
||||
# 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,7 @@ from plane.db.models import (
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
Project,
|
||||
IssueUserProperty,
|
||||
ProjectUserProperty,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,27 +47,18 @@ 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
|
||||
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}"))
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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, user__is_bot=False).update(
|
||||
workspace_id=None,
|
||||
|
||||
)
|
||||
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),
|
||||
]
|
||||
@@ -34,7 +34,6 @@ from .issue import (
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
IssueMention,
|
||||
IssueUserProperty,
|
||||
IssueReaction,
|
||||
IssueRelation,
|
||||
IssueSequence,
|
||||
@@ -54,6 +53,7 @@ from .project import (
|
||||
ProjectMemberInvite,
|
||||
ProjectNetwork,
|
||||
ProjectPublicMember,
|
||||
ProjectUserProperty,
|
||||
)
|
||||
from .session import Session
|
||||
from .social_connection import SocialLoginConnection
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
@@ -206,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)
|
||||
|
||||
@@ -313,3 +318,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="project_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 = "Project User Property"
|
||||
verbose_name_plural = "Project 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="project_user_property_unique_user_project_when_deleted_at_null",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Return properties status of the project"""
|
||||
return str(self.user)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user