From 0d70397639e3cda0e939007fc12f0afa76152343 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Thu, 28 Nov 2024 12:42:30 +0530 Subject: [PATCH] chore: issue version migrations updates --- apiserver/plane/app/serializers/__init__.py | 1 - apiserver/plane/app/serializers/workspace.py | 49 ---- apiserver/plane/app/urls/project.py | 6 - apiserver/plane/app/urls/workspace.py | 22 +- apiserver/plane/app/views/__init__.py | 5 - apiserver/plane/app/views/project/member.py | 83 ++---- apiserver/plane/app/views/workspace/member.py | 85 ++---- ...alter_teampage_unique_together_and_more.py | 242 ++++++++++++++++++ apiserver/plane/db/models/__init__.py | 2 - apiserver/plane/db/models/asset.py | 29 ++- apiserver/plane/db/models/issue.py | 126 ++++++++- apiserver/plane/db/models/page.py | 29 --- apiserver/plane/db/models/webhook.py | 9 +- apiserver/plane/db/models/workspace.py | 66 ++--- 14 files changed, 473 insertions(+), 281 deletions(-) create mode 100644 apiserver/plane/db/migrations/0086_issueversion_alter_teampage_unique_together_and_more.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 2e7022688..cd9adb939 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -13,7 +13,6 @@ from .user import ( from .workspace import ( WorkSpaceSerializer, WorkSpaceMemberSerializer, - TeamSerializer, WorkSpaceMemberInviteSerializer, WorkspaceLiteSerializer, WorkspaceThemeSerializer, diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 03e64e32a..49cd55bf7 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -6,11 +6,8 @@ from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( - User, Workspace, WorkspaceMember, - Team, - TeamMember, WorkspaceMemberInvite, WorkspaceTheme, WorkspaceUserProperties, @@ -97,52 +94,6 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer): ] -class TeamSerializer(BaseSerializer): - members_detail = UserLiteSerializer(read_only=True, source="members", many=True) - members = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), - write_only=True, - required=False, - ) - - class Meta: - model = Team - fields = "__all__" - read_only_fields = [ - "workspace", - "created_by", - "updated_by", - "created_at", - "updated_at", - ] - - def create(self, validated_data, **kwargs): - if "members" in validated_data: - members = validated_data.pop("members") - workspace = self.context["workspace"] - team = Team.objects.create(**validated_data, workspace=workspace) - team_members = [ - TeamMember(member=member, team=team, workspace=workspace) - for member in members - ] - TeamMember.objects.bulk_create(team_members, batch_size=10) - return team - team = Team.objects.create(**validated_data) - return team - - def update(self, instance, validated_data): - if "members" in validated_data: - members = validated_data.pop("members") - TeamMember.objects.filter(team=instance).delete() - team_members = [ - TeamMember(member=member, team=instance, workspace=instance.workspace) - for member in members - ] - TeamMember.objects.bulk_create(team_members, batch_size=10) - return super().update(instance, validated_data) - return super().update(instance, validated_data) - - class WorkspaceThemeSerializer(BaseSerializer): class Meta: model = WorkspaceTheme diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py index 3f41caee8..4037402ab 100644 --- a/apiserver/plane/app/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -7,7 +7,6 @@ from plane.app.views import ( ProjectMemberViewSet, ProjectMemberUserEndpoint, ProjectJoinEndpoint, - AddTeamToProjectEndpoint, ProjectUserViewsEndpoint, ProjectIdentifierEndpoint, ProjectFavoritesViewSet, @@ -83,11 +82,6 @@ urlpatterns = [ ProjectMemberViewSet.as_view({"post": "leave"}), name="project-member", ), - path( - "workspaces//projects//team-invite/", - AddTeamToProjectEndpoint.as_view(), - name="projects", - ), path( "workspaces//projects//project-views/", ProjectUserViewsEndpoint.as_view(), diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 00e996995..26e623864 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -10,7 +10,6 @@ from plane.app.views import ( WorkspaceMemberUserEndpoint, WorkspaceMemberUserViewsEndpoint, WorkSpaceAvailabilityCheckEndpoint, - TeamMemberViewSet, UserLastProjectWithWorkspaceEndpoint, WorkspaceThemeViewSet, WorkspaceUserProfileStatsEndpoint, @@ -69,7 +68,9 @@ urlpatterns = [ # user workspace invitations path( "users/me/workspaces/invitations/", - UserWorkspaceInvitationsViewSet.as_view({"get": "list", "post": "create"}), + UserWorkspaceInvitationsViewSet.as_view( + {"get": "list", "post": "create"} + ), name="user-workspace-invitations", ), path( @@ -100,23 +101,6 @@ urlpatterns = [ WorkSpaceMemberViewSet.as_view({"post": "leave"}), name="leave-workspace-members", ), - path( - "workspaces//teams/", - TeamMemberViewSet.as_view({"get": "list", "post": "create"}), - name="workspace-team-members", - ), - path( - "workspaces//teams//", - TeamMemberViewSet.as_view( - { - "put": "update", - "patch": "partial_update", - "delete": "destroy", - "get": "retrieve", - } - ), - name="workspace-team-members", - ), path( "users/last-visited-workspace/", UserLastProjectWithWorkspaceEndpoint.as_view(), diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index c21000a4e..581a1065d 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -16,7 +16,6 @@ from .project.invite import ( from .project.member import ( ProjectMemberViewSet, - AddTeamToProjectEndpoint, ProjectMemberUserEndpoint, UserProjectRolesEndpoint, ) @@ -49,7 +48,6 @@ from .workspace.favorite import ( from .workspace.member import ( WorkSpaceMemberViewSet, - TeamMemberViewSet, WorkspaceMemberUserEndpoint, WorkspaceProjectMemberEndpoint, WorkspaceMemberUserViewsEndpoint, @@ -88,8 +86,6 @@ from .cycle.base import ( CycleFavoriteViewSet, TransferCycleIssueEndpoint, CycleUserPropertiesEndpoint, - CycleViewSet, - TransferCycleIssueEndpoint, CycleAnalyticsEndpoint, CycleProgressEndpoint, ) @@ -206,6 +202,5 @@ from .dashboard.base import DashboardEndpoint, WidgetsEndpoint from .error_404 import custom_404_view -from .exporter.base import ExportIssuesEndpoint from .notification.base import MarkAllReadNotificationViewSet from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index f46bf6c50..c274d87c5 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -11,7 +11,6 @@ from plane.app.serializers import ( ) from plane.app.permissions import ( - ProjectBasePermission, ProjectMemberPermission, ProjectLitePermission, WorkspaceUserPermission, @@ -20,8 +19,6 @@ from plane.app.permissions import ( from plane.db.models import ( Project, ProjectMember, - Workspace, - TeamMember, IssueUserProperty, WorkspaceMember, ) @@ -86,7 +83,10 @@ class ProjectMemberViewSet(BaseViewSet): workspace_member_role = WorkspaceMember.objects.get( workspace__slug=slug, member=member, is_active=True ).role - if workspace_member_role in [20] and member_roles.get(member) in [5, 15]: + if workspace_member_role in [20] and member_roles.get(member) in [ + 5, + 15, + ]: return Response( { "error": "You cannot add a user with role lower than the workspace role" @@ -94,7 +94,10 @@ class ProjectMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - if workspace_member_role in [5] and member_roles.get(member) in [15, 20]: + if workspace_member_role in [5] and member_roles.get(member) in [ + 15, + 20, + ]: return Response( { "error": "You cannot add a user with role higher than the workspace role" @@ -132,7 +135,8 @@ class ProjectMemberViewSet(BaseViewSet): 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")) + if str(project_member.get("member_id")) + == str(member.get("member_id")) ] # Create a new project member bulk_project_members.append( @@ -141,7 +145,9 @@ 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), + sort_order=( + sort_order[0] - 10000 if len(sort_order) else 65535 + ), ) ) # Create a new issue property @@ -232,7 +238,9 @@ class ProjectMemberViewSet(BaseViewSet): > requested_project_member.role ): return Response( - {"error": "You cannot update a role that is higher than your own role"}, + { + "error": "You cannot update a role that is higher than your own role" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -272,7 +280,9 @@ class ProjectMemberViewSet(BaseViewSet): # User cannot deactivate higher role if requesting_project_member.role < project_member.role: return Response( - {"error": "You cannot remove a user having role higher than you"}, + { + "error": "You cannot remove a user having role higher than you" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -293,7 +303,10 @@ class ProjectMemberViewSet(BaseViewSet): if ( project_member.role == 20 and not ProjectMember.objects.filter( - workspace__slug=slug, project_id=project_id, role=20, is_active=True + workspace__slug=slug, + project_id=project_id, + role=20, + is_active=True, ).count() > 1 ): @@ -309,53 +322,6 @@ class ProjectMemberViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class AddTeamToProjectEndpoint(BaseAPIView): - permission_classes = [ProjectBasePermission] - - def post(self, request, slug, project_id): - team_members = TeamMember.objects.filter( - workspace__slug=slug, team__in=request.data.get("teams", []) - ).values_list("member", flat=True) - - if len(team_members) == 0: - return Response( - {"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST - ) - - workspace = Workspace.objects.get(slug=slug) - - project_members = [] - issue_props = [] - for member in team_members: - project_members.append( - ProjectMember( - project_id=project_id, - member_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - issue_props.append( - IssueUserProperty( - project_id=project_id, - user_id=member, - workspace=workspace, - created_by=request.user, - ) - ) - - ProjectMember.objects.bulk_create( - project_members, batch_size=10, ignore_conflicts=True - ) - - _ = IssueUserProperty.objects.bulk_create( - issue_props, batch_size=10, ignore_conflicts=True - ) - - serializer = ProjectMemberSerializer(project_members, many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - class ProjectMemberUserEndpoint(BaseAPIView): def get(self, request, slug, project_id): project_member = ProjectMember.objects.get( @@ -378,6 +344,7 @@ class UserProjectRolesEndpoint(BaseAPIView): ).values("project_id", "role") project_members = { - str(member["project_id"]): member["role"] for member in project_members + str(member["project_id"]): member["role"] + for member in project_members } return Response(project_members, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index 3f5a4bd2b..91a89ad07 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -1,14 +1,18 @@ # Django imports -from django.db.models import CharField, Count, Q, OuterRef, Subquery, IntegerField +from django.db.models import ( + Count, + Q, + OuterRef, + Subquery, + IntegerField, +) from django.db.models.functions import Coalesce -from django.db.models.functions import Cast # Third party modules from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ( - WorkSpaceAdminPermission, WorkspaceEntityPermission, allow_permission, ROLE, @@ -17,8 +21,6 @@ from plane.app.permissions import ( # Module imports from plane.app.serializers import ( ProjectMemberRoleSerializer, - TeamSerializer, - UserLiteSerializer, WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, WorkSpaceMemberSerializer, @@ -27,9 +29,6 @@ from plane.app.views.base import BaseAPIView from plane.db.models import ( Project, ProjectMember, - Team, - User, - Workspace, WorkspaceMember, DraftIssue, ) @@ -120,7 +119,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): if requesting_workspace_member.role < workspace_member.role: return Response( - {"error": "You cannot remove a user having role higher than you"}, + { + "error": "You cannot remove a user having role higher than you" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -147,7 +148,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): # Deactivate the users from the projects where the user is part of _ = ProjectMember.objects.filter( - workspace__slug=slug, member_id=workspace_member.member_id, is_active=True + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, ).update(is_active=False) workspace_member.is_active = False @@ -161,7 +164,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): multiple=True, ) @invalidate_cache(path="/api/users/me/settings/") - @invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True) + @invalidate_cache( + path="api/users/me/workspaces/", user=False, multiple=True + ) @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" ) @@ -208,7 +213,9 @@ class WorkSpaceMemberViewSet(BaseViewSet): # # Deactivate the users from the projects where the user is part of _ = ProjectMember.objects.filter( - workspace__slug=slug, member_id=workspace_member.member_id, is_active=True + workspace__slug=slug, + member_id=workspace_member.member_id, + is_active=True, ).update(is_active=False) # # Deactivate the user @@ -272,7 +279,9 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView): project_members = ProjectMember.objects.filter( workspace__slug=slug, project_id__in=project_ids, is_active=True ).select_related("project", "member", "workspace") - project_members = ProjectMemberRoleSerializer(project_members, many=True).data + project_members = ProjectMemberRoleSerializer( + project_members, many=True + ).data project_members_dict = dict() @@ -284,53 +293,3 @@ class WorkspaceProjectMemberEndpoint(BaseAPIView): project_members_dict[str(project_id)].append(project_member) return Response(project_members_dict, status=status.HTTP_200_OK) - - -class TeamMemberViewSet(BaseViewSet): - serializer_class = TeamSerializer - model = Team - permission_classes = [WorkSpaceAdminPermission] - - search_fields = ["member__display_name", "member__first_name"] - - def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "workspace__owner") - .prefetch_related("members") - ) - - def create(self, request, slug): - members = list( - WorkspaceMember.objects.filter( - workspace__slug=slug, - member__id__in=request.data.get("members", []), - is_active=True, - ) - .annotate(member_str_id=Cast("member", output_field=CharField())) - .distinct() - .values_list("member_str_id", flat=True) - ) - - if len(members) != len(request.data.get("members", [])): - users = list(set(request.data.get("members", [])).difference(members)) - users = User.objects.filter(pk__in=users) - - serializer = UserLiteSerializer(users, many=True) - return Response( - { - "error": f"{len(users)} of the member(s) are not a part of the workspace", - "members": serializer.data, - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - workspace = Workspace.objects.get(slug=slug) - - serializer = TeamSerializer(data=request.data, context={"workspace": workspace}) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/db/migrations/0086_issueversion_alter_teampage_unique_together_and_more.py b/apiserver/plane/db/migrations/0086_issueversion_alter_teampage_unique_together_and_more.py new file mode 100644 index 000000000..d38f17c5d --- /dev/null +++ b/apiserver/plane/db/migrations/0086_issueversion_alter_teampage_unique_together_and_more.py @@ -0,0 +1,242 @@ +# Generated by Django 4.2.15 on 2024-11-27 09:07 + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import plane.db.models.webhook +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0085_intake_intakeissue_remove_inboxissue_created_by_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="IssueVersion", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("parent", models.UUIDField(blank=True, null=True)), + ("state", models.UUIDField(blank=True, null=True)), + ("estimate_point", models.UUIDField(blank=True, null=True)), + ("name", models.CharField(max_length=255, verbose_name="Issue Name")), + ("description", models.JSONField(blank=True, default=dict)), + ("description_html", models.TextField(blank=True, default="

")), + ("description_stripped", models.TextField(blank=True, null=True)), + ("description_binary", models.BinaryField(null=True)), + ( + "priority", + models.CharField( + choices=[ + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ], + default="none", + max_length=30, + verbose_name="Issue Priority", + ), + ), + ("start_date", models.DateField(blank=True, null=True)), + ("target_date", models.DateField(blank=True, null=True)), + ( + "sequence_id", + models.IntegerField(default=1, verbose_name="Issue Sequence ID"), + ), + ("sort_order", models.FloatField(default=65535)), + ("completed_at", models.DateTimeField(null=True)), + ("archived_at", models.DateField(null=True)), + ("is_draft", models.BooleanField(default=False)), + ( + "external_source", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "external_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("type", models.UUIDField(blank=True, null=True)), + ( + "last_saved_at", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("owned_by", models.UUIDField()), + ( + "assignees", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "labels", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), + blank=True, + default=list, + size=None, + ), + ), + ("cycle", models.UUIDField(blank=True, null=True)), + ( + "modules", + django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), + blank=True, + default=list, + size=None, + ), + ), + ("properties", models.JSONField(default=dict)), + ("meta", models.JSONField(default=dict)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="versions", + to="db.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Issue Version", + "verbose_name_plural": "Issue Versions", + "db_table": "issue_versions", + "ordering": ("-created_at",), + }, + ), + migrations.AlterUniqueTogether( + name="teampage", + unique_together=None, + ), + migrations.RemoveField( + model_name="teampage", + name="created_by", + ), + migrations.RemoveField( + model_name="teampage", + name="page", + ), + migrations.RemoveField( + model_name="teampage", + name="team", + ), + migrations.RemoveField( + model_name="teampage", + name="updated_by", + ), + migrations.RemoveField( + model_name="teampage", + name="workspace", + ), + migrations.RemoveField( + model_name="page", + name="teams", + ), + migrations.RemoveField( + model_name="team", + name="members", + ), + migrations.AddField( + model_name="fileasset", + name="entity_identifier", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name="webhook", + name="is_internal", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="fileasset", + name="entity_type", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="webhook", + name="url", + field=models.URLField( + max_length=1024, + validators=[ + plane.db.models.webhook.validate_schema, + plane.db.models.webhook.validate_domain, + ], + ), + ), + migrations.DeleteModel( + name="TeamMember", + ), + migrations.DeleteModel( + name="TeamPage", + ), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index d00a2fa07..36810956c 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -61,8 +61,6 @@ from .user import Account, Profile, User from .view import IssueView from .webhook import Webhook, WebhookLog from .workspace import ( - Team, - TeamMember, Workspace, WorkspaceBaseModel, WorkspaceMember, diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index d7a380003..9f99a8144 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -44,25 +44,44 @@ class FileAsset(BaseModel): "db.User", on_delete=models.CASCADE, null=True, related_name="assets" ) workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, null=True, related_name="assets" + "db.Workspace", + on_delete=models.CASCADE, + null=True, + related_name="assets", ) draft_issue = models.ForeignKey( - "db.DraftIssue", on_delete=models.CASCADE, null=True, related_name="assets" + "db.DraftIssue", + on_delete=models.CASCADE, + null=True, + related_name="assets", ) project = models.ForeignKey( - "db.Project", on_delete=models.CASCADE, null=True, related_name="assets" + "db.Project", + on_delete=models.CASCADE, + null=True, + related_name="assets", ) issue = models.ForeignKey( "db.Issue", on_delete=models.CASCADE, null=True, related_name="assets" ) comment = models.ForeignKey( - "db.IssueComment", on_delete=models.CASCADE, null=True, related_name="assets" + "db.IssueComment", + on_delete=models.CASCADE, + null=True, + related_name="assets", ) page = models.ForeignKey( "db.Page", on_delete=models.CASCADE, null=True, related_name="assets" ) entity_type = models.CharField( - max_length=255, choices=EntityTypeContext.choices, null=True, blank=True + max_length=255, + null=True, + blank=True, + ) + entity_identifier = models.CharField( + max_length=255, + null=True, + blank=True, ) is_deleted = models.BooleanField(default=False) is_archived = models.BooleanField(default=False) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index e37c0e7c1..e50dbe7ce 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -9,11 +9,12 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.utils import timezone from django.db.models import Q +from django import apps # Module imports from plane.utils.html_processor import strip_tags from plane.db.mixins import SoftDeletionManager - +from plane.utils.exception_logger import log_exception from .project import ProjectBaseModel @@ -656,3 +657,126 @@ class IssueVote(ProjectBaseModel): def __str__(self): return f"{self.issue.name} {self.actor.email}" + + +class IssueVersion(ProjectBaseModel): + issue = models.ForeignKey( + "db.Issue", + on_delete=models.CASCADE, + related_name="versions", + ) + PRIORITY_CHOICES = ( + ("urgent", "Urgent"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("none", "None"), + ) + parent = models.UUIDField(blank=True, null=True) + state = models.UUIDField(blank=True, null=True) + estimate_point = models.UUIDField(blank=True, null=True) + name = models.CharField(max_length=255, verbose_name="Issue Name") + description = models.JSONField(blank=True, default=dict) + description_html = models.TextField(blank=True, default="

") + description_stripped = models.TextField(blank=True, null=True) + description_binary = models.BinaryField(null=True) + priority = models.CharField( + max_length=30, + choices=PRIORITY_CHOICES, + verbose_name="Issue Priority", + default="none", + ) + start_date = models.DateField(null=True, blank=True) + target_date = models.DateField(null=True, blank=True) + sequence_id = models.IntegerField( + default=1, verbose_name="Issue Sequence ID" + ) + sort_order = models.FloatField(default=65535) + completed_at = models.DateTimeField(null=True) + archived_at = models.DateField(null=True) + is_draft = models.BooleanField(default=False) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) + type = models.UUIDField(blank=True, null=True) + last_saved_at = models.DateTimeField(default=timezone.now) + owned_by = models.UUIDField() + assignees = ArrayField( + models.UUIDField(), + blank=True, + default=list, + ) + labels = ArrayField( + models.UUIDField(), + blank=True, + default=list, + ) + cycle = models.UUIDField( + null=True, + blank=True, + ) + modules = ArrayField( + models.UUIDField(), + blank=True, + default=list, + ) + properties = models.JSONField(default=dict) + meta = models.JSONField(default=dict) + + class Meta: + verbose_name = "Issue Version" + verbose_name_plural = "Issue Versions" + db_table = "issue_versions" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.name} <{self.project.name}>" + + @classmethod + def log_issue_version(cls, issue, user): + try: + """ + Log the issue version + """ + + Module = apps.get_model("db.Module") + CycleIssue = apps.get_model("db.CycleIssue") + + cycle_issue = CycleIssue.objects.filter( + issue=issue, + ).first() + + cls.objects.create( + issue=issue, + parent=issue.parent, + state=issue.state, + point=issue.point, + estimate_point=issue.estimate_point, + name=issue.name, + description=issue.description, + description_html=issue.description_html, + description_stripped=issue.description_stripped, + description_binary=issue.description_binary, + priority=issue.priority, + start_date=issue.start_date, + target_date=issue.target_date, + sequence_id=issue.sequence_id, + sort_order=issue.sort_order, + completed_at=issue.completed_at, + archived_at=issue.archived_at, + is_draft=issue.is_draft, + external_source=issue.external_source, + external_id=issue.external_id, + type=issue.type, + last_saved_at=issue.last_saved_at, + assignees=issue.assignees, + labels=issue.labels, + cycle=cycle_issue.cycle if cycle_issue else None, + modules=Module.objects.filter(issue=issue).values_list( + "id", flat=True + ), + owned_by=user, + ) + return True + except Exception as e: + log_exception(e) + return False diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py index 91fd6ac44..81e2b15a0 100644 --- a/apiserver/plane/db/models/page.py +++ b/apiserver/plane/db/models/page.py @@ -50,9 +50,6 @@ class Page(BaseModel): projects = models.ManyToManyField( "db.Project", related_name="pages", through="db.ProjectPage" ) - teams = models.ManyToManyField( - "db.Team", related_name="pages", through="db.TeamPage" - ) class Meta: verbose_name = "Page" @@ -160,32 +157,6 @@ class ProjectPage(BaseModel): return f"{self.project.name} {self.page.name}" -class TeamPage(BaseModel): - team = models.ForeignKey( - "db.Team", on_delete=models.CASCADE, related_name="team_pages" - ) - page = models.ForeignKey( - "db.Page", on_delete=models.CASCADE, related_name="team_pages" - ) - workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="team_pages" - ) - - class Meta: - unique_together = ["team", "page", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["team", "page"], - condition=models.Q(deleted_at__isnull=True), - name="team_page_unique_team_page_when_deleted_at_null", - ) - ] - verbose_name = "Team Page" - verbose_name_plural = "Team Pages" - db_table = "team_pages" - ordering = ("-created_at",) - - class PageVersion(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="page_versions" diff --git a/apiserver/plane/db/models/webhook.py b/apiserver/plane/db/models/webhook.py index be2a5e9a3..92d45a058 100644 --- a/apiserver/plane/db/models/webhook.py +++ b/apiserver/plane/db/models/webhook.py @@ -29,9 +29,13 @@ def validate_domain(value): class Webhook(BaseModel): workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="workspace_webhooks" + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_webhooks", + ) + url = models.URLField( + validators=[validate_schema, validate_domain], max_length=1024 ) - url = models.URLField(validators=[validate_schema, validate_domain]) is_active = models.BooleanField(default=True) secret_key = models.CharField(max_length=255, default=generate_token) project = models.BooleanField(default=False) @@ -39,6 +43,7 @@ class Webhook(BaseModel): module = models.BooleanField(default=False) cycle = models.BooleanField(default=False) issue_comment = models.BooleanField(default=False) + is_internal = models.BooleanField(default=False) def __str__(self): return f"{self.workspace.slug} {self.url}" diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 93f8a24e0..df1f26d3f 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -102,7 +102,12 @@ def get_default_display_properties(): def get_issue_props(): - return {"subscribed": True, "assigned": True, "created": True, "all_issues": True} + return { + "subscribed": True, + "assigned": True, + "created": True, + "all_issues": True, + } def slug_validator(value): @@ -131,7 +136,9 @@ class Workspace(BaseModel): max_length=48, db_index=True, unique=True, validators=[slug_validator] ) organization_size = models.CharField(max_length=20, blank=True, null=True) - timezone = models.CharField(max_length=255, default="UTC", choices=TIMEZONE_CHOICES) + timezone = models.CharField( + max_length=255, default="UTC", choices=TIMEZONE_CHOICES + ) def __str__(self): """Return name of the Workspace""" @@ -160,7 +167,10 @@ class WorkspaceBaseModel(BaseModel): "db.Workspace", models.CASCADE, related_name="workspace_%(class)s" ) project = models.ForeignKey( - "db.Project", models.CASCADE, related_name="project_%(class)s", null=True + "db.Project", + models.CASCADE, + related_name="project_%(class)s", + null=True, ) class Meta: @@ -174,7 +184,9 @@ class WorkspaceBaseModel(BaseModel): class WorkspaceMember(BaseModel): workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member" + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_member", ) member = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -209,7 +221,9 @@ class WorkspaceMember(BaseModel): class WorkspaceMemberInvite(BaseModel): workspace = models.ForeignKey( - "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member_invite" + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_member_invite", ) email = models.CharField(max_length=255) accepted = models.BooleanField(default=False) @@ -239,13 +253,6 @@ class WorkspaceMemberInvite(BaseModel): class Team(BaseModel): name = models.CharField(max_length=255, verbose_name="Team Name") description = models.TextField(verbose_name="Team Description", blank=True) - members = models.ManyToManyField( - settings.AUTH_USER_MODEL, - blank=True, - related_name="members", - through="TeamMember", - through_fields=("team", "member"), - ) workspace = models.ForeignKey( Workspace, on_delete=models.CASCADE, related_name="workspace_team" ) @@ -270,40 +277,15 @@ class Team(BaseModel): ordering = ("-created_at",) -class TeamMember(BaseModel): - workspace = models.ForeignKey( - Workspace, on_delete=models.CASCADE, related_name="team_member" - ) - team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="team_member") - member = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="team_member" - ) - - def __str__(self): - return self.team.name - - class Meta: - unique_together = ["team", "member", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["team", "member"], - condition=models.Q(deleted_at__isnull=True), - name="team_member_unique_team_member_when_deleted_at_null", - ) - ] - verbose_name = "Team Member" - verbose_name_plural = "Team Members" - db_table = "team_members" - ordering = ("-created_at",) - - class WorkspaceTheme(BaseModel): workspace = models.ForeignKey( "db.Workspace", on_delete=models.CASCADE, related_name="themes" ) name = models.CharField(max_length=300) actor = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="themes" + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="themes", ) colors = models.JSONField(default=dict) @@ -338,7 +320,9 @@ class WorkspaceUserProperties(BaseModel): ) 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) + display_properties = models.JSONField( + default=get_default_display_properties + ) class Meta: unique_together = ["workspace", "user", "deleted_at"]