forked from github/plane
dev: migrations (#4489)
* dev: estimates and pages migrations * dev: favorite and user migrations * chore: workspace base model * chore: workspace user properties * chore: removed unused variables * chore: favorite view set changes * chore: default sequence id --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
38f5ecbdf2
commit
85b54d2490
@ -28,7 +28,6 @@ from .project import (
|
|||||||
ProjectMemberSerializer,
|
ProjectMemberSerializer,
|
||||||
ProjectMemberInviteSerializer,
|
ProjectMemberInviteSerializer,
|
||||||
ProjectIdentifierSerializer,
|
ProjectIdentifierSerializer,
|
||||||
ProjectFavoriteSerializer,
|
|
||||||
ProjectLiteSerializer,
|
ProjectLiteSerializer,
|
||||||
ProjectMemberLiteSerializer,
|
ProjectMemberLiteSerializer,
|
||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
@ -40,12 +39,10 @@ from .state import StateSerializer, StateLiteSerializer
|
|||||||
from .view import (
|
from .view import (
|
||||||
GlobalViewSerializer,
|
GlobalViewSerializer,
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueViewFavoriteSerializer,
|
|
||||||
)
|
)
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
CycleUserPropertiesSerializer,
|
CycleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
@ -83,7 +80,6 @@ from .module import (
|
|||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
|
||||||
ModuleUserPropertiesSerializer,
|
ModuleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,7 +92,6 @@ from .page import (
|
|||||||
PageLogSerializer,
|
PageLogSerializer,
|
||||||
SubPageSerializer,
|
SubPageSerializer,
|
||||||
PageDetailSerializer,
|
PageDetailSerializer,
|
||||||
PageFavoriteSerializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
|
@ -7,7 +7,6 @@ from .issue import IssueStateSerializer
|
|||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
CycleFavorite,
|
|
||||||
CycleUserProperties,
|
CycleUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -93,20 +92,6 @@ class CycleIssueSerializer(BaseSerializer):
|
|||||||
"cycle",
|
"cycle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CycleFavoriteSerializer(BaseSerializer):
|
|
||||||
cycle_detail = CycleSerializer(source="cycle", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = CycleFavorite
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CycleUserPropertiesSerializer(BaseSerializer):
|
class CycleUserPropertiesSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CycleUserProperties
|
model = CycleUserProperties
|
||||||
|
@ -11,7 +11,6 @@ from plane.db.models import (
|
|||||||
ModuleMember,
|
ModuleMember,
|
||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleFavorite,
|
|
||||||
ModuleUserProperties,
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -223,19 +222,6 @@ class ModuleDetailSerializer(ModuleSerializer):
|
|||||||
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues"]
|
fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues"]
|
||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteSerializer(BaseSerializer):
|
|
||||||
module_detail = ModuleFlatSerializer(source="module", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ModuleFavorite
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleUserPropertiesSerializer(BaseSerializer):
|
class ModuleUserPropertiesSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleUserProperties
|
model = ModuleUserProperties
|
||||||
|
@ -6,7 +6,6 @@ from .base import BaseSerializer
|
|||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Page,
|
Page,
|
||||||
PageLog,
|
PageLog,
|
||||||
PageFavorite,
|
|
||||||
PageLabel,
|
PageLabel,
|
||||||
Label,
|
Label,
|
||||||
)
|
)
|
||||||
@ -142,16 +141,3 @@ class PageLogSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"page",
|
"page",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PageFavoriteSerializer(BaseSerializer):
|
|
||||||
page_detail = PageSerializer(source="page", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PageFavorite
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
@ -13,7 +13,6 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
ProjectMemberInvite,
|
ProjectMemberInvite,
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
ProjectFavorite,
|
|
||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
)
|
)
|
||||||
@ -197,16 +196,6 @@ class ProjectIdentifierSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class ProjectFavoriteSerializer(BaseSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = ProjectFavorite
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberLiteSerializer(BaseSerializer):
|
class ProjectMemberLiteSerializer(BaseSerializer):
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
is_subscribed = serializers.BooleanField(read_only=True)
|
is_subscribed = serializers.BooleanField(read_only=True)
|
||||||
|
@ -5,7 +5,7 @@ from rest_framework import serializers
|
|||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
from plane.db.models import GlobalView, IssueView
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
@ -72,16 +72,3 @@ class IssueViewSerializer(DynamicBaseSerializer):
|
|||||||
validated_data["query"] = {}
|
validated_data["query"] = {}
|
||||||
validated_data["query"] = issue_filters(query_params, "PATCH")
|
validated_data["query"] = issue_filters(query_params, "PATCH")
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewFavoriteSerializer(BaseSerializer):
|
|
||||||
view_detail = IssueViewSerializer(source="issue_view", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = IssueViewFavorite
|
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
|
||||||
"workspace",
|
|
||||||
"project",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
@ -24,7 +24,7 @@ from rest_framework.response import Response
|
|||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleFavorite,
|
UserFavorite,
|
||||||
Issue,
|
Issue,
|
||||||
Label,
|
Label,
|
||||||
User,
|
User,
|
||||||
@ -42,9 +42,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
favorite_subquery = CycleFavorite.objects.filter(
|
favorite_subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
cycle_id=OuterRef("pk"),
|
entity_type="cycle",
|
||||||
|
entity_identifier=OuterRef("pk"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
|
@ -30,7 +30,6 @@ from plane.app.permissions import (
|
|||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
CycleFavoriteSerializer,
|
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleUserPropertiesSerializer,
|
CycleUserPropertiesSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
@ -38,8 +37,8 @@ from plane.app.serializers import (
|
|||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleFavorite,
|
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
|
UserFavorite,
|
||||||
CycleUserProperties,
|
CycleUserProperties,
|
||||||
Issue,
|
Issue,
|
||||||
Label,
|
Label,
|
||||||
@ -67,9 +66,10 @@ class CycleViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
favorite_subquery = CycleFavorite.objects.filter(
|
favorite_subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
cycle_id=OuterRef("pk"),
|
entity_identifier=OuterRef("pk"),
|
||||||
|
entity_type="cycle",
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
@ -241,7 +241,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
"backlog_issues",
|
"backlog_issues",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"status",
|
"status",
|
||||||
"created_by"
|
"created_by",
|
||||||
)
|
)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
@ -754,8 +754,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class CycleFavoriteViewSet(BaseViewSet):
|
class CycleFavoriteViewSet(BaseViewSet):
|
||||||
serializer_class = CycleFavoriteSerializer
|
model = UserFavorite
|
||||||
model = CycleFavorite
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
@ -767,18 +766,21 @@ class CycleFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
serializer = CycleFavoriteSerializer(data=request.data)
|
_ = UserFavorite.objects.create(
|
||||||
if serializer.is_valid():
|
project_id=project_id,
|
||||||
serializer.save(user=request.user, project_id=project_id)
|
user=request.user,
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
entity_type="cycle",
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
entity_identifier=request.data.get("cycle"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, cycle_id):
|
def destroy(self, request, slug, project_id, cycle_id):
|
||||||
cycle_favorite = CycleFavorite.objects.get(
|
cycle_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
|
entity_type="cycle",
|
||||||
user=request.user,
|
user=request.user,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
cycle_id=cycle_id,
|
entity_identifier=cycle_id,
|
||||||
)
|
)
|
||||||
cycle_favorite.delete()
|
cycle_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -25,12 +25,7 @@ from plane.app.permissions import (
|
|||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleDetailSerializer,
|
ModuleDetailSerializer,
|
||||||
)
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import Issue, Module, ModuleLink, UserFavorite
|
||||||
Issue,
|
|
||||||
Module,
|
|
||||||
ModuleFavorite,
|
|
||||||
ModuleLink,
|
|
||||||
)
|
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
@ -46,9 +41,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
favorite_subquery = ModuleFavorite.objects.filter(
|
favorite_subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
module_id=OuterRef("pk"),
|
entity_identifier=OuterRef("pk"),
|
||||||
|
entity_type="module",
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
|
@ -32,7 +32,6 @@ from plane.app.permissions import (
|
|||||||
)
|
)
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleDetailSerializer,
|
ModuleDetailSerializer,
|
||||||
ModuleFavoriteSerializer,
|
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
ModuleUserPropertiesSerializer,
|
ModuleUserPropertiesSerializer,
|
||||||
@ -42,7 +41,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
|
|||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
Module,
|
Module,
|
||||||
ModuleFavorite,
|
UserFavorite,
|
||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleUserProperties,
|
ModuleUserProperties,
|
||||||
@ -69,9 +68,10 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
favorite_subquery = ModuleFavorite.objects.filter(
|
favorite_subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
module_id=OuterRef("pk"),
|
entity_type="module",
|
||||||
|
entity_identifier=OuterRef("pk"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
@ -554,8 +554,7 @@ class ModuleLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteViewSet(BaseViewSet):
|
class ModuleFavoriteViewSet(BaseViewSet):
|
||||||
serializer_class = ModuleFavoriteSerializer
|
model = UserFavorite
|
||||||
model = ModuleFavorite
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
@ -567,18 +566,21 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
serializer = ModuleFavoriteSerializer(data=request.data)
|
_ = UserFavorite.objects.create(
|
||||||
if serializer.is_valid():
|
project_id=project_id,
|
||||||
serializer.save(user=request.user, project_id=project_id)
|
user=request.user,
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
entity_type="module",
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
entity_identifier=request.data.get("module"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, module_id):
|
def destroy(self, request, slug, project_id, module_id):
|
||||||
module_favorite = ModuleFavorite.objects.get(
|
module_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
project_id=project_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
module_id=module_id,
|
entity_type="module",
|
||||||
|
entity_identifier=module_id,
|
||||||
)
|
)
|
||||||
module_favorite.delete()
|
module_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -15,7 +15,6 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
PageFavoriteSerializer,
|
|
||||||
PageLogSerializer,
|
PageLogSerializer,
|
||||||
PageSerializer,
|
PageSerializer,
|
||||||
SubPageSerializer,
|
SubPageSerializer,
|
||||||
@ -23,8 +22,8 @@ from plane.app.serializers import (
|
|||||||
)
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Page,
|
Page,
|
||||||
PageFavorite,
|
|
||||||
PageLog,
|
PageLog,
|
||||||
|
UserFavorite,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,9 +60,10 @@ class PageViewSet(BaseViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
subquery = PageFavorite.objects.filter(
|
subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
page_id=OuterRef("pk"),
|
entity_type="page",
|
||||||
|
entity_identifier=OuterRef("pk"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
@ -303,23 +303,24 @@ class PageFavoriteViewSet(BaseViewSet):
|
|||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer_class = PageFavoriteSerializer
|
model = UserFavorite
|
||||||
model = PageFavorite
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, pk):
|
def create(self, request, slug, project_id, pk):
|
||||||
_ = PageFavorite.objects.create(
|
_ = UserFavorite.objects.create(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
page_id=pk,
|
entity_identifier=pk,
|
||||||
|
entity_type="page",
|
||||||
user=request.user,
|
user=request.user,
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
page_favorite = PageFavorite.objects.get(
|
page_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
page_id=pk,
|
entity_identifier=pk,
|
||||||
|
entity_type="page",
|
||||||
)
|
)
|
||||||
page_favorite.delete()
|
page_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -28,7 +28,6 @@ from plane.app.views.base import BaseViewSet, BaseAPIView
|
|||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
ProjectListSerializer,
|
ProjectListSerializer,
|
||||||
ProjectFavoriteSerializer,
|
|
||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
Workspace,
|
Workspace,
|
||||||
State,
|
State,
|
||||||
ProjectFavorite,
|
UserFavorite,
|
||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
Module,
|
Module,
|
||||||
Cycle,
|
Cycle,
|
||||||
@ -90,10 +89,11 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_favorite=Exists(
|
is_favorite=Exists(
|
||||||
ProjectFavorite.objects.filter(
|
UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
entity_identifier=OuterRef("pk"),
|
||||||
|
entity_type="project",
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -560,8 +560,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectFavoritesViewSet(BaseViewSet):
|
class ProjectFavoritesViewSet(BaseViewSet):
|
||||||
serializer_class = ProjectFavoriteSerializer
|
model = UserFavorite
|
||||||
model = ProjectFavorite
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
@ -579,15 +578,21 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
|||||||
serializer.save(user=self.request.user)
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
serializer = ProjectFavoriteSerializer(data=request.data)
|
_ = UserFavorite.objects.create(
|
||||||
if serializer.is_valid():
|
user=request.user,
|
||||||
serializer.save(user=request.user)
|
entity_type="project",
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
entity_identifier=request.data.get("project"),
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
project_id=request.data.get("project"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id):
|
def destroy(self, request, slug, project_id):
|
||||||
project_favorite = ProjectFavorite.objects.get(
|
project_favorite = UserFavorite.objects.get(
|
||||||
project=project_id, user=request.user, workspace__slug=slug
|
entity_identifier=project_id,
|
||||||
|
entity_type="project",
|
||||||
|
project=project_id,
|
||||||
|
user=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
project_favorite.delete()
|
project_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -27,7 +27,6 @@ from .. import BaseViewSet
|
|||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
IssueViewFavoriteSerializer,
|
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
@ -37,7 +36,7 @@ from plane.db.models import (
|
|||||||
Workspace,
|
Workspace,
|
||||||
IssueView,
|
IssueView,
|
||||||
Issue,
|
Issue,
|
||||||
IssueViewFavorite,
|
UserFavorite,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
)
|
)
|
||||||
@ -273,9 +272,10 @@ class IssueViewViewSet(BaseViewSet):
|
|||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
subquery = IssueViewFavorite.objects.filter(
|
subquery = UserFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
view_id=OuterRef("pk"),
|
entity_identifier=OuterRef("pk"),
|
||||||
|
entity_type="view",
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
)
|
)
|
||||||
@ -310,8 +310,7 @@ class IssueViewViewSet(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class IssueViewFavoriteViewSet(BaseViewSet):
|
class IssueViewFavoriteViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewFavoriteSerializer
|
model = UserFavorite
|
||||||
model = IssueViewFavorite
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
@ -323,18 +322,21 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
serializer = IssueViewFavoriteSerializer(data=request.data)
|
_ = UserFavorite.objects.create(
|
||||||
if serializer.is_valid():
|
user=request.user,
|
||||||
serializer.save(user=request.user, project_id=project_id)
|
entity_identifier=request.data.get("view"),
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
entity_type="view",
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, view_id):
|
def destroy(self, request, slug, project_id, view_id):
|
||||||
view_favorite = IssueViewFavorite.objects.get(
|
view_favorite = UserFavorite.objects.get(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
view_id=view_id,
|
entity_type="view",
|
||||||
|
entity_identifier=view_id,
|
||||||
)
|
)
|
||||||
view_favorite.delete()
|
view_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -7,7 +7,6 @@ from zxcvbn import zxcvbn
|
|||||||
|
|
||||||
## Module imports
|
## Module imports
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ChangePasswordSerializer,
|
|
||||||
UserSerializer,
|
UserSerializer,
|
||||||
)
|
)
|
||||||
from plane.authentication.utils.login import user_login
|
from plane.authentication.utils.login import user_login
|
||||||
|
@ -45,6 +45,51 @@ def migrate_user_profile(apps, schema_editor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_favorite_migration(apps, schema_editor):
|
||||||
|
# Import the models
|
||||||
|
CycleFavorite = apps.get_model("db", "CycleFavorite")
|
||||||
|
ModuleFavorite = apps.get_model("db", "ModuleFavorite")
|
||||||
|
ProjectFavorite = apps.get_model("db", "ProjectFavorite")
|
||||||
|
PageFavorite = apps.get_model("db", "PageFavorite")
|
||||||
|
IssueViewFavorite = apps.get_model("db", "IssueViewFavorite")
|
||||||
|
UserFavorite = apps.get_model("db", "UserFavorite")
|
||||||
|
|
||||||
|
# List of source models
|
||||||
|
source_models = [
|
||||||
|
CycleFavorite,
|
||||||
|
ModuleFavorite,
|
||||||
|
ProjectFavorite,
|
||||||
|
PageFavorite,
|
||||||
|
IssueViewFavorite,
|
||||||
|
]
|
||||||
|
|
||||||
|
entity_mapper = {
|
||||||
|
"CycleFavorite": "cycle",
|
||||||
|
"ModuleFavorite": "module",
|
||||||
|
"ProjectFavorite": "project",
|
||||||
|
"PageFavorite": "page",
|
||||||
|
"IssueViewFavorite": "view",
|
||||||
|
}
|
||||||
|
|
||||||
|
for source_model in source_models:
|
||||||
|
entity_type = entity_mapper[source_model.__name__]
|
||||||
|
UserFavorite.objects.bulk_create(
|
||||||
|
[
|
||||||
|
UserFavorite(
|
||||||
|
user_id=obj.user_id,
|
||||||
|
entity_type=entity_type,
|
||||||
|
entity_identifier=str(getattr(obj, entity_type).id),
|
||||||
|
project_id=obj.project_id,
|
||||||
|
workspace_id=obj.workspace_id,
|
||||||
|
created_by_id=obj.created_by_id,
|
||||||
|
updated_by_id=obj.updated_by_id,
|
||||||
|
)
|
||||||
|
for obj in source_model.objects.all().iterator()
|
||||||
|
],
|
||||||
|
batch_size=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -262,9 +307,156 @@ class Migration(migrations.Migration):
|
|||||||
name="logo_props",
|
name="logo_props",
|
||||||
field=models.JSONField(default=dict),
|
field=models.JSONField(default=dict),
|
||||||
),
|
),
|
||||||
|
# Pages
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="page",
|
model_name="page",
|
||||||
name="logo_props",
|
name="logo_props",
|
||||||
field=models.JSONField(default=dict),
|
field=models.JSONField(default=dict),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="page",
|
||||||
|
name="description_binary",
|
||||||
|
field=models.BinaryField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="page",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(blank=True, max_length=255),
|
||||||
|
),
|
||||||
|
# Estimates
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="estimate",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(default="Categories", max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="estimatepoint",
|
||||||
|
name="key",
|
||||||
|
field=models.IntegerField(
|
||||||
|
default=0,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(0),
|
||||||
|
django.core.validators.MaxValueValidator(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="issue",
|
||||||
|
name="estimate_point",
|
||||||
|
field=models.IntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(0),
|
||||||
|
django.core.validators.MaxValueValidator(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# workspace user properties
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="workspaceuserproperties",
|
||||||
|
table="workspace_user_properties",
|
||||||
|
),
|
||||||
|
# Favorites
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserFavorite",
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
db_index=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("entity_type", models.CharField(max_length=100)),
|
||||||
|
("entity_identifier", models.UUIDField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
("is_folder", models.BooleanField(default=False)),
|
||||||
|
("sequence", models.IntegerField(default=65535)),
|
||||||
|
(
|
||||||
|
"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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"parent",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="parent_folder",
|
||||||
|
to="db.userfavorite",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"project",
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="favorites",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"workspace",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="workspace_%(class)s",
|
||||||
|
to="db.workspace",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "User Favorite",
|
||||||
|
"verbose_name_plural": "User Favorites",
|
||||||
|
"db_table": "user_favorites",
|
||||||
|
"ordering": ("-created_at",),
|
||||||
|
"unique_together": {
|
||||||
|
("entity_type", "user", "entity_identifier")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(user_favorite_migration),
|
||||||
]
|
]
|
||||||
|
@ -98,3 +98,5 @@ from .exporter import ExporterHistory
|
|||||||
from .webhook import Webhook, WebhookLog
|
from .webhook import Webhook, WebhookLog
|
||||||
|
|
||||||
from .dashboard import Dashboard, DashboardWidget, Widget
|
from .dashboard import Dashboard, DashboardWidget, Widget
|
||||||
|
|
||||||
|
from .favorite import UserFavorite
|
||||||
|
@ -11,6 +11,7 @@ class Estimate(ProjectBaseModel):
|
|||||||
description = models.TextField(
|
description = models.TextField(
|
||||||
verbose_name="Estimate Description", blank=True
|
verbose_name="Estimate Description", blank=True
|
||||||
)
|
)
|
||||||
|
type = models.CharField(max_length=255, default="Categories")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the estimate"""
|
"""Return name of the estimate"""
|
||||||
@ -31,7 +32,7 @@ class EstimatePoint(ProjectBaseModel):
|
|||||||
related_name="points",
|
related_name="points",
|
||||||
)
|
)
|
||||||
key = models.IntegerField(
|
key = models.IntegerField(
|
||||||
default=0, validators=[MinValueValidator(0), MaxValueValidator(7)]
|
default=0, validators=[MinValueValidator(0), MaxValueValidator(12)]
|
||||||
)
|
)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
value = models.CharField(max_length=20)
|
value = models.CharField(max_length=20)
|
||||||
|
52
apiserver/plane/db/models/favorite.py
Normal file
52
apiserver/plane/db/models/favorite.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .workspace import WorkspaceBaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserFavorite(WorkspaceBaseModel):
|
||||||
|
"""_summary_
|
||||||
|
UserFavorite (model): To store all the favorites of the user
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="favorites",
|
||||||
|
)
|
||||||
|
entity_type = models.CharField(max_length=100)
|
||||||
|
entity_identifier = models.UUIDField(null=True, blank=True)
|
||||||
|
name = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
is_folder = models.BooleanField(default=False)
|
||||||
|
sequence = models.IntegerField(default=65535)
|
||||||
|
parent = models.ForeignKey(
|
||||||
|
"self",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="parent_folder",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["entity_type", "user", "entity_identifier"]
|
||||||
|
verbose_name = "User Favorite"
|
||||||
|
verbose_name_plural = "User Favorites"
|
||||||
|
db_table = "user_favorites"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
largest_sort_order = UserFavorite.objects.filter(
|
||||||
|
workspace=self.workspace
|
||||||
|
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||||
|
if largest_sort_order is not None:
|
||||||
|
self.sort_order = largest_sort_order + 10000
|
||||||
|
|
||||||
|
super(UserFavorite, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return user and the entity type"""
|
||||||
|
return f"{self.user.email} <{self.entity_type}>"
|
@ -120,7 +120,7 @@ class Issue(ProjectBaseModel):
|
|||||||
related_name="state_issue",
|
related_name="state_issue",
|
||||||
)
|
)
|
||||||
estimate_point = models.IntegerField(
|
estimate_point = models.IntegerField(
|
||||||
validators=[MinValueValidator(0), MaxValueValidator(7)],
|
validators=[MinValueValidator(0), MaxValueValidator(12)],
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
@ -16,7 +16,7 @@ def get_view_props():
|
|||||||
|
|
||||||
|
|
||||||
class Page(ProjectBaseModel):
|
class Page(ProjectBaseModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255, blank=True)
|
||||||
description = models.JSONField(default=dict, blank=True)
|
description = models.JSONField(default=dict, blank=True)
|
||||||
description_html = models.TextField(blank=True, default="<p></p>")
|
description_html = models.TextField(blank=True, default="<p></p>")
|
||||||
description_stripped = models.TextField(blank=True, null=True)
|
description_stripped = models.TextField(blank=True, null=True)
|
||||||
@ -43,6 +43,7 @@ class Page(ProjectBaseModel):
|
|||||||
is_locked = models.BooleanField(default=False)
|
is_locked = models.BooleanField(default=False)
|
||||||
view_props = models.JSONField(default=get_view_props)
|
view_props = models.JSONField(default=get_view_props)
|
||||||
logo_props = models.JSONField(default=dict)
|
logo_props = models.JSONField(default=dict)
|
||||||
|
description_binary = models.BinaryField(null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Page"
|
verbose_name = "Page"
|
||||||
|
@ -325,7 +325,7 @@ class WorkspaceUserProperties(BaseModel):
|
|||||||
unique_together = ["workspace", "user"]
|
unique_together = ["workspace", "user"]
|
||||||
verbose_name = "Workspace User Property"
|
verbose_name = "Workspace User Property"
|
||||||
verbose_name_plural = "Workspace User Property"
|
verbose_name_plural = "Workspace User Property"
|
||||||
db_table = "Workspace_user_properties"
|
db_table = "workspace_user_properties"
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user