From 6c877ee3ba26100f78b90e622471c10374e0b276 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 22 Jul 2024 18:53:16 +0530 Subject: [PATCH 1/7] feat: added external id and external source for issue attachments --- ...72_issueattachment_external_id_and_more.py | 23 +++++++++++++++++++ apiserver/plane/db/models/issue.py | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py diff --git a/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py b/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py new file mode 100644 index 00000000000..01ef488a455 --- /dev/null +++ b/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.14 on 2024-07-22 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0071_rename_issueproperty_issueuserproperty_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='issueattachment', + name='external_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='issueattachment', + name='external_source', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 0c4373303f1..6d603b1ef69 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -386,6 +386,8 @@ class IssueAttachment(ProjectBaseModel): issue = models.ForeignKey( "db.Issue", on_delete=models.CASCADE, related_name="issue_attachment" ) + external_source = models.CharField(max_length=255, null=True, blank=True) + external_id = models.CharField(max_length=255, blank=True, null=True) class Meta: verbose_name = "Issue Attachment" From 4644470dde279394e86a51cae426996c450a30a4 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 22 Jul 2024 18:55:54 +0530 Subject: [PATCH 2/7] feat: added endpoint for creating users --- apiserver/plane/api/urls/__init__.py | 2 + apiserver/plane/api/urls/user.py | 13 +++ apiserver/plane/api/views/__init__.py | 3 + apiserver/plane/api/views/user.py | 147 ++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 apiserver/plane/api/urls/user.py create mode 100644 apiserver/plane/api/views/user.py diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index 84927439e2e..60023b73a5b 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -4,6 +4,7 @@ from .cycle import urlpatterns as cycle_patterns from .module import urlpatterns as module_patterns from .inbox import urlpatterns as inbox_patterns +from .user import urlpatterns as user_patterns urlpatterns = [ *project_patterns, @@ -12,4 +13,5 @@ *cycle_patterns, *module_patterns, *inbox_patterns, + *user_patterns, ] diff --git a/apiserver/plane/api/urls/user.py b/apiserver/plane/api/urls/user.py new file mode 100644 index 00000000000..32ed3549e55 --- /dev/null +++ b/apiserver/plane/api/urls/user.py @@ -0,0 +1,13 @@ +from django.urls import path + +from plane.api.views import ( + UserAPIEndpoint, +) + +urlpatterns = [ + path( + "workspaces//users/", + UserAPIEndpoint.as_view(), + name="users", + ), +] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index d59b40fc590..6348f8377d0 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -9,6 +9,7 @@ IssueLinkAPIEndpoint, IssueCommentAPIEndpoint, IssueActivityAPIEndpoint, + IssueAttachmentEndpoint, ) from .cycle import ( @@ -24,4 +25,6 @@ ModuleArchiveUnarchiveAPIEndpoint, ) +from .user import UserAPIEndpoint + from .inbox import InboxIssueAPIEndpoint diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/api/views/user.py new file mode 100644 index 00000000000..e6e0c7a49ca --- /dev/null +++ b/apiserver/plane/api/views/user.py @@ -0,0 +1,147 @@ +# Python imports +import uuid + +# Django imports +from django.contrib.auth.hashers import make_password +from django.core.validators import validate_email +from django.core.exceptions import ValidationError + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status + +# Module imports +from .base import BaseAPIView +from plane.api.serializers import UserLiteSerializer +from plane.db.models import ( + User, + Workspace, + Project, + WorkspaceMember, + ProjectMember, +) + + +# API endpoint to get and insert users inside the workspace +class UserAPIEndpoint(BaseAPIView): + # Get all the users that are present inside the workspace + def get(self, request, slug): + # Check if the workspace exists + if not Workspace.objects.filter(slug=slug).exists(): + return Response( + {"error": "Provided workspace does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the workspace members that are present inside the workspace + workspace_members = WorkspaceMember.objects.filter( + workspace__slug=slug + ) + + # Get all the users that are present inside the workspace + users = UserLiteSerializer( + User.objects.filter( + id__in=workspace_members.values_list("member_id", flat=True) + ), + many=True, + ).data + + return Response(users, status=status.HTTP_200_OK) + + # Insert a new user inside the workspace, and assign the user to the project + def post(self, request, slug): + # Check if user with email already exists, and send bad request if it's + # not present, check for workspace and valid project mandat + # ------------------- Validation ------------------- + if ( + request.data.get("email") is None + or request.data.get("display_name") is None + or request.data.get("project_id") is None + ): + return Response( + { + "error": "Expected email, display_name, workspace_slug, project_id, one or more of the fields are missing." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + email = request.data.get("email") + + try: + validate_email(email) + except ValidationError: + return Response( + {"error": "Invalid email provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.filter(slug=slug).first() + project = Project.objects.filter( + pk=request.data.get("project_id") + ).first() + + if not all([workspace, project]): + return Response( + {"error": "Provided workspace or project does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Check if user exists + user = User.objects.filter(email=email).first() + workspace_member = None + project_member = None + + if user: + # Check if user is part of the workspace + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace, member=user + ).first() + if workspace_member: + # Check if user is part of the project + project_member = ProjectMember.objects.filter( + project=project, member=user + ).first() + if project_member: + return Response( + { + "error": "User is already part of the workspace and project" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + # If user does not exist, create the user + if not user: + user = User.objects.create( + email=email, + display_name=request.data.get("display_name"), + first_name=request.data.get("first_name", ""), + last_name=request.data.get("last_name", ""), + username=uuid.uuid4().hex, + password=make_password(uuid.uuid4().hex), + is_password_autoset=True, + is_active=False, + ) + user.save() + + # Create a workspace member for the user if not already a member + if not workspace_member: + workspace_member = WorkspaceMember.objects.create( + workspace=workspace, + member=user, + role=request.data.get("role", 10), + ) + workspace_member.save() + + # Create a project member for the user if not already a member + if not project_member: + project_member = ProjectMember.objects.create( + project=project, + member=user, + role=request.data.get("role", 10), + ) + project_member.save() + + # Serialize the user and return the response + user_data = UserLiteSerializer(user).data + + return Response(user_data, status=status.HTTP_201_CREATED) From 98f23f167c65ad03aadf0afdf8a3dbe13f661268 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 22 Jul 2024 18:58:04 +0530 Subject: [PATCH 3/7] feat: added issue attachment endpoint --- apiserver/plane/api/urls/issue.py | 6 +++ apiserver/plane/api/views/issue.py | 84 +++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index 5ce9db85c70..e9bf030a2a2 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -7,6 +7,7 @@ IssueCommentAPIEndpoint, IssueActivityAPIEndpoint, WorkspaceIssueAPIEndpoint, + IssueAttachmentEndpoint, ) urlpatterns = [ @@ -65,4 +66,9 @@ IssueActivityAPIEndpoint.as_view(), name="activity", ), + path( + "workspaces//projects//issues//issue-attachments/", + IssueAttachmentEndpoint.as_view(), + name="attachment", + ), ] diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ce0501dd2ee..3172e7495ea 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -22,9 +22,11 @@ # Third party imports from rest_framework import status from rest_framework.response import Response +from rest_framework.parsers import MultiPartParser, FormParser # Module imports from plane.api.serializers import ( + IssueAttachmentSerializer, IssueActivitySerializer, IssueCommentSerializer, IssueLinkSerializer, @@ -36,7 +38,7 @@ ProjectLitePermission, ProjectMemberPermission, ) -from plane.bgtasks.issue_activites_task import issue_activity +from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( Issue, IssueActivity, @@ -874,3 +876,83 @@ def get(self, request, slug, project_id, issue_id, pk=None): expand=self.expand, ).data, ) + + +class IssueAttachmentEndpoint(BaseAPIView): + serializer_class = IssueAttachmentSerializer + permission_classes = [ + ProjectEntityPermission, + ] + model = IssueAttachment + parser_classes = (MultiPartParser, FormParser) + + def post(self, request, slug, project_id, issue_id): + serializer = IssueAttachmentSerializer(data=request.data) + if ( + request.data.get("external_id") + and request.data.get("external_source") + and IssueAttachment.objects.filter( + project_id=project_id, + workspace__slug=slug, + issue_id=issue_id, + external_source=request.data.get("external_source"), + external_id=request.data.get("external_id"), + ).exists() + ): + issue_attachment = IssueAttachment.objects.filter( + workspace__slug=slug, + project_id=project_id, + external_id=request.data.get("external_id"), + external_source=request.data.get("external_source"), + ).first() + return Response( + { + "error": "Issue attachment with the same external id and external source already exists", + "id": str(issue_attachment.id), + }, + status=status.HTTP_409_CONFLICT, + ) + + if serializer.is_valid(): + serializer.save(project_id=project_id, issue_id=issue_id) + issue_activity.delay( + type="attachment.activity.created", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + serializer.data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, project_id, issue_id, pk): + issue_attachment = IssueAttachment.objects.get(pk=pk) + issue_attachment.asset.delete(save=False) + issue_attachment.delete() + issue_activity.delay( + type="attachment.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + return Response(status=status.HTTP_204_NO_CONTENT) + + def get(self, request, slug, project_id, issue_id): + issue_attachments = IssueAttachment.objects.filter( + issue_id=issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) From 808ff5b78e0a03997d710736af709878cb50d022 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 22 Jul 2024 19:13:16 +0530 Subject: [PATCH 4/7] fix: converted user to workspace member --- apiserver/plane/api/urls/__init__.py | 4 ++-- apiserver/plane/api/urls/{user.py => member.py} | 6 +++--- apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/{user.py => member.py} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename apiserver/plane/api/urls/{user.py => member.py} (50%) rename apiserver/plane/api/views/{user.py => member.py} (99%) diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/api/urls/__init__.py index 60023b73a5b..efa84bce038 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/api/urls/__init__.py @@ -4,7 +4,7 @@ from .cycle import urlpatterns as cycle_patterns from .module import urlpatterns as module_patterns from .inbox import urlpatterns as inbox_patterns -from .user import urlpatterns as user_patterns +from .member import urlpatterns as member_patterns urlpatterns = [ *project_patterns, @@ -13,5 +13,5 @@ *cycle_patterns, *module_patterns, *inbox_patterns, - *user_patterns, + *member_patterns, ] diff --git a/apiserver/plane/api/urls/user.py b/apiserver/plane/api/urls/member.py similarity index 50% rename from apiserver/plane/api/urls/user.py rename to apiserver/plane/api/urls/member.py index 32ed3549e55..9a622d35a67 100644 --- a/apiserver/plane/api/urls/user.py +++ b/apiserver/plane/api/urls/member.py @@ -1,13 +1,13 @@ from django.urls import path from plane.api.views import ( - UserAPIEndpoint, + WorkspaceMemberAPIEndpoint, ) urlpatterns = [ path( - "workspaces//users/", - UserAPIEndpoint.as_view(), + "workspaces//members/", + WorkspaceMemberAPIEndpoint.as_view(), name="users", ), ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 6348f8377d0..48461cee273 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -25,6 +25,6 @@ ModuleArchiveUnarchiveAPIEndpoint, ) -from .user import UserAPIEndpoint +from .member import WorkspaceMemberAPIEndpoint from .inbox import InboxIssueAPIEndpoint diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/api/views/member.py similarity index 99% rename from apiserver/plane/api/views/user.py rename to apiserver/plane/api/views/member.py index e6e0c7a49ca..5d47bbb0684 100644 --- a/apiserver/plane/api/views/user.py +++ b/apiserver/plane/api/views/member.py @@ -23,7 +23,7 @@ # API endpoint to get and insert users inside the workspace -class UserAPIEndpoint(BaseAPIView): +class WorkspaceMemberAPIEndpoint(BaseAPIView): # Get all the users that are present inside the workspace def get(self, request, slug): # Check if the workspace exists From 6e951801d5ed6445b1c69086615f0e0bae97e73c Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 23 Jul 2024 17:03:59 +0530 Subject: [PATCH 5/7] chore: removed code blocking adding issues when the cycle has been completed --- apiserver/plane/api/views/cycle.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 8b9f90de5ff..106b6ee3ec4 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -393,7 +393,6 @@ def delete(self, request, slug, project_id, pk): class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView): - permission_classes = [ ProjectEntityPermission, ] @@ -647,17 +646,6 @@ def post(self, request, slug, project_id, cycle_id): workspace__slug=slug, project_id=project_id, pk=cycle_id ) - if ( - cycle.end_date is not None - and cycle.end_date < timezone.now().date() - ): - return Response( - { - "error": "The Cycle has already been completed so no new issues can be added" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - issues = Issue.objects.filter( pk__in=issues, workspace__slug=slug, project_id=project_id ).values_list("id", flat=True) From 62bfa781a65d5c25c3cff7e7e73ec197afef4531 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 23 Jul 2024 17:32:15 +0530 Subject: [PATCH 6/7] chore: update models --- apiserver/plane/api/views/issue.py | 2 +- apiserver/plane/db/models/issue.py | 6 ++---- apiserver/plane/db/models/project.py | 6 ++---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 3172e7495ea..365e1c470f7 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -38,7 +38,7 @@ ProjectLitePermission, ProjectMemberPermission, ) -from plane.bgtasks.issue_activities_task import issue_activity +from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( Issue, IssueActivity, diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 6d603b1ef69..b4c2db9ff67 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -7,8 +7,6 @@ from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction -from django.db.models.signals import post_save -from django.dispatch import receiver from django.utils import timezone # Module imports @@ -580,9 +578,9 @@ class IssueSequence(ProjectBaseModel): Issue, on_delete=models.SET_NULL, related_name="issue_sequence", - null=True, + null=True, # This is set to null because we want to keep the sequence even if the issue is deleted ) - sequence = models.PositiveBigIntegerField(default=1) + sequence = models.PositiveBigIntegerField(default=1, db_index=True) deleted = models.BooleanField(default=False) class Meta: diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 2957562db6c..c9a8a34bc2b 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -72,6 +72,7 @@ class Project(BaseModel): identifier = models.CharField( max_length=12, verbose_name="Project Identifier", + db_index=True, ) default_assignee = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -117,9 +118,6 @@ class Project(BaseModel): related_name="default_state", ) archived_at = models.DateTimeField(null=True) - # Project start and target date - start_date = models.DateTimeField(null=True, blank=True) - target_date = models.DateTimeField(null=True, blank=True) def __str__(self): """Return name of the project""" @@ -222,7 +220,7 @@ class ProjectIdentifier(AuditModel): project = models.OneToOneField( Project, on_delete=models.CASCADE, related_name="project_identifier" ) - name = models.CharField(max_length=12) + name = models.CharField(max_length=12, db_index=True) class Meta: unique_together = ["name", "workspace"] From f341573548b59a5eaba0151d63b84b7bb49f3f13 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 23 Jul 2024 18:21:43 +0530 Subject: [PATCH 7/7] chore: added user recent visited table --- ...72_issueattachment_external_id_and_more.py | 134 +++++++++++++++++- apiserver/plane/db/models/__init__.py | 2 + apiserver/plane/db/models/recent_visit.py | 38 +++++ 3 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 apiserver/plane/db/models/recent_visit.py diff --git a/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py b/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py index 01ef488a455..73d67aad08f 100644 --- a/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py +++ b/apiserver/plane/db/migrations/0072_issueattachment_external_id_and_more.py @@ -1,23 +1,145 @@ # Generated by Django 4.2.14 on 2024-07-22 13:22 - from django.db import migrations, models +from django.conf import settings +import django.db.models.deletion +import uuid class Migration(migrations.Migration): dependencies = [ - ('db', '0071_rename_issueproperty_issueuserproperty_and_more'), + ("db", "0071_rename_issueproperty_issueuserproperty_and_more"), ] operations = [ migrations.AddField( - model_name='issueattachment', - name='external_id', + model_name="issueattachment", + name="external_id", field=models.CharField(blank=True, max_length=255, null=True), ), migrations.AddField( - model_name='issueattachment', - name='external_source', + model_name="issueattachment", + name="external_source", field=models.CharField(blank=True, max_length=255, null=True), ), + migrations.CreateModel( + name="UserRecentVisit", + 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_identifier", models.UUIDField(null=True)), + ( + "entity_name", + models.CharField( + choices=[ + ("VIEW", "View"), + ("PAGE", "Page"), + ("ISSUE", "Issue"), + ("CYCLE", "Cycle"), + ("MODULE", "Module"), + ("PROJECT", "Project"), + ], + max_length=30, + ), + ), + ("visited_at", models.DateTimeField(auto_now=True)), + ( + "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", + ), + ), + ( + "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="user_recent_visit", + 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 Recent Visit", + "verbose_name_plural": "User Recent Visits", + "db_table": "user_recent_visits", + "ordering": ("-created_at",), + }, + ), + migrations.RemoveField( + model_name="project", + name="start_date", + ), + migrations.RemoveField( + model_name="project", + name="target_date", + ), + migrations.AlterField( + model_name="issuesequence", + name="sequence", + field=models.PositiveBigIntegerField(db_index=True, default=1), + ), + migrations.AlterField( + model_name="project", + name="identifier", + field=models.CharField( + db_index=True, max_length=12, verbose_name="Project Identifier" + ), + ), + migrations.AlterField( + model_name="projectidentifier", + name="name", + field=models.CharField(db_index=True, max_length=12), + ), ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index cee0e18a2c1..4874902a420 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -110,3 +110,5 @@ from .favorite import UserFavorite from .issue_type import IssueType + +from .recent_visit import UserRecentVisit \ No newline at end of file diff --git a/apiserver/plane/db/models/recent_visit.py b/apiserver/plane/db/models/recent_visit.py new file mode 100644 index 00000000000..4696ead46bb --- /dev/null +++ b/apiserver/plane/db/models/recent_visit.py @@ -0,0 +1,38 @@ +# Django imports +from django.db import models +from django.conf import settings + +# Module imports +from .workspace import WorkspaceBaseModel + + +class EntityNameEnum(models.TextChoices): + VIEW = "VIEW", "View" + PAGE = "PAGE", "Page" + ISSUE = "ISSUE", "Issue" + CYCLE = "CYCLE", "Cycle" + MODULE = "MODULE", "Module" + PROJECT = "PROJECT", "Project" + + +class UserRecentVisit(WorkspaceBaseModel): + entity_identifier = models.UUIDField(null=True) + entity_name = models.CharField( + max_length=30, + choices=EntityNameEnum.choices, + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="user_recent_visit", + ) + visited_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "User Recent Visit" + verbose_name_plural = "User Recent Visits" + db_table = "user_recent_visits" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.entity_name} {self.user.email}"