feat: added external api endpoints for creating users and adding attachments to issues (#5193)
* feat: added external id and external source for issue attachments * feat: added endpoint for creating users * feat: added issue attachment endpoint * fix: converted user to workspace member * chore: removed code blocking adding issues when the cycle has been completed * chore: update models * chore: added user recent visited table --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
66c2cbe7d6
commit
3a6d3d4e82
12 changed files with 444 additions and 20 deletions
|
|
@ -4,6 +4,7 @@ from .issue import urlpatterns as issue_patterns
|
||||||
from .cycle import urlpatterns as cycle_patterns
|
from .cycle import urlpatterns as cycle_patterns
|
||||||
from .module import urlpatterns as module_patterns
|
from .module import urlpatterns as module_patterns
|
||||||
from .inbox import urlpatterns as inbox_patterns
|
from .inbox import urlpatterns as inbox_patterns
|
||||||
|
from .member import urlpatterns as member_patterns
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
*project_patterns,
|
*project_patterns,
|
||||||
|
|
@ -12,4 +13,5 @@ urlpatterns = [
|
||||||
*cycle_patterns,
|
*cycle_patterns,
|
||||||
*module_patterns,
|
*module_patterns,
|
||||||
*inbox_patterns,
|
*inbox_patterns,
|
||||||
|
*member_patterns,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from plane.api.views import (
|
||||||
IssueCommentAPIEndpoint,
|
IssueCommentAPIEndpoint,
|
||||||
IssueActivityAPIEndpoint,
|
IssueActivityAPIEndpoint,
|
||||||
WorkspaceIssueAPIEndpoint,
|
WorkspaceIssueAPIEndpoint,
|
||||||
|
IssueAttachmentEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
@ -65,4 +66,9 @@ urlpatterns = [
|
||||||
IssueActivityAPIEndpoint.as_view(),
|
IssueActivityAPIEndpoint.as_view(),
|
||||||
name="activity",
|
name="activity",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
|
||||||
|
IssueAttachmentEndpoint.as_view(),
|
||||||
|
name="attachment",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
13
apiserver/plane/api/urls/member.py
Normal file
13
apiserver/plane/api/urls/member.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from plane.api.views import (
|
||||||
|
WorkspaceMemberAPIEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/members/",
|
||||||
|
WorkspaceMemberAPIEndpoint.as_view(),
|
||||||
|
name="users",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -9,6 +9,7 @@ from .issue import (
|
||||||
IssueLinkAPIEndpoint,
|
IssueLinkAPIEndpoint,
|
||||||
IssueCommentAPIEndpoint,
|
IssueCommentAPIEndpoint,
|
||||||
IssueActivityAPIEndpoint,
|
IssueActivityAPIEndpoint,
|
||||||
|
IssueAttachmentEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
|
|
@ -24,4 +25,6 @@ from .module import (
|
||||||
ModuleArchiveUnarchiveAPIEndpoint,
|
ModuleArchiveUnarchiveAPIEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .member import WorkspaceMemberAPIEndpoint
|
||||||
|
|
||||||
from .inbox import InboxIssueAPIEndpoint
|
from .inbox import InboxIssueAPIEndpoint
|
||||||
|
|
|
||||||
|
|
@ -393,7 +393,6 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
|
||||||
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
@ -647,17 +646,6 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||||
workspace__slug=slug, project_id=project_id, pk=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(
|
issues = Issue.objects.filter(
|
||||||
pk__in=issues, workspace__slug=slug, project_id=project_id
|
pk__in=issues, workspace__slug=slug, project_id=project_id
|
||||||
).values_list("id", flat=True)
|
).values_list("id", flat=True)
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@ from django.utils import timezone
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
|
IssueAttachmentSerializer,
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssueLinkSerializer,
|
IssueLinkSerializer,
|
||||||
|
|
@ -874,3 +876,83 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
||||||
expand=self.expand,
|
expand=self.expand,
|
||||||
).data,
|
).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)
|
||||||
|
|
|
||||||
147
apiserver/plane/api/views/member.py
Normal file
147
apiserver/plane/api/views/member.py
Normal file
|
|
@ -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 WorkspaceMemberAPIEndpoint(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)
|
||||||
|
|
@ -0,0 +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"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -110,3 +110,5 @@ from .dashboard import Dashboard, DashboardWidget, Widget
|
||||||
from .favorite import UserFavorite
|
from .favorite import UserFavorite
|
||||||
|
|
||||||
from .issue_type import IssueType
|
from .issue_type import IssueType
|
||||||
|
|
||||||
|
from .recent_visit import UserRecentVisit
|
||||||
|
|
@ -7,8 +7,6 @@ from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
|
|
@ -386,6 +384,8 @@ class IssueAttachment(ProjectBaseModel):
|
||||||
issue = models.ForeignKey(
|
issue = models.ForeignKey(
|
||||||
"db.Issue", on_delete=models.CASCADE, related_name="issue_attachment"
|
"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:
|
class Meta:
|
||||||
verbose_name = "Issue Attachment"
|
verbose_name = "Issue Attachment"
|
||||||
|
|
@ -578,9 +578,9 @@ class IssueSequence(ProjectBaseModel):
|
||||||
Issue,
|
Issue,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="issue_sequence",
|
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)
|
deleted = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ class Project(BaseModel):
|
||||||
identifier = models.CharField(
|
identifier = models.CharField(
|
||||||
max_length=12,
|
max_length=12,
|
||||||
verbose_name="Project Identifier",
|
verbose_name="Project Identifier",
|
||||||
|
db_index=True,
|
||||||
)
|
)
|
||||||
default_assignee = models.ForeignKey(
|
default_assignee = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
|
|
@ -117,9 +118,6 @@ class Project(BaseModel):
|
||||||
related_name="default_state",
|
related_name="default_state",
|
||||||
)
|
)
|
||||||
archived_at = models.DateTimeField(null=True)
|
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):
|
def __str__(self):
|
||||||
"""Return name of the project"""
|
"""Return name of the project"""
|
||||||
|
|
@ -222,7 +220,7 @@ class ProjectIdentifier(AuditModel):
|
||||||
project = models.OneToOneField(
|
project = models.OneToOneField(
|
||||||
Project, on_delete=models.CASCADE, related_name="project_identifier"
|
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:
|
class Meta:
|
||||||
unique_together = ["name", "workspace"]
|
unique_together = ["name", "workspace"]
|
||||||
|
|
|
||||||
38
apiserver/plane/db/models/recent_visit.py
Normal file
38
apiserver/plane/db/models/recent_visit.py
Normal file
|
|
@ -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}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue