diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 813c1af21..bdcdf6c0d 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -28,7 +28,6 @@ from .project import ( ProjectMemberSerializer, ProjectMemberInviteSerializer, ProjectIdentifierSerializer, - ProjectFavoriteSerializer, ProjectLiteSerializer, ProjectMemberLiteSerializer, ProjectDeployBoardSerializer, @@ -40,12 +39,10 @@ from .state import StateSerializer, StateLiteSerializer from .view import ( GlobalViewSerializer, IssueViewSerializer, - IssueViewFavoriteSerializer, ) from .cycle import ( CycleSerializer, CycleIssueSerializer, - CycleFavoriteSerializer, CycleWriteSerializer, CycleUserPropertiesSerializer, ) @@ -83,7 +80,6 @@ from .module import ( ModuleSerializer, ModuleIssueSerializer, ModuleLinkSerializer, - ModuleFavoriteSerializer, ModuleUserPropertiesSerializer, ) @@ -96,7 +92,6 @@ from .page import ( PageLogSerializer, SubPageSerializer, PageDetailSerializer, - PageFavoriteSerializer, ) from .estimate import ( diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index 13d321780..1a9ce52d1 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -7,7 +7,6 @@ from .issue import IssueStateSerializer from plane.db.models import ( Cycle, CycleIssue, - CycleFavorite, CycleUserProperties, ) @@ -93,20 +92,6 @@ class CycleIssueSerializer(BaseSerializer): "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 Meta: model = CycleUserProperties diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 687747242..6a0c4c94f 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -11,7 +11,6 @@ from plane.db.models import ( ModuleMember, ModuleIssue, ModuleLink, - ModuleFavorite, ModuleUserProperties, ) @@ -223,19 +222,6 @@ class ModuleDetailSerializer(ModuleSerializer): 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 Meta: model = ModuleUserProperties diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index 604ac2c2e..4f3cde39b 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -6,7 +6,6 @@ from .base import BaseSerializer from plane.db.models import ( Page, PageLog, - PageFavorite, PageLabel, Label, ) @@ -141,17 +140,4 @@ class PageLogSerializer(BaseSerializer): "workspace", "project", "page", - ] - - -class PageFavoriteSerializer(BaseSerializer): - page_detail = PageSerializer(source="page", read_only=True) - - class Meta: - model = PageFavorite - fields = "__all__" - read_only_fields = [ - "workspace", - "project", - "user", - ] + ] \ No newline at end of file diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index a0c2318e3..96d92f340 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -13,7 +13,6 @@ from plane.db.models import ( ProjectMember, ProjectMemberInvite, ProjectIdentifier, - ProjectFavorite, ProjectDeployBoard, ProjectPublicMember, ) @@ -197,16 +196,6 @@ class ProjectIdentifierSerializer(BaseSerializer): fields = "__all__" -class ProjectFavoriteSerializer(BaseSerializer): - class Meta: - model = ProjectFavorite - fields = "__all__" - read_only_fields = [ - "workspace", - "user", - ] - - class ProjectMemberLiteSerializer(BaseSerializer): member = UserLiteSerializer(read_only=True) is_subscribed = serializers.BooleanField(read_only=True) diff --git a/apiserver/plane/app/serializers/view.py b/apiserver/plane/app/serializers/view.py index f864f2b6c..c46a545d0 100644 --- a/apiserver/plane/app/serializers/view.py +++ b/apiserver/plane/app/serializers/view.py @@ -5,7 +5,7 @@ from rest_framework import serializers from .base import BaseSerializer, DynamicBaseSerializer from .workspace import WorkspaceLiteSerializer 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 @@ -72,16 +72,3 @@ class IssueViewSerializer(DynamicBaseSerializer): validated_data["query"] = {} validated_data["query"] = issue_filters(query_params, "PATCH") 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", - ] diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index e6d82795a..5e1241b08 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -24,7 +24,7 @@ from rest_framework.response import Response from plane.app.permissions import ProjectEntityPermission from plane.db.models import ( Cycle, - CycleFavorite, + UserFavorite, Issue, Label, User, @@ -42,9 +42,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): ] def get_queryset(self): - favorite_subquery = CycleFavorite.objects.filter( + favorite_subquery = UserFavorite.objects.filter( user=self.request.user, - cycle_id=OuterRef("pk"), + entity_type="cycle", + entity_identifier=OuterRef("pk"), project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index bc55d9abb..e0b28ac7b 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -30,7 +30,6 @@ from plane.app.permissions import ( ProjectLitePermission, ) from plane.app.serializers import ( - CycleFavoriteSerializer, CycleSerializer, CycleUserPropertiesSerializer, CycleWriteSerializer, @@ -38,8 +37,8 @@ from plane.app.serializers import ( from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( Cycle, - CycleFavorite, CycleIssue, + UserFavorite, CycleUserProperties, Issue, Label, @@ -67,9 +66,10 @@ class CycleViewSet(BaseViewSet): ) def get_queryset(self): - favorite_subquery = CycleFavorite.objects.filter( + favorite_subquery = UserFavorite.objects.filter( user=self.request.user, - cycle_id=OuterRef("pk"), + entity_identifier=OuterRef("pk"), + entity_type="cycle", project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) @@ -241,7 +241,7 @@ class CycleViewSet(BaseViewSet): "backlog_issues", "assignee_ids", "status", - "created_by" + "created_by", ) if data: @@ -754,8 +754,7 @@ class CycleDateCheckEndpoint(BaseAPIView): class CycleFavoriteViewSet(BaseViewSet): - serializer_class = CycleFavoriteSerializer - model = CycleFavorite + model = UserFavorite def get_queryset(self): return self.filter_queryset( @@ -767,18 +766,21 @@ class CycleFavoriteViewSet(BaseViewSet): ) def create(self, request, slug, project_id): - serializer = CycleFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + _ = UserFavorite.objects.create( + project_id=project_id, + user=request.user, + entity_type="cycle", + entity_identifier=request.data.get("cycle"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id, cycle_id): - cycle_favorite = CycleFavorite.objects.get( + cycle_favorite = UserFavorite.objects.get( project=project_id, + entity_type="cycle", user=request.user, workspace__slug=slug, - cycle_id=cycle_id, + entity_identifier=cycle_id, ) cycle_favorite.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py index 8a5345ff4..2cac5f366 100644 --- a/apiserver/plane/app/views/module/archive.py +++ b/apiserver/plane/app/views/module/archive.py @@ -25,12 +25,7 @@ from plane.app.permissions import ( from plane.app.serializers import ( ModuleDetailSerializer, ) -from plane.db.models import ( - Issue, - Module, - ModuleFavorite, - ModuleLink, -) +from plane.db.models import Issue, Module, ModuleLink, UserFavorite from plane.utils.analytics_plot import burndown_plot from plane.utils.user_timezone_converter import user_timezone_converter @@ -46,9 +41,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView): ] def get_queryset(self): - favorite_subquery = ModuleFavorite.objects.filter( + favorite_subquery = UserFavorite.objects.filter( user=self.request.user, - module_id=OuterRef("pk"), + entity_identifier=OuterRef("pk"), + entity_type="module", project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 4dc4c4921..f98e0fbc2 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -32,7 +32,6 @@ from plane.app.permissions import ( ) from plane.app.serializers import ( ModuleDetailSerializer, - ModuleFavoriteSerializer, ModuleLinkSerializer, ModuleSerializer, ModuleUserPropertiesSerializer, @@ -42,7 +41,7 @@ from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( Issue, Module, - ModuleFavorite, + UserFavorite, ModuleIssue, ModuleLink, ModuleUserProperties, @@ -69,9 +68,10 @@ class ModuleViewSet(BaseViewSet): ) def get_queryset(self): - favorite_subquery = ModuleFavorite.objects.filter( + favorite_subquery = UserFavorite.objects.filter( user=self.request.user, - module_id=OuterRef("pk"), + entity_type="module", + entity_identifier=OuterRef("pk"), project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) @@ -554,8 +554,7 @@ class ModuleLinkViewSet(BaseViewSet): class ModuleFavoriteViewSet(BaseViewSet): - serializer_class = ModuleFavoriteSerializer - model = ModuleFavorite + model = UserFavorite def get_queryset(self): return self.filter_queryset( @@ -567,18 +566,21 @@ class ModuleFavoriteViewSet(BaseViewSet): ) def create(self, request, slug, project_id): - serializer = ModuleFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + _ = UserFavorite.objects.create( + project_id=project_id, + user=request.user, + entity_type="module", + entity_identifier=request.data.get("module"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id, module_id): - module_favorite = ModuleFavorite.objects.get( - project=project_id, + module_favorite = UserFavorite.objects.get( + project_id=project_id, user=request.user, workspace__slug=slug, - module_id=module_id, + entity_type="module", + entity_identifier=module_id, ) module_favorite.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 29dc2dbf5..16ea78033 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -15,7 +15,6 @@ from rest_framework.response import Response from plane.app.permissions import ProjectEntityPermission from plane.app.serializers import ( - PageFavoriteSerializer, PageLogSerializer, PageSerializer, SubPageSerializer, @@ -23,8 +22,8 @@ from plane.app.serializers import ( ) from plane.db.models import ( Page, - PageFavorite, PageLog, + UserFavorite, ProjectMember, ) @@ -61,9 +60,10 @@ class PageViewSet(BaseViewSet): ] def get_queryset(self): - subquery = PageFavorite.objects.filter( + subquery = UserFavorite.objects.filter( user=self.request.user, - page_id=OuterRef("pk"), + entity_type="page", + entity_identifier=OuterRef("pk"), project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) @@ -303,23 +303,24 @@ class PageFavoriteViewSet(BaseViewSet): ProjectEntityPermission, ] - serializer_class = PageFavoriteSerializer - model = PageFavorite + model = UserFavorite def create(self, request, slug, project_id, pk): - _ = PageFavorite.objects.create( + _ = UserFavorite.objects.create( project_id=project_id, - page_id=pk, + entity_identifier=pk, + entity_type="page", user=request.user, ) return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id, pk): - page_favorite = PageFavorite.objects.get( + page_favorite = UserFavorite.objects.get( project=project_id, user=request.user, workspace__slug=slug, - page_id=pk, + entity_identifier=pk, + entity_type="page", ) page_favorite.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 1d23bd3aa..39db11871 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -28,7 +28,6 @@ from plane.app.views.base import BaseViewSet, BaseAPIView from plane.app.serializers import ( ProjectSerializer, ProjectListSerializer, - ProjectFavoriteSerializer, ProjectDeployBoardSerializer, ) @@ -42,7 +41,7 @@ from plane.db.models import ( ProjectMember, Workspace, State, - ProjectFavorite, + UserFavorite, ProjectIdentifier, Module, Cycle, @@ -90,10 +89,11 @@ class ProjectViewSet(BaseViewSet): ) .annotate( is_favorite=Exists( - ProjectFavorite.objects.filter( + UserFavorite.objects.filter( user=self.request.user, + entity_identifier=OuterRef("pk"), + entity_type="project", project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), ) ) ) @@ -560,8 +560,7 @@ class ProjectUserViewsEndpoint(BaseAPIView): class ProjectFavoritesViewSet(BaseViewSet): - serializer_class = ProjectFavoriteSerializer - model = ProjectFavorite + model = UserFavorite def get_queryset(self): return self.filter_queryset( @@ -579,15 +578,21 @@ class ProjectFavoritesViewSet(BaseViewSet): serializer.save(user=self.request.user) def create(self, request, slug): - serializer = ProjectFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + _ = UserFavorite.objects.create( + user=request.user, + entity_type="project", + entity_identifier=request.data.get("project"), + project_id=request.data.get("project"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id): - project_favorite = ProjectFavorite.objects.get( - project=project_id, user=request.user, workspace__slug=slug + project_favorite = UserFavorite.objects.get( + entity_identifier=project_id, + entity_type="project", + project=project_id, + user=request.user, + workspace__slug=slug, ) project_favorite.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index 7736e465c..72c27d20a 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -27,7 +27,6 @@ from .. import BaseViewSet from plane.app.serializers import ( IssueViewSerializer, IssueSerializer, - IssueViewFavoriteSerializer, ) from plane.app.permissions import ( WorkspaceEntityPermission, @@ -37,7 +36,7 @@ from plane.db.models import ( Workspace, IssueView, Issue, - IssueViewFavorite, + UserFavorite, IssueLink, IssueAttachment, ) @@ -273,9 +272,10 @@ class IssueViewViewSet(BaseViewSet): serializer.save(project_id=self.kwargs.get("project_id")) def get_queryset(self): - subquery = IssueViewFavorite.objects.filter( + subquery = UserFavorite.objects.filter( user=self.request.user, - view_id=OuterRef("pk"), + entity_identifier=OuterRef("pk"), + entity_type="view", project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) @@ -310,8 +310,7 @@ class IssueViewViewSet(BaseViewSet): class IssueViewFavoriteViewSet(BaseViewSet): - serializer_class = IssueViewFavoriteSerializer - model = IssueViewFavorite + model = UserFavorite def get_queryset(self): return self.filter_queryset( @@ -323,18 +322,21 @@ class IssueViewFavoriteViewSet(BaseViewSet): ) def create(self, request, slug, project_id): - serializer = IssueViewFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save(user=request.user, project_id=project_id) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + _ = UserFavorite.objects.create( + user=request.user, + entity_identifier=request.data.get("view"), + entity_type="view", + project_id=project_id, + ) + return Response(status=status.HTTP_204_NO_CONTENT) def destroy(self, request, slug, project_id, view_id): - view_favorite = IssueViewFavorite.objects.get( + view_favorite = UserFavorite.objects.get( project=project_id, user=request.user, workspace__slug=slug, - view_id=view_id, + entity_type="view", + entity_identifier=view_id, ) view_favorite.delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/authentication/views/common.py b/apiserver/plane/authentication/views/common.py index 3d17e93f5..67c068092 100644 --- a/apiserver/plane/authentication/views/common.py +++ b/apiserver/plane/authentication/views/common.py @@ -7,7 +7,6 @@ from zxcvbn import zxcvbn ## Module imports from plane.app.serializers import ( - ChangePasswordSerializer, UserSerializer, ) from plane.authentication.utils.login import user_login diff --git a/apiserver/plane/db/migrations/0065_auto_20240415_0937.py b/apiserver/plane/db/migrations/0065_auto_20240415_0937.py index e8e17a513..4698c7120 100644 --- a/apiserver/plane/db/migrations/0065_auto_20240415_0937.py +++ b/apiserver/plane/db/migrations/0065_auto_20240415_0937.py @@ -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): dependencies = [ @@ -262,9 +307,156 @@ class Migration(migrations.Migration): name="logo_props", field=models.JSONField(default=dict), ), + # Pages migrations.AddField( model_name="page", name="logo_props", 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), ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 2dc6d7909..b11ce7aa3 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -98,3 +98,5 @@ from .exporter import ExporterHistory from .webhook import Webhook, WebhookLog from .dashboard import Dashboard, DashboardWidget, Widget + +from .favorite import UserFavorite diff --git a/apiserver/plane/db/models/estimate.py b/apiserver/plane/db/models/estimate.py index 5a783f9b9..6ff1186c3 100644 --- a/apiserver/plane/db/models/estimate.py +++ b/apiserver/plane/db/models/estimate.py @@ -11,6 +11,7 @@ class Estimate(ProjectBaseModel): description = models.TextField( verbose_name="Estimate Description", blank=True ) + type = models.CharField(max_length=255, default="Categories") def __str__(self): """Return name of the estimate""" @@ -31,7 +32,7 @@ class EstimatePoint(ProjectBaseModel): related_name="points", ) key = models.IntegerField( - default=0, validators=[MinValueValidator(0), MaxValueValidator(7)] + default=0, validators=[MinValueValidator(0), MaxValueValidator(12)] ) description = models.TextField(blank=True) value = models.CharField(max_length=20) diff --git a/apiserver/plane/db/models/favorite.py b/apiserver/plane/db/models/favorite.py new file mode 100644 index 000000000..95157c793 --- /dev/null +++ b/apiserver/plane/db/models/favorite.py @@ -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}>" diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index e3d1e62a7..7a17853c3 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -120,7 +120,7 @@ class Issue(ProjectBaseModel): related_name="state_issue", ) estimate_point = models.IntegerField( - validators=[MinValueValidator(0), MaxValueValidator(7)], + validators=[MinValueValidator(0), MaxValueValidator(12)], null=True, blank=True, ) diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py index 6a06d8d44..3602bce1f 100644 --- a/apiserver/plane/db/models/page.py +++ b/apiserver/plane/db/models/page.py @@ -16,7 +16,7 @@ def get_view_props(): 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_html = models.TextField(blank=True, default="

") description_stripped = models.TextField(blank=True, null=True) @@ -43,6 +43,7 @@ class Page(ProjectBaseModel): is_locked = models.BooleanField(default=False) view_props = models.JSONField(default=get_view_props) logo_props = models.JSONField(default=dict) + description_binary = models.BinaryField(null=True) class Meta: verbose_name = "Page" diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 56e136126..fe39f2d09 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -325,7 +325,7 @@ class WorkspaceUserProperties(BaseModel): unique_together = ["workspace", "user"] verbose_name = "Workspace User Property" verbose_name_plural = "Workspace User Property" - db_table = "Workspace_user_properties" + db_table = "workspace_user_properties" ordering = ("-created_at",) def __str__(self):