Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6c8d513ed | ||
|
|
dd95dd9f5e | ||
|
|
e43139726f | ||
|
|
020fe91267 | ||
|
|
6e1cd4194a | ||
|
|
615ccf9459 | ||
|
|
13362590b6 | ||
|
|
7833ca7bea | ||
|
|
a1d27a1bf0 | ||
|
|
8fbd4a059b | ||
|
|
e751686683 | ||
|
|
8ee5ba96ce | ||
|
|
9e8885df5f | ||
|
|
bc48010377 | ||
|
|
9fde539b1d | ||
|
|
ec26bf6e68 | ||
|
|
e9ef3fb32a | ||
|
|
ee2c7c5fa1 | ||
|
|
d64ae9a2e4 | ||
|
|
f58a00a4ab | ||
|
|
a3e5284f71 | ||
|
|
1c06c3f43e | ||
|
|
da1496fe65 | ||
|
|
3d489e186f | ||
|
|
57d5ff7646 | ||
|
|
3c9926d383 | ||
|
|
ece4d5b1ed | ||
|
|
73eed69aa6 | ||
|
|
09603cf189 | ||
|
|
23e53df3ad | ||
|
|
57594aac4e | ||
|
|
8b884ab681 | ||
|
|
08e5f2b156 | ||
|
|
cb3a73e515 | ||
|
|
cb2a7d0930 | ||
|
|
c38e048ce8 | ||
|
|
94b72effbf | ||
|
|
eccb1f5d10 | ||
|
|
a71491ecb9 | ||
|
|
455c2cc787 | ||
|
|
81f6557908 | ||
|
|
2f10f35191 | ||
|
|
cf64c7bbc6 | ||
|
|
9dd8c8ba14 | ||
|
|
d98b688342 | ||
|
|
ce21630388 | ||
|
|
0927fa150c | ||
|
|
eec411baaf | ||
|
|
ecc8fbd79b | ||
|
|
c9b628e578 | ||
|
|
b522de99ba | ||
|
|
b58d7a715a | ||
|
|
87cd44bcd2 | ||
|
+7 |
804b7d8663 | ||
|
|
1539340113 | ||
|
|
d9ee692ce9 |
@@ -3,14 +3,14 @@ name: Create Sync Action
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- preview
|
- preview
|
||||||
types:
|
types:
|
||||||
- closed
|
- closed
|
||||||
env:
|
env:
|
||||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_pr:
|
sync_changes:
|
||||||
# Only run the job when a PR is merged
|
# Only run the job when a PR is merged
|
||||||
if: github.event.pull_request.merged == true
|
if: github.event.pull_request.merged == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -43,4 +43,4 @@ jobs:
|
|||||||
|
|
||||||
git checkout $SOURCE_BRANCH
|
git checkout $SOURCE_BRANCH
|
||||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ SENTRY_DSN=""
|
|||||||
SENTRY_ENVIRONMENT="development"
|
SENTRY_ENVIRONMENT="development"
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
POSTGRES_USER="plane"
|
||||||
PGPASSWORD="plane"
|
POSTGRES_PASSWORD="plane"
|
||||||
PGHOST="plane-db"
|
POSTGRES_HOST="plane-db"
|
||||||
PGDATABASE="plane"
|
POSTGRES_DB="plane"
|
||||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}
|
||||||
|
|
||||||
# Oauth variables
|
# Oauth variables
|
||||||
GOOGLE_CLIENT_ID=""
|
GOOGLE_CLIENT_ID=""
|
||||||
@@ -39,7 +39,6 @@ OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
|||||||
OPENAI_API_KEY="sk-" # deprecated
|
OPENAI_API_KEY="sk-" # deprecated
|
||||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||||
|
|
||||||
|
|
||||||
# Settings related to Docker
|
# Settings related to Docker
|
||||||
DOCKERIZED=1 # deprecated
|
DOCKERIZED=1 # deprecated
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ def update_description():
|
|||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
|
|
||||||
Issue.objects.bulk_update(
|
Issue.objects.bulk_update(
|
||||||
updated_issues, ["description_html", "description_stripped"], batch_size=100
|
updated_issues,
|
||||||
|
["description_html", "description_stripped"],
|
||||||
|
batch_size=100,
|
||||||
)
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -40,7 +42,9 @@ def update_comments():
|
|||||||
updated_issue_comments = []
|
updated_issue_comments = []
|
||||||
|
|
||||||
for issue_comment in issue_comments:
|
for issue_comment in issue_comments:
|
||||||
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
|
issue_comment.comment_html = (
|
||||||
|
f"<p>{issue_comment.comment_stripped}</p>"
|
||||||
|
)
|
||||||
updated_issue_comments.append(issue_comment)
|
updated_issue_comments.append(issue_comment)
|
||||||
|
|
||||||
IssueComment.objects.bulk_update(
|
IssueComment.objects.bulk_update(
|
||||||
@@ -99,7 +103,9 @@ def updated_issue_sort_order():
|
|||||||
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
|
|
||||||
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
|
Issue.objects.bulk_update(
|
||||||
|
updated_issues, ["sort_order"], batch_size=100
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -137,7 +143,9 @@ def update_project_cover_images():
|
|||||||
project.cover_image = project_cover_images[random.randint(0, 19)]
|
project.cover_image = project_cover_images[random.randint(0, 19)]
|
||||||
updated_projects.append(project)
|
updated_projects.append(project)
|
||||||
|
|
||||||
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
|
Project.objects.bulk_update(
|
||||||
|
updated_projects, ["cover_image"], batch_size=100
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -186,7 +194,9 @@ def update_label_color():
|
|||||||
|
|
||||||
def create_slack_integration():
|
def create_slack_integration():
|
||||||
try:
|
try:
|
||||||
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
|
_ = Integration.objects.create(
|
||||||
|
provider="slack", network=2, title="Slack"
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -212,12 +222,16 @@ def update_integration_verified():
|
|||||||
|
|
||||||
def update_start_date():
|
def update_start_date():
|
||||||
try:
|
try:
|
||||||
issues = Issue.objects.filter(state__group__in=["started", "completed"])
|
issues = Issue.objects.filter(
|
||||||
|
state__group__in=["started", "completed"]
|
||||||
|
)
|
||||||
updated_issues = []
|
updated_issues = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
issue.start_date = issue.created_at.date()
|
issue.start_date = issue.created_at.date()
|
||||||
updated_issues.append(issue)
|
updated_issues.append(issue)
|
||||||
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
|
Issue.objects.bulk_update(
|
||||||
|
updated_issues, ["start_date"], batch_size=500
|
||||||
|
)
|
||||||
print("Success")
|
print("Success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
Regular → Executable
+1
@@ -2,4 +2,5 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
|
python manage.py wait_for_migrations
|
||||||
celery -A plane beat -l info
|
celery -A plane beat -l info
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
python manage.py migrate
|
python manage.py safe_migrate
|
||||||
|
|
||||||
# Create the default bucket
|
# Create the default bucket
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
python manage.py migrate
|
python manage.py safe_migrate
|
||||||
|
|
||||||
# Create the default bucket
|
# Create the default bucket
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
|
python manage.py wait_for_migrations
|
||||||
celery -A plane worker -l info
|
celery -A plane worker -l info
|
||||||
+3
-3
@@ -2,10 +2,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
'DJANGO_SETTINGS_MODULE',
|
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
|
||||||
'plane.settings.production')
|
)
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
|
|
||||||
__all__ = ('celery_app',)
|
__all__ = ("celery_app",)
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class AnalyticsConfig(AppConfig):
|
class AnalyticsConfig(AppConfig):
|
||||||
name = 'plane.analytics'
|
name = "plane.analytics"
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class ApiConfig(AppConfig):
|
class ApiConfig(AppConfig):
|
||||||
name = "plane.api"
|
name = "plane.api"
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
def validate_api_token(self, token):
|
def validate_api_token(self, token):
|
||||||
try:
|
try:
|
||||||
api_token = APIToken.objects.get(
|
api_token = APIToken.objects.get(
|
||||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
Q(
|
||||||
|
Q(expired_at__gt=timezone.now())
|
||||||
|
| Q(expired_at__isnull=True)
|
||||||
|
),
|
||||||
token=token,
|
token=token,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
@@ -44,4 +47,4 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
|
|
||||||
# Validate the API token
|
# Validate the API token
|
||||||
user, token = self.validate_api_token(token)
|
user, token = self.validate_api_token(token)
|
||||||
return user, token
|
return user, token
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
from rest_framework.throttling import SimpleRateThrottle
|
from rest_framework.throttling import SimpleRateThrottle
|
||||||
|
|
||||||
|
|
||||||
class ApiKeyRateThrottle(SimpleRateThrottle):
|
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||||
scope = 'api_key'
|
scope = "api_key"
|
||||||
rate = '60/minute'
|
rate = "60/minute"
|
||||||
|
|
||||||
def get_cache_key(self, request, view):
|
def get_cache_key(self, request, view):
|
||||||
# Retrieve the API key from the request header
|
# Retrieve the API key from the request header
|
||||||
api_key = request.headers.get('X-Api-Key')
|
api_key = request.headers.get("X-Api-Key")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return None # Allow the request if there's no API key
|
return None # Allow the request if there's no API key
|
||||||
|
|
||||||
# Use the API key as part of the cache key
|
# Use the API key as part of the cache key
|
||||||
return f'{self.scope}:{api_key}'
|
return f"{self.scope}:{api_key}"
|
||||||
|
|
||||||
def allow_request(self, request, view):
|
def allow_request(self, request, view):
|
||||||
allowed = super().allow_request(request, view)
|
allowed = super().allow_request(request, view)
|
||||||
@@ -24,7 +25,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
|||||||
# Remove old histories
|
# Remove old histories
|
||||||
while history and history[-1] <= now - self.duration:
|
while history and history[-1] <= now - self.duration:
|
||||||
history.pop()
|
history.pop()
|
||||||
|
|
||||||
# Calculate the requests
|
# Calculate the requests
|
||||||
num_requests = len(history)
|
num_requests = len(history)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
|||||||
reset_time = int(now + self.duration)
|
reset_time = int(now + self.duration)
|
||||||
|
|
||||||
# Add headers
|
# Add headers
|
||||||
request.META['X-RateLimit-Remaining'] = max(0, available)
|
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||||
request.META['X-RateLimit-Reset'] = reset_time
|
request.META["X-RateLimit-Reset"] = reset_time
|
||||||
|
|
||||||
return allowed
|
return allowed
|
||||||
|
|||||||
@@ -13,5 +13,9 @@ from .issue import (
|
|||||||
)
|
)
|
||||||
from .state import StateLiteSerializer, StateSerializer
|
from .state import StateLiteSerializer, StateSerializer
|
||||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
||||||
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
|
from .module import (
|
||||||
from .inbox import InboxIssueSerializer
|
ModuleSerializer,
|
||||||
|
ModuleIssueSerializer,
|
||||||
|
ModuleLiteSerializer,
|
||||||
|
)
|
||||||
|
from .inbox import InboxIssueSerializer
|
||||||
|
|||||||
@@ -97,9 +97,11 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
exp_serializer = expansion[expand](
|
exp_serializer = expansion[expand](
|
||||||
getattr(instance, expand)
|
getattr(instance, expand)
|
||||||
)
|
)
|
||||||
response[expand] = exp_serializer.data
|
response[expand] = exp_serializer.data
|
||||||
else:
|
else:
|
||||||
# You might need to handle this case differently
|
# You might need to handle this case differently
|
||||||
response[expand] = getattr(instance, f"{expand}_id", None)
|
response[expand] = getattr(
|
||||||
|
instance, f"{expand}_id", None
|
||||||
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ class CycleSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed end date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -55,7 +57,6 @@ class CycleIssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class CycleLiteSerializer(BaseSerializer):
|
class CycleLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from plane.db.models import InboxIssue
|
from plane.db.models import InboxIssue
|
||||||
|
|
||||||
class InboxIssueSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
|
class InboxIssueSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InboxIssue
|
model = InboxIssue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -16,4 +16,4 @@ class InboxIssueSerializer(BaseSerializer):
|
|||||||
"updated_by",
|
"updated_by",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from .module import ModuleSerializer, ModuleLiteSerializer
|
|||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from .state import StateLiteSerializer
|
from .state import StateLiteSerializer
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(BaseSerializer):
|
||||||
assignees = serializers.ListField(
|
assignees = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(
|
child=serializers.PrimaryKeyRelatedField(
|
||||||
@@ -66,14 +67,16 @@ class IssueSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if(data.get("description_html", None) is not None):
|
if data.get("description_html", None) is not None:
|
||||||
parsed = html.fromstring(data["description_html"])
|
parsed = html.fromstring(data["description_html"])
|
||||||
parsed_str = html.tostring(parsed, encoding='unicode')
|
parsed_str = html.tostring(parsed, encoding="unicode")
|
||||||
data["description_html"] = parsed_str
|
data["description_html"] = parsed_str
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
||||||
|
|
||||||
@@ -96,7 +99,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
if (
|
if (
|
||||||
data.get("state")
|
data.get("state")
|
||||||
and not State.objects.filter(
|
and not State.objects.filter(
|
||||||
project_id=self.context.get("project_id"), pk=data.get("state").id
|
project_id=self.context.get("project_id"),
|
||||||
|
pk=data.get("state").id,
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@@ -107,7 +111,8 @@ class IssueSerializer(BaseSerializer):
|
|||||||
if (
|
if (
|
||||||
data.get("parent")
|
data.get("parent")
|
||||||
and not Issue.objects.filter(
|
and not Issue.objects.filter(
|
||||||
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
|
workspace_id=self.context.get("workspace_id"),
|
||||||
|
pk=data.get("parent").id,
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@@ -238,9 +243,13 @@ class IssueSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
if "labels" in self.fields:
|
if "labels" in self.fields:
|
||||||
if "labels" in self.expand:
|
if "labels" in self.expand:
|
||||||
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
|
data["labels"] = LabelSerializer(
|
||||||
|
instance.labels.all(), many=True
|
||||||
|
).data
|
||||||
else:
|
else:
|
||||||
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
data["labels"] = [
|
||||||
|
str(label.id) for label in instance.labels.all()
|
||||||
|
]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -278,7 +287,8 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if IssueLink.objects.filter(
|
if IssueLink.objects.filter(
|
||||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
url=validated_data.get("url"),
|
||||||
|
issue_id=validated_data.get("issue_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@@ -324,11 +334,11 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
try:
|
try:
|
||||||
if(data.get("comment_html", None) is not None):
|
if data.get("comment_html", None) is not None:
|
||||||
parsed = html.fromstring(data["comment_html"])
|
parsed = html.fromstring(data["comment_html"])
|
||||||
parsed_str = html.tostring(parsed, encoding='unicode')
|
parsed_str = html.tostring(parsed, encoding="unicode")
|
||||||
data["comment_html"] = parsed_str
|
data["comment_html"] = parsed_str
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
||||||
return data
|
return data
|
||||||
@@ -362,7 +372,6 @@ class ModuleIssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class LabelLiteSerializer(BaseSerializer):
|
class LabelLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Label
|
model = Label
|
||||||
fields = [
|
fields = [
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
|
|
||||||
if data.get("members", []):
|
if data.get("members", []):
|
||||||
data["members"] = ProjectMember.objects.filter(
|
data["members"] = ProjectMember.objects.filter(
|
||||||
@@ -146,16 +148,16 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if ModuleLink.objects.filter(
|
if ModuleLink.objects.filter(
|
||||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
url=validated_data.get("url"),
|
||||||
|
module_id=validated_data.get("module_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
)
|
)
|
||||||
return ModuleLink.objects.create(**validated_data)
|
return ModuleLink.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
class ModuleLiteSerializer(BaseSerializer):
|
class ModuleLiteSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember, State, Estimate
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
ProjectIdentifier,
|
||||||
|
WorkspaceMember,
|
||||||
|
State,
|
||||||
|
Estimate,
|
||||||
|
)
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
|
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
total_cycles = serializers.IntegerField(read_only=True)
|
total_cycles = serializers.IntegerField(read_only=True)
|
||||||
total_modules = serializers.IntegerField(read_only=True)
|
total_modules = serializers.IntegerField(read_only=True)
|
||||||
@@ -21,7 +26,7 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
'emoji',
|
"emoji",
|
||||||
"workspace",
|
"workspace",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
@@ -59,12 +64,16 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
identifier = validated_data.get("identifier", "").strip().upper()
|
identifier = validated_data.get("identifier", "").strip().upper()
|
||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is required"
|
||||||
|
)
|
||||||
|
|
||||||
if ProjectIdentifier.objects.filter(
|
if ProjectIdentifier.objects.filter(
|
||||||
name=identifier, workspace_id=self.context["workspace_id"]
|
name=identifier, workspace_id=self.context["workspace_id"]
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is taken"
|
||||||
|
)
|
||||||
|
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**validated_data, workspace_id=self.context["workspace_id"]
|
||||||
@@ -89,4 +98,4 @@ class ProjectLiteSerializer(BaseSerializer):
|
|||||||
"emoji",
|
"emoji",
|
||||||
"description",
|
"description",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
|
|||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
# If the default is being provided then make all other states default False
|
# If the default is being provided then make all other states default False
|
||||||
if data.get("default", False):
|
if data.get("default", False):
|
||||||
State.objects.filter(project_id=self.context.get("project_id")).update(
|
State.objects.filter(
|
||||||
default=False
|
project_id=self.context.get("project_id")
|
||||||
)
|
).update(default=False)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -35,4 +35,4 @@ class StateLiteSerializer(BaseSerializer):
|
|||||||
"color",
|
"color",
|
||||||
"group",
|
"group",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ class UserLiteSerializer(BaseSerializer):
|
|||||||
"avatar",
|
"avatar",
|
||||||
"display_name",
|
"display_name",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .base import BaseSerializer
|
|||||||
|
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
"""Lite serializer with only required fields"""
|
"""Lite serializer with only required fields"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
fields = [
|
fields = [
|
||||||
@@ -12,4 +13,4 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
|||||||
"slug",
|
"slug",
|
||||||
"id",
|
"id",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ urlpatterns = [
|
|||||||
*cycle_patterns,
|
*cycle_patterns,
|
||||||
*module_patterns,
|
*module_patterns,
|
||||||
*inbox_patterns,
|
*inbox_patterns,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -32,4 +32,4 @@ urlpatterns = [
|
|||||||
TransferCycleIssueAPIEndpoint.as_view(),
|
TransferCycleIssueAPIEndpoint.as_view(),
|
||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ urlpatterns = [
|
|||||||
InboxIssueAPIEndpoint.as_view(),
|
InboxIssueAPIEndpoint.as_view(),
|
||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ urlpatterns = [
|
|||||||
ModuleIssueAPIEndpoint.as_view(),
|
ModuleIssueAPIEndpoint.as_view(),
|
||||||
name="module-issues",
|
name="module-issues",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from django.urls import path
|
|||||||
from plane.api.views import ProjectAPIEndpoint
|
from plane.api.views import ProjectAPIEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/",
|
"workspaces/<str:slug>/projects/",
|
||||||
ProjectAPIEndpoint.as_view(),
|
ProjectAPIEndpoint.as_view(),
|
||||||
name="project",
|
name="project",
|
||||||
@@ -13,4 +13,4 @@ urlpatterns = [
|
|||||||
ProjectAPIEndpoint.as_view(),
|
ProjectAPIEndpoint.as_view(),
|
||||||
name="project",
|
name="project",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ urlpatterns = [
|
|||||||
StateAPIEndpoint.as_view(),
|
StateAPIEndpoint.as_view(),
|
||||||
name="states",
|
name="states",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ from .cycle import (
|
|||||||
|
|
||||||
from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
|
from .module import ModuleAPIEndpoint, ModuleIssueAPIEndpoint
|
||||||
|
|
||||||
from .inbox import InboxIssueAPIEndpoint
|
from .inbox import InboxIssueAPIEndpoint
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ class WebhookMixin:
|
|||||||
bulk = False
|
bulk = False
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
response = super().finalize_response(request, response, *args, **kwargs)
|
response = super().finalize_response(
|
||||||
|
request, response, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
# Check for the case should webhook be sent
|
||||||
if (
|
if (
|
||||||
@@ -139,7 +141,9 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
# Call super to get the default response
|
# Call super to get the default response
|
||||||
response = super().finalize_response(request, response, *args, **kwargs)
|
response = super().finalize_response(
|
||||||
|
request, response, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# Add custom headers if they exist in the request META
|
# Add custom headers if they exist in the request META
|
||||||
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
|
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
|
||||||
@@ -163,13 +167,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
@property
|
@property
|
||||||
def fields(self):
|
def fields(self):
|
||||||
fields = [
|
fields = [
|
||||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
field
|
||||||
|
for field in self.request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
]
|
]
|
||||||
return fields if fields else None
|
return fields if fields else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def expand(self):
|
def expand(self):
|
||||||
expand = [
|
expand = [
|
||||||
expand for expand in self.request.GET.get("expand", "").split(",") if expand
|
expand
|
||||||
|
for expand in self.request.GET.get("expand", "").split(",")
|
||||||
|
if expand
|
||||||
]
|
]
|
||||||
return expand if expand else None
|
return expand if expand else None
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ from rest_framework import status
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView, WebhookMixin
|
||||||
from plane.db.models import Cycle, Issue, CycleIssue, IssueLink, IssueAttachment
|
from plane.db.models import (
|
||||||
|
Cycle,
|
||||||
|
Issue,
|
||||||
|
CycleIssue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
@@ -102,7 +108,9 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
.annotate(
|
||||||
|
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
@@ -201,7 +209,8 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Incomplete Cycles
|
# Incomplete Cycles
|
||||||
if cycle_view == "incomplete":
|
if cycle_view == "incomplete":
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
Q(end_date__gte=timezone.now().date())
|
||||||
|
| Q(end_date__isnull=True),
|
||||||
)
|
)
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -238,8 +247,12 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -249,15 +262,22 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
def patch(self, request, slug, project_id, pk):
|
||||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
|
||||||
request_data = request.data
|
request_data = request.data
|
||||||
|
|
||||||
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
if (
|
||||||
|
cycle.end_date is not None
|
||||||
|
and cycle.end_date < timezone.now().date()
|
||||||
|
):
|
||||||
if "sort_order" in request_data:
|
if "sort_order" in request_data:
|
||||||
# Can only change sort order
|
# Can only change sort order
|
||||||
request_data = {
|
request_data = {
|
||||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
"sort_order": request_data.get(
|
||||||
|
"sort_order", cycle.sort_order
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -275,11 +295,13 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, pk):
|
||||||
cycle_issues = list(
|
cycle_issues = list(
|
||||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
CycleIssue.objects.filter(
|
||||||
"issue", flat=True
|
cycle_id=self.kwargs.get("pk")
|
||||||
)
|
).values_list("issue", flat=True)
|
||||||
|
)
|
||||||
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
|
||||||
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
@@ -319,7 +341,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
CycleIssue.objects.annotate(
|
CycleIssue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue_id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -342,7 +366,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -364,7 +390,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -387,14 +415,18 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
if not issues:
|
if not issues:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
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():
|
if (
|
||||||
|
cycle.end_date is not None
|
||||||
|
and cycle.end_date < timezone.now().date()
|
||||||
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
"error": "The Cycle has already been completed so no new issues can be added"
|
||||||
@@ -479,7 +511,10 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, cycle_id, issue_id):
|
def delete(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
cycle_issue = CycleIssue.objects.get(
|
cycle_issue = CycleIssue.objects.get(
|
||||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
issue_id = cycle_issue.issue_id
|
issue_id = cycle_issue.issue_id
|
||||||
cycle_issue.delete()
|
cycle_issue.delete()
|
||||||
@@ -550,4 +585,4 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||||||
updated_cycles, ["cycle_id"], batch_size=100
|
updated_cycles, ["cycle_id"], batch_size=100
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ from rest_framework.response import Response
|
|||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectLitePermission
|
from plane.app.permissions import ProjectLitePermission
|
||||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||||
from plane.db.models import InboxIssue, Issue, State, ProjectMember, Project, Inbox
|
from plane.db.models import (
|
||||||
|
InboxIssue,
|
||||||
|
Issue,
|
||||||
|
State,
|
||||||
|
ProjectMember,
|
||||||
|
Project,
|
||||||
|
Inbox,
|
||||||
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +50,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
project = Project.objects.get(
|
project = Project.objects.get(
|
||||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
pk=self.kwargs.get("project_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if inbox is None and not project.inbox_view:
|
if inbox is None and not project.inbox_view:
|
||||||
@@ -51,7 +59,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
InboxIssue.objects.filter(
|
InboxIssue.objects.filter(
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
inbox_id=inbox.id,
|
inbox_id=inbox.id,
|
||||||
@@ -87,7 +96,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
inbox = Inbox.objects.filter(
|
inbox = Inbox.objects.filter(
|
||||||
@@ -117,7 +127,8 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
"none",
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create or get state
|
# Create or get state
|
||||||
@@ -222,10 +233,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
"description_html": issue_data.get(
|
"description_html": issue_data.get(
|
||||||
"description_html", issue.description_html
|
"description_html", issue.description_html
|
||||||
),
|
),
|
||||||
"description": issue_data.get("description", issue.description),
|
"description": issue_data.get(
|
||||||
|
"description", issue.description
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
|
issue_serializer = IssueSerializer(
|
||||||
|
issue, data=issue_data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
if issue_serializer.is_valid():
|
if issue_serializer.is_valid():
|
||||||
current_instance = issue
|
current_instance = issue
|
||||||
@@ -266,7 +281,9 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
group="cancelled",
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
@@ -284,17 +301,22 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
if issue.state.name == "Triage":
|
if issue.state.name == "Triage":
|
||||||
# Move to default state
|
# Move to default state
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, default=True
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
default=True,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
InboxIssueSerializer(inbox_issue).data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id):
|
def delete(self, request, slug, project_id, issue_id):
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -86,7 +88,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, pk=None):
|
||||||
if pk:
|
if pk:
|
||||||
issue = Issue.issue_objects.annotate(
|
issue = Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -102,7 +106,13 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@@ -117,7 +127,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -127,7 +139,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -175,7 +189,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
@@ -209,7 +225,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
# Track the issue
|
# Track the issue
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
@@ -220,7 +238,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
@@ -250,7 +270,9 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk=None):
|
def delete(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
@@ -297,11 +319,17 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
serializer = LabelSerializer(data=request.data)
|
serializer = LabelSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Label with the same name already exists in the project"},
|
{
|
||||||
|
"error": "Label with the same name already exists in the project"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -318,7 +346,11 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
label = self.get_queryset().get(pk=pk)
|
label = self.get_queryset().get(pk=pk)
|
||||||
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
|
serializer = LabelSerializer(
|
||||||
|
label,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk=None):
|
def patch(self, request, slug, project_id, pk=None):
|
||||||
@@ -328,7 +360,6 @@ class LabelAPIEndpoint(BaseAPIView):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk=None):
|
def delete(self, request, slug, project_id, pk=None):
|
||||||
label = self.get_queryset().get(pk=pk)
|
label = self.get_queryset().get(pk=pk)
|
||||||
@@ -395,7 +426,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="link.activity.created",
|
type="link.activity.created",
|
||||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
serializer.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@@ -407,14 +440,19 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
)
|
)
|
||||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
serializer = IssueLinkSerializer(
|
||||||
|
issue_link, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -431,7 +469,10 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
@@ -466,7 +507,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
IssueComment.objects.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug")
|
||||||
|
)
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(issue_id=self.kwargs.get("issue_id"))
|
.filter(issue_id=self.kwargs.get("issue_id"))
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
@@ -518,7 +561,9 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="comment.activity.created",
|
type="comment.activity.created",
|
||||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
serializer.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@@ -530,7 +575,10 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
@@ -556,7 +604,10 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, issue_id, pk):
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueCommentSerializer(issue_comment).data,
|
IssueCommentSerializer(issue_comment).data,
|
||||||
@@ -591,7 +642,7 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.select_related("actor", "workspace", "issue", "project")
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
).order_by(request.GET.get("order_by", "created_at"))
|
).order_by(request.GET.get("order_by", "created_at"))
|
||||||
|
|
||||||
if pk:
|
if pk:
|
||||||
issue_activities = issue_activities.get(pk=pk)
|
issue_activities = issue_activities.get(pk=pk)
|
||||||
serializer = IssueActivitySerializer(issue_activities)
|
serializer = IssueActivitySerializer(issue_activities)
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"link_module",
|
"link_module",
|
||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
queryset=ModuleLink.objects.select_related(
|
||||||
|
"module", "created_by"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@@ -122,17 +124,30 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
serializer = ModuleSerializer(data=request.data, context={"project_id": project_id, "workspace_id": project.workspace_id})
|
serializer = ModuleSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
module = Module.objects.get(pk=serializer.data["id"])
|
module = Module.objects.get(pk=serializer.data["id"])
|
||||||
serializer = ModuleSerializer(module)
|
serializer = ModuleSerializer(module)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
def patch(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
module = Module.objects.get(
|
||||||
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
|
pk=pk, project_id=project_id, workspace__slug=slug
|
||||||
|
)
|
||||||
|
serializer = ModuleSerializer(
|
||||||
|
module,
|
||||||
|
data=request.data,
|
||||||
|
context={"project_id": project_id},
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@@ -162,9 +177,13 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
module = Module.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
module_issues = list(
|
module_issues = list(
|
||||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||||
|
"issue", flat=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
@@ -204,7 +223,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
ModuleIssue.objects.annotate(
|
ModuleIssue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -228,7 +249,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -250,7 +273,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -271,7 +296,8 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||||
@@ -354,7 +380,10 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
def delete(self, request, slug, project_id, module_id, issue_id):
|
def delete(self, request, slug, project_id, module_id, issue_id):
|
||||||
module_issue = ModuleIssue.objects.get(
|
module_issue = ModuleIssue.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=module_id,
|
||||||
|
issue_id=issue_id,
|
||||||
)
|
)
|
||||||
module_issue.delete()
|
module_issue.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -371,4 +400,4 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -39,9 +39,15 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
.filter(
|
||||||
|
Q(project_projectmember__member=self.request.user)
|
||||||
|
| Q(network=2)
|
||||||
|
)
|
||||||
.select_related(
|
.select_related(
|
||||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
"workspace",
|
||||||
|
"workspace__owner",
|
||||||
|
"default_assignee",
|
||||||
|
"project_lead",
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_member=Exists(
|
is_member=Exists(
|
||||||
@@ -120,11 +126,18 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
request=request,
|
request=request,
|
||||||
queryset=(projects),
|
queryset=(projects),
|
||||||
on_results=lambda projects: ProjectSerializer(
|
on_results=lambda projects: ProjectSerializer(
|
||||||
projects, many=True, fields=self.fields, expand=self.expand,
|
projects,
|
||||||
|
many=True,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
||||||
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
|
serializer = ProjectSerializer(
|
||||||
|
project,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
@@ -138,7 +151,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
# Add the user as Administrator to the project
|
# Add the user as Administrator to the project
|
||||||
project_member = ProjectMember.objects.create(
|
project_member = ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"], member=request.user, role=20
|
project_id=serializer.data["id"],
|
||||||
|
member=request.user,
|
||||||
|
role=20,
|
||||||
)
|
)
|
||||||
# Also create the issue property for the user
|
# Also create the issue property for the user
|
||||||
_ = IssueProperty.objects.create(
|
_ = IssueProperty.objects.create(
|
||||||
@@ -211,9 +226,15 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -226,7 +247,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
except Workspace.DoesNotExist as e:
|
except Workspace.DoesNotExist as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Workspace does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -250,7 +272,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
if serializer.data["inbox_view"]:
|
if serializer.data["inbox_view"]:
|
||||||
Inbox.objects.get_or_create(
|
Inbox.objects.get_or_create(
|
||||||
name=f"{project.name} Inbox", project=project, is_default=True
|
name=f"{project.name} Inbox",
|
||||||
|
project=project,
|
||||||
|
is_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the triage state in Backlog group
|
# Create the triage state in Backlog group
|
||||||
@@ -262,10 +286,16 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
color="#ff7700",
|
color="#ff7700",
|
||||||
)
|
)
|
||||||
|
|
||||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
@@ -274,7 +304,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
)
|
)
|
||||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Project does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -285,4 +316,4 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
def delete(self, request, slug, project_id):
|
def delete(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
project.delete()
|
project.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
serializer = StateSerializer(data=request.data, context={"project_id": project_id})
|
serializer = StateSerializer(
|
||||||
|
data=request.data, context={"project_id": project_id}
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -64,14 +66,19 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
if state.default:
|
||||||
return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Default state cannot be deleted"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
||||||
|
|
||||||
if issue_exist:
|
if issue_exist:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The state is not empty, only empty states can be deleted"},
|
{
|
||||||
|
"error": "The state is not empty, only empty states can be deleted"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,9 +86,11 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, state_id=None):
|
def patch(self, request, slug, project_id, state_id=None):
|
||||||
state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
|
state = State.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=state_id
|
||||||
|
)
|
||||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
|||||||
def validate_api_token(self, token):
|
def validate_api_token(self, token):
|
||||||
try:
|
try:
|
||||||
api_token = APIToken.objects.get(
|
api_token = APIToken.objects.get(
|
||||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
Q(
|
||||||
|
Q(expired_at__gt=timezone.now())
|
||||||
|
| Q(expired_at__isnull=True)
|
||||||
|
),
|
||||||
token=token,
|
token=token,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceBasePermission,
|
WorkSpaceBasePermission,
|
||||||
WorkspaceOwnerPermission,
|
WorkspaceOwnerPermission,
|
||||||
@@ -13,5 +12,3 @@ from .project import (
|
|||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from .workspace import (
|
|||||||
WorkspaceThemeSerializer,
|
WorkspaceThemeSerializer,
|
||||||
WorkspaceMemberAdminSerializer,
|
WorkspaceMemberAdminSerializer,
|
||||||
WorkspaceMemberMeSerializer,
|
WorkspaceMemberMeSerializer,
|
||||||
|
WorkspaceUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
from .project import (
|
from .project import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
@@ -31,14 +32,20 @@ from .project import (
|
|||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
ProjectMemberAdminSerializer,
|
ProjectMemberAdminSerializer,
|
||||||
ProjectPublicMemberSerializer,
|
ProjectPublicMemberSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
)
|
)
|
||||||
from .state import StateSerializer, StateLiteSerializer
|
from .state import StateSerializer, StateLiteSerializer
|
||||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
from .view import (
|
||||||
|
GlobalViewSerializer,
|
||||||
|
IssueViewSerializer,
|
||||||
|
IssueViewFavoriteSerializer,
|
||||||
|
)
|
||||||
from .cycle import (
|
from .cycle import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
CycleFavoriteSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
|
CycleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetSerializer
|
from .asset import FileAssetSerializer
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@@ -61,6 +68,7 @@ from .issue import (
|
|||||||
IssueRelationSerializer,
|
IssueRelationSerializer,
|
||||||
RelatedIssueSerializer,
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
|
IssueRelationLiteSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module import (
|
||||||
@@ -69,6 +77,7 @@ from .module import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
|
ModuleUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import APITokenSerializer, APITokenReadSerializer
|
from .api import APITokenSerializer, APITokenReadSerializer
|
||||||
@@ -85,7 +94,12 @@ from .integration import (
|
|||||||
|
|
||||||
from .importer import ImporterSerializer
|
from .importer import ImporterSerializer
|
||||||
|
|
||||||
from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
|
from .page import (
|
||||||
|
PageSerializer,
|
||||||
|
PageLogSerializer,
|
||||||
|
SubPageSerializer,
|
||||||
|
PageFavoriteSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
EstimateSerializer,
|
EstimateSerializer,
|
||||||
@@ -93,7 +107,11 @@ from .estimate import (
|
|||||||
EstimateReadSerializer,
|
EstimateReadSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer
|
from .inbox import (
|
||||||
|
InboxSerializer,
|
||||||
|
InboxIssueSerializer,
|
||||||
|
IssueStateInboxSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from .analytic import AnalyticViewSerializer
|
from .analytic import AnalyticViewSerializer
|
||||||
|
|
||||||
@@ -101,4 +119,4 @@ from .notification import NotificationSerializer
|
|||||||
|
|
||||||
from .exporter import ExporterHistorySerializer
|
from .exporter import ExporterHistorySerializer
|
||||||
|
|
||||||
from .webhook import WebhookSerializer, WebhookLogSerializer
|
from .webhook import WebhookSerializer, WebhookLogSerializer
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from plane.db.models import APIToken, APIActivityLog
|
|||||||
|
|
||||||
|
|
||||||
class APITokenSerializer(BaseSerializer):
|
class APITokenSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIToken
|
model = APIToken
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -18,14 +17,12 @@ class APITokenSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class APITokenReadSerializer(BaseSerializer):
|
class APITokenReadSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIToken
|
model = APIToken
|
||||||
exclude = ('token',)
|
exclude = ("token",)
|
||||||
|
|
||||||
|
|
||||||
class APIActivityLogSerializer(BaseSerializer):
|
class APIActivityLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = APIActivityLog
|
model = APIActivityLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ from rest_framework import serializers
|
|||||||
class BaseSerializer(serializers.ModelSerializer):
|
class BaseSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
class DynamicBaseSerializer(BaseSerializer):
|
|
||||||
|
|
||||||
|
class DynamicBaseSerializer(BaseSerializer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# If 'fields' is provided in the arguments, remove it and store it separately.
|
# If 'fields' is provided in the arguments, remove it and store it separately.
|
||||||
# This is done so as not to pass this custom argument up to the superclass.
|
# This is done so as not to pass this custom argument up to the superclass.
|
||||||
fields = kwargs.pop("fields", None)
|
fields = kwargs.pop("fields", [])
|
||||||
|
self.expand = kwargs.pop("expand", []) or []
|
||||||
|
fields = self.expand
|
||||||
|
|
||||||
# Call the initialization of the superclass.
|
# Call the initialization of the superclass.
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
# If 'fields' was provided, filter the fields of the serializer accordingly.
|
||||||
if fields is not None:
|
if fields is not None:
|
||||||
self.fields = self._filter_fields(fields)
|
self.fields = self._filter_fields(fields)
|
||||||
@@ -31,7 +32,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
# loop through its keys and values.
|
# loop through its keys and values.
|
||||||
if isinstance(field_name, dict):
|
if isinstance(field_name, dict):
|
||||||
for key, value in field_name.items():
|
for key, value in field_name.items():
|
||||||
# If the value of this nested field is a list,
|
# If the value of this nested field is a list,
|
||||||
# perform a recursive filter on it.
|
# perform a recursive filter on it.
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
self._filter_fields(self.fields[key], value)
|
self._filter_fields(self.fields[key], value)
|
||||||
@@ -47,12 +48,98 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
elif isinstance(item, dict):
|
elif isinstance(item, dict):
|
||||||
allowed.append(list(item.keys())[0])
|
allowed.append(list(item.keys())[0])
|
||||||
|
|
||||||
# Convert the current serializer's fields and the allowed fields to sets.
|
for field in allowed:
|
||||||
existing = set(self.fields)
|
if field not in self.fields:
|
||||||
allowed = set(allowed)
|
from . import (
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
StateLiteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
LabelSerializer,
|
||||||
|
CycleIssueSerializer,
|
||||||
|
IssueFlatSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
# Remove fields from the serializer that aren't in the 'allowed' list.
|
# Expansion mapper
|
||||||
for field_name in (existing - allowed):
|
expansion = {
|
||||||
self.fields.pop(field_name)
|
"user": UserLiteSerializer,
|
||||||
|
"workspace": WorkspaceLiteSerializer,
|
||||||
|
"project": ProjectLiteSerializer,
|
||||||
|
"default_assignee": UserLiteSerializer,
|
||||||
|
"project_lead": UserLiteSerializer,
|
||||||
|
"state": StateLiteSerializer,
|
||||||
|
"created_by": UserLiteSerializer,
|
||||||
|
"issue": IssueSerializer,
|
||||||
|
"actor": UserLiteSerializer,
|
||||||
|
"owned_by": UserLiteSerializer,
|
||||||
|
"members": UserLiteSerializer,
|
||||||
|
"assignees": UserLiteSerializer,
|
||||||
|
"labels": LabelSerializer,
|
||||||
|
"issue_cycle": CycleIssueSerializer,
|
||||||
|
"parent": IssueSerializer,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fields[field] = expansion[field](
|
||||||
|
many=True
|
||||||
|
if field
|
||||||
|
in ["members", "assignees", "labels", "issue_cycle"]
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
return self.fields
|
return self.fields
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
response = super().to_representation(instance)
|
||||||
|
|
||||||
|
# Ensure 'expand' is iterable before processing
|
||||||
|
if self.expand:
|
||||||
|
for expand in self.expand:
|
||||||
|
if expand in self.fields:
|
||||||
|
# Import all the expandable serializers
|
||||||
|
from . import (
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
StateLiteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
LabelSerializer,
|
||||||
|
CycleIssueSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Expansion mapper
|
||||||
|
expansion = {
|
||||||
|
"user": UserLiteSerializer,
|
||||||
|
"workspace": WorkspaceLiteSerializer,
|
||||||
|
"project": ProjectLiteSerializer,
|
||||||
|
"default_assignee": UserLiteSerializer,
|
||||||
|
"project_lead": UserLiteSerializer,
|
||||||
|
"state": StateLiteSerializer,
|
||||||
|
"created_by": UserLiteSerializer,
|
||||||
|
"issue": IssueSerializer,
|
||||||
|
"actor": UserLiteSerializer,
|
||||||
|
"owned_by": UserLiteSerializer,
|
||||||
|
"members": UserLiteSerializer,
|
||||||
|
"assignees": UserLiteSerializer,
|
||||||
|
"labels": LabelSerializer,
|
||||||
|
"issue_cycle": CycleIssueSerializer,
|
||||||
|
"parent": IssueSerializer,
|
||||||
|
}
|
||||||
|
# Check if field in expansion then expand the field
|
||||||
|
if expand in expansion:
|
||||||
|
if isinstance(response.get(expand), list):
|
||||||
|
exp_serializer = expansion[expand](
|
||||||
|
getattr(instance, expand), many=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
exp_serializer = expansion[expand](
|
||||||
|
getattr(instance, expand)
|
||||||
|
)
|
||||||
|
response[expand] = exp_serializer.data
|
||||||
|
else:
|
||||||
|
# You might need to handle this case differently
|
||||||
|
response[expand] = getattr(
|
||||||
|
instance, f"{expand}_id", None
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ from .user import UserLiteSerializer
|
|||||||
from .issue import IssueStateSerializer
|
from .issue import IssueStateSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
from plane.db.models import (
|
||||||
|
Cycle,
|
||||||
|
CycleIssue,
|
||||||
|
CycleFavorite,
|
||||||
|
CycleUserProperties,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CycleWriteSerializer(BaseSerializer):
|
class CycleWriteSerializer(BaseSerializer):
|
||||||
@@ -17,7 +22,9 @@ class CycleWriteSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed end date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -38,7 +45,9 @@ class CycleSerializer(BaseSerializer):
|
|||||||
total_estimates = serializers.IntegerField(read_only=True)
|
total_estimates = serializers.IntegerField(read_only=True)
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
completed_estimates = serializers.IntegerField(read_only=True)
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
started_estimates = serializers.IntegerField(read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
status = serializers.CharField(read_only=True)
|
status = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
@@ -48,7 +57,9 @@ class CycleSerializer(BaseSerializer):
|
|||||||
and data.get("end_date", None) is not None
|
and data.get("end_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("end_date", None)
|
and data.get("start_date", None) > data.get("end_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed end date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_assignees(self, obj):
|
def get_assignees(self, obj):
|
||||||
@@ -106,3 +117,14 @@ class CycleFavoriteSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CycleUserPropertiesSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = CycleUserProperties
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"cycle" "user",
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
from plane.db.models import Estimate, EstimatePoint
|
from plane.db.models import Estimate, EstimatePoint
|
||||||
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
|
from plane.app.serializers import (
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class EstimateSerializer(BaseSerializer):
|
class EstimateSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -20,13 +26,14 @@ class EstimateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class EstimatePointSerializer(BaseSerializer):
|
class EstimatePointSerializer(BaseSerializer):
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if not data:
|
if not data:
|
||||||
raise serializers.ValidationError("Estimate points are required")
|
raise serializers.ValidationError("Estimate points are required")
|
||||||
value = data.get("value")
|
value = data.get("value")
|
||||||
if value and len(value) > 20:
|
if value and len(value) > 20:
|
||||||
raise serializers.ValidationError("Value can't be more than 20 characters")
|
raise serializers.ValidationError(
|
||||||
|
"Value can't be more than 20 characters"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -41,7 +48,9 @@ class EstimatePointSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class EstimateReadSerializer(BaseSerializer):
|
class EstimateReadSerializer(BaseSerializer):
|
||||||
points = EstimatePointSerializer(read_only=True, many=True)
|
points = EstimatePointSerializer(read_only=True, many=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from .user import UserLiteSerializer
|
|||||||
|
|
||||||
|
|
||||||
class ExporterHistorySerializer(BaseSerializer):
|
class ExporterHistorySerializer(BaseSerializer):
|
||||||
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
initiated_by_detail = UserLiteSerializer(
|
||||||
|
source="initiated_by", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ExporterHistory
|
model = ExporterHistory
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ from plane.db.models import Importer
|
|||||||
|
|
||||||
|
|
||||||
class ImporterSerializer(BaseSerializer):
|
class ImporterSerializer(BaseSerializer):
|
||||||
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
initiated_by_detail = UserLiteSerializer(
|
||||||
|
source="initiated_by", read_only=True
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Importer
|
model = Importer
|
||||||
|
|||||||
@@ -46,10 +46,13 @@ class InboxIssueLiteSerializer(BaseSerializer):
|
|||||||
class IssueStateInboxSerializer(BaseSerializer):
|
class IssueStateInboxSerializer(BaseSerializer):
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
read_only=True, source="labels", many=True
|
||||||
|
)
|
||||||
|
assignee_details = UserLiteSerializer(
|
||||||
|
read_only=True, source="assignees", many=True
|
||||||
|
)
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
bridge_id = serializers.UUIDField(read_only=True)
|
|
||||||
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ class IntegrationSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceIntegrationSerializer(BaseSerializer):
|
class WorkspaceIntegrationSerializer(BaseSerializer):
|
||||||
integration_detail = IntegrationSerializer(read_only=True, source="integration")
|
integration_detail = IntegrationSerializer(
|
||||||
|
read_only=True, source="integration"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceIntegration
|
model = WorkspaceIntegration
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ from plane.db.models import (
|
|||||||
CommentReaction,
|
CommentReaction,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
IssueRelation,
|
IssueRelation,
|
||||||
|
State,
|
||||||
|
Project,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,19 +71,26 @@ class IssueProjectLiteSerializer(BaseSerializer):
|
|||||||
##TODO: Find a better way to write this serializer
|
##TODO: Find a better way to write this serializer
|
||||||
## Find a better approach to save manytomany?
|
## Find a better approach to save manytomany?
|
||||||
class IssueCreateSerializer(BaseSerializer):
|
class IssueCreateSerializer(BaseSerializer):
|
||||||
state_detail = StateSerializer(read_only=True, source="state")
|
# ids
|
||||||
created_by_detail = UserLiteSerializer(read_only=True, source="created_by")
|
state_id = serializers.PrimaryKeyRelatedField(
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
source="state",
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
queryset=State.objects.all(),
|
||||||
|
required=False,
|
||||||
assignees = serializers.ListField(
|
allow_null=True,
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
)
|
||||||
|
parent_id = serializers.PrimaryKeyRelatedField(
|
||||||
|
source="parent",
|
||||||
|
queryset=Issue.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
label_ids = serializers.ListField(
|
||||||
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
assignee_ids = serializers.ListField(
|
||||||
labels = serializers.ListField(
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
@@ -100,8 +109,10 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()]
|
assignee_ids = self.initial_data.get("assignee_ids")
|
||||||
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
data["assignee_ids"] = assignee_ids if assignee_ids else []
|
||||||
|
label_ids = self.initial_data.get("label_ids")
|
||||||
|
data["label_ids"] = label_ids if label_ids else []
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@@ -110,12 +121,14 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
and data.get("target_date", None) is not None
|
and data.get("target_date", None) is not None
|
||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
assignees = validated_data.pop("assignees", None)
|
assignees = validated_data.pop("assignee_ids", None)
|
||||||
labels = validated_data.pop("labels", None)
|
labels = validated_data.pop("label_ids", None)
|
||||||
|
|
||||||
project_id = self.context["project_id"]
|
project_id = self.context["project_id"]
|
||||||
workspace_id = self.context["workspace_id"]
|
workspace_id = self.context["workspace_id"]
|
||||||
@@ -173,8 +186,8 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
return issue
|
return issue
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
assignees = validated_data.pop("assignees", None)
|
assignees = validated_data.pop("assignee_ids", None)
|
||||||
labels = validated_data.pop("labels", None)
|
labels = validated_data.pop("label_ids", None)
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
@@ -225,14 +238,15 @@ class IssueActivitySerializer(BaseSerializer):
|
|||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueActivity
|
model = IssueActivity
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssuePropertySerializer(BaseSerializer):
|
class IssuePropertySerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueProperty
|
model = IssueProperty
|
||||||
@@ -245,7 +259,9 @@ class IssuePropertySerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class LabelSerializer(BaseSerializer):
|
class LabelSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -268,7 +284,6 @@ class LabelLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueLabelSerializer(BaseSerializer):
|
class IssueLabelSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueLabel
|
model = IssueLabel
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -278,34 +293,45 @@ class IssueLabelSerializer(BaseSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class IssueRelationSerializer(BaseSerializer):
|
class IssueRelationLiteSerializer(DynamicBaseSerializer):
|
||||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
|
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = Issue
|
||||||
fields = [
|
fields = [
|
||||||
"issue_detail",
|
"id",
|
||||||
"relation_type",
|
"project_id",
|
||||||
"related_issue",
|
"sequence_id",
|
||||||
"issue",
|
|
||||||
"id"
|
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRelationSerializer(BaseSerializer):
|
||||||
|
issue_detail = IssueRelationLiteSerializer(
|
||||||
|
read_only=True, source="related_issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = IssueRelation
|
||||||
|
fields = [
|
||||||
|
"issue_detail",
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class RelatedIssueSerializer(BaseSerializer):
|
class RelatedIssueSerializer(BaseSerializer):
|
||||||
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
|
issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueRelation
|
model = IssueRelation
|
||||||
fields = [
|
fields = [
|
||||||
"issue_detail",
|
"issue_detail",
|
||||||
"relation_type",
|
|
||||||
"related_issue",
|
|
||||||
"issue",
|
|
||||||
"id"
|
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"workspace",
|
"workspace",
|
||||||
@@ -400,7 +426,8 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if IssueLink.objects.filter(
|
if IssueLink.objects.filter(
|
||||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
url=validated_data.get("url"),
|
||||||
|
issue_id=validated_data.get("issue_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@@ -424,9 +451,8 @@ class IssueAttachmentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueReactionSerializer(BaseSerializer):
|
class IssueReactionSerializer(BaseSerializer):
|
||||||
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueReaction
|
model = IssueReaction
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -459,12 +485,18 @@ class CommentReactionSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class IssueVoteSerializer(BaseSerializer):
|
class IssueVoteSerializer(BaseSerializer):
|
||||||
|
|
||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueVote
|
model = IssueVote
|
||||||
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
fields = [
|
||||||
|
"issue",
|
||||||
|
"vote",
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"actor",
|
||||||
|
"actor_detail",
|
||||||
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
@@ -472,8 +504,12 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
|
comment_reactions = CommentReactionLiteSerializer(
|
||||||
|
read_only=True, many=True
|
||||||
|
)
|
||||||
is_member = serializers.BooleanField(read_only=True)
|
is_member = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -507,12 +543,15 @@ class IssueStateFlatSerializer(BaseSerializer):
|
|||||||
|
|
||||||
# Issue Serializer with state details
|
# Issue Serializer with state details
|
||||||
class IssueStateSerializer(DynamicBaseSerializer):
|
class IssueStateSerializer(DynamicBaseSerializer):
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
|
read_only=True, source="labels", many=True
|
||||||
|
)
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
assignee_details = UserLiteSerializer(
|
||||||
|
read_only=True, source="assignees", many=True
|
||||||
|
)
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
bridge_id = serializers.UUIDField(read_only=True)
|
|
||||||
attachment_count = serializers.IntegerField(read_only=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
link_count = serializers.IntegerField(read_only=True)
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@@ -521,40 +560,76 @@ class IssueStateSerializer(DynamicBaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(BaseSerializer):
|
class IssueSerializer(DynamicBaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
# ids
|
||||||
state_detail = StateSerializer(read_only=True, source="state")
|
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
|
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
label_details = LabelSerializer(read_only=True, source="labels", many=True)
|
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
|
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
|
|
||||||
issue_cycle = IssueCycleDetailSerializer(read_only=True)
|
# Many to many
|
||||||
issue_module = IssueModuleDetailSerializer(read_only=True)
|
label_ids = serializers.PrimaryKeyRelatedField(
|
||||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
read_only=True, many=True, source="labels"
|
||||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
)
|
||||||
|
assignee_ids = serializers.PrimaryKeyRelatedField(
|
||||||
|
read_only=True, many=True, source="assignees"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Count items
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
# is_subscribed
|
||||||
|
is_subscribed = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = "__all__"
|
fields = [
|
||||||
read_only_fields = [
|
"id",
|
||||||
"workspace",
|
"name",
|
||||||
"project",
|
"state_id",
|
||||||
"created_by",
|
"description_html",
|
||||||
"updated_by",
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_id",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_subscribed",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
]
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
class IssueLiteSerializer(DynamicBaseSerializer):
|
class IssueLiteSerializer(DynamicBaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
|
read_only=True, source="labels", many=True
|
||||||
|
)
|
||||||
|
assignee_details = UserLiteSerializer(
|
||||||
|
read_only=True, source="assignees", many=True
|
||||||
|
)
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
cycle_id = serializers.UUIDField(read_only=True)
|
cycle_id = serializers.UUIDField(read_only=True)
|
||||||
module_id = serializers.UUIDField(read_only=True)
|
module_id = serializers.UUIDField(read_only=True)
|
||||||
@@ -581,7 +656,9 @@ class IssueLiteSerializer(DynamicBaseSerializer):
|
|||||||
class IssuePublicSerializer(BaseSerializer):
|
class IssuePublicSerializer(BaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
|
reactions = IssueReactionSerializer(
|
||||||
|
read_only=True, many=True, source="issue_reactions"
|
||||||
|
)
|
||||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -604,7 +681,6 @@ class IssuePublicSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssueSubscriberSerializer(BaseSerializer):
|
class IssueSubscriberSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueSubscriber
|
model = IssueSubscriber
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
@@ -14,6 +14,7 @@ from plane.db.models import (
|
|||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +26,9 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
@@ -38,16 +41,22 @@ class ModuleWriteSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data['members'] = [str(member.id) for member in instance.members.all()]
|
data["members"] = [str(member.id) for member in instance.members.all()]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
|
if (
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
data.get("start_date", None) is not None
|
||||||
return data
|
and data.get("target_date", None) is not None
|
||||||
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Start date cannot exceed target date"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
members = validated_data.pop("members", None)
|
members = validated_data.pop("members", None)
|
||||||
@@ -151,7 +160,8 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
# Validation if url already exists
|
# Validation if url already exists
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
if ModuleLink.objects.filter(
|
if ModuleLink.objects.filter(
|
||||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
url=validated_data.get("url"),
|
||||||
|
module_id=validated_data.get("module_id"),
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "URL already exists for this Issue"}
|
{"error": "URL already exists for this Issue"}
|
||||||
@@ -159,10 +169,12 @@ class ModuleLinkSerializer(BaseSerializer):
|
|||||||
return ModuleLink.objects.create(**validated_data)
|
return ModuleLink.objects.create(**validated_data)
|
||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(BaseSerializer):
|
class ModuleSerializer(DynamicBaseSerializer):
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
lead_detail = UserLiteSerializer(read_only=True, source="lead")
|
||||||
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
|
members_detail = UserLiteSerializer(
|
||||||
|
read_only=True, many=True, source="members"
|
||||||
|
)
|
||||||
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
link_module = ModuleLinkSerializer(read_only=True, many=True)
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
@@ -196,3 +208,10 @@ class ModuleFavoriteSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"user",
|
"user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleUserPropertiesSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleUserProperties
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = ["workspace", "project", "module", "user"]
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ from .base import BaseSerializer
|
|||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from plane.db.models import Notification
|
from plane.db.models import Notification
|
||||||
|
|
||||||
|
|
||||||
class NotificationSerializer(BaseSerializer):
|
class NotificationSerializer(BaseSerializer):
|
||||||
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
|
triggered_by_details = UserLiteSerializer(
|
||||||
|
read_only=True, source="triggered_by"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Notification
|
model = Notification
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,31 @@ from .base import BaseSerializer
|
|||||||
from .issue import IssueFlatSerializer, LabelLiteSerializer
|
from .issue import IssueFlatSerializer, LabelLiteSerializer
|
||||||
from .workspace import WorkspaceLiteSerializer
|
from .workspace import WorkspaceLiteSerializer
|
||||||
from .project import ProjectLiteSerializer
|
from .project import ProjectLiteSerializer
|
||||||
from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
|
from plane.db.models import (
|
||||||
|
Page,
|
||||||
|
PageLog,
|
||||||
|
PageFavorite,
|
||||||
|
PageLabel,
|
||||||
|
Label,
|
||||||
|
Issue,
|
||||||
|
Module,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PageSerializer(BaseSerializer):
|
class PageSerializer(BaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
|
label_details = LabelLiteSerializer(
|
||||||
|
read_only=True, source="labels", many=True
|
||||||
|
)
|
||||||
labels = serializers.ListField(
|
labels = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Page
|
model = Page
|
||||||
@@ -28,9 +40,10 @@ class PageSerializer(BaseSerializer):
|
|||||||
"project",
|
"project",
|
||||||
"owned_by",
|
"owned_by",
|
||||||
]
|
]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
data = super().to_representation(instance)
|
data = super().to_representation(instance)
|
||||||
data['labels'] = [str(label.id) for label in instance.labels.all()]
|
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
@@ -94,7 +107,7 @@ class SubPageSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def get_entity_details(self, obj):
|
def get_entity_details(self, obj):
|
||||||
entity_name = obj.entity_name
|
entity_name = obj.entity_name
|
||||||
if entity_name == 'forward_link' or entity_name == 'back_link':
|
if entity_name == "forward_link" or entity_name == "back_link":
|
||||||
try:
|
try:
|
||||||
page = Page.objects.get(pk=obj.entity_identifier)
|
page = Page.objects.get(pk=obj.entity_identifier)
|
||||||
return PageSerializer(page).data
|
return PageSerializer(page).data
|
||||||
@@ -104,7 +117,6 @@ class SubPageSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class PageLogSerializer(BaseSerializer):
|
class PageLogSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PageLog
|
model = PageLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ from rest_framework import serializers
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer, DynamicBaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
from plane.app.serializers.workspace import WorkspaceLiteSerializer
|
||||||
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
|
from plane.app.serializers.user import (
|
||||||
|
UserLiteSerializer,
|
||||||
|
UserAdminLiteSerializer,
|
||||||
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
@@ -17,7 +20,9 @@ from plane.db.models import (
|
|||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
@@ -29,12 +34,16 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
identifier = validated_data.get("identifier", "").strip().upper()
|
identifier = validated_data.get("identifier", "").strip().upper()
|
||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is required"
|
||||||
|
)
|
||||||
|
|
||||||
if ProjectIdentifier.objects.filter(
|
if ProjectIdentifier.objects.filter(
|
||||||
name=identifier, workspace_id=self.context["workspace_id"]
|
name=identifier, workspace_id=self.context["workspace_id"]
|
||||||
).exists():
|
).exists():
|
||||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is taken"
|
||||||
|
)
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**validated_data, workspace_id=self.context["workspace_id"]
|
||||||
)
|
)
|
||||||
@@ -73,7 +82,9 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
return project
|
return project
|
||||||
|
|
||||||
# If not same fail update
|
# If not same fail update
|
||||||
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
raise serializers.ValidationError(
|
||||||
|
detail="Project Identifier is already taken"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProjectLiteSerializer(BaseSerializer):
|
class ProjectLiteSerializer(BaseSerializer):
|
||||||
@@ -160,6 +171,12 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ProjectMember
|
||||||
|
fields = ("id", "role", "member", "project")
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteSerializer(BaseSerializer):
|
class ProjectMemberInviteSerializer(BaseSerializer):
|
||||||
project = ProjectLiteSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
@@ -197,7 +214,9 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
read_only=True, source="workspace"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProjectDeployBoard
|
model = ProjectDeployBoard
|
||||||
@@ -217,4 +236,4 @@ class ProjectPublicMemberSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"member",
|
"member",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from plane.db.models import State
|
|||||||
|
|
||||||
|
|
||||||
class StateSerializer(BaseSerializer):
|
class StateSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = State
|
model = State
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -25,4 +24,4 @@ class StateLiteSerializer(BaseSerializer):
|
|||||||
"color",
|
"color",
|
||||||
"group",
|
"group",
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
).first()
|
).first()
|
||||||
return {
|
return {
|
||||||
"last_workspace_id": obj.last_workspace_id,
|
"last_workspace_id": obj.last_workspace_id,
|
||||||
"last_workspace_slug": workspace.slug if workspace is not None else "",
|
"last_workspace_slug": workspace.slug
|
||||||
|
if workspace is not None
|
||||||
|
else "",
|
||||||
"fallback_workspace_id": obj.last_workspace_id,
|
"fallback_workspace_id": obj.last_workspace_id,
|
||||||
"fallback_workspace_slug": workspace.slug
|
"fallback_workspace_slug": workspace.slug
|
||||||
if workspace is not None
|
if workspace is not None
|
||||||
@@ -109,7 +111,8 @@ class UserMeSettingsSerializer(BaseSerializer):
|
|||||||
else:
|
else:
|
||||||
fallback_workspace = (
|
fallback_workspace = (
|
||||||
Workspace.objects.filter(
|
Workspace.objects.filter(
|
||||||
workspace_member__member_id=obj.id, workspace_member__is_active=True
|
workspace_member__member_id=obj.id,
|
||||||
|
workspace_member__is_active=True,
|
||||||
)
|
)
|
||||||
.order_by("created_at")
|
.order_by("created_at")
|
||||||
.first()
|
.first()
|
||||||
@@ -180,7 +183,9 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
if data.get("new_password") != data.get("confirm_password"):
|
if data.get("new_password") != data.get("confirm_password"):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"error": "Confirm password should be same as the new password."}
|
{
|
||||||
|
"error": "Confirm password should be same as the new password."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -190,4 +195,5 @@ class ResetPasswordSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
Serializer for password change endpoint.
|
Serializer for password change endpoint.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
new_password = serializers.CharField(required=True, min_length=8)
|
new_password = serializers.CharField(required=True, min_length=8)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
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, IssueViewFavorite
|
||||||
@@ -10,7 +10,9 @@ from plane.utils.issue_filters import issue_filters
|
|||||||
|
|
||||||
|
|
||||||
class GlobalViewSerializer(BaseSerializer):
|
class GlobalViewSerializer(BaseSerializer):
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GlobalView
|
model = GlobalView
|
||||||
@@ -38,10 +40,12 @@ class GlobalViewSerializer(BaseSerializer):
|
|||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSerializer(BaseSerializer):
|
class IssueViewSerializer(DynamicBaseSerializer):
|
||||||
is_favorite = serializers.BooleanField(read_only=True)
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
workspace_detail = WorkspaceLiteSerializer(
|
||||||
|
source="workspace", read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueView
|
model = IssueView
|
||||||
|
|||||||
@@ -10,78 +10,113 @@ from rest_framework import serializers
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import DynamicBaseSerializer
|
from .base import DynamicBaseSerializer
|
||||||
from plane.db.models import Webhook, WebhookLog
|
from plane.db.models import Webhook, WebhookLog
|
||||||
from plane.db.models.webhook import validate_domain, validate_schema
|
from plane.db.models.webhook import validate_domain, validate_schema
|
||||||
|
|
||||||
|
|
||||||
class WebhookSerializer(DynamicBaseSerializer):
|
class WebhookSerializer(DynamicBaseSerializer):
|
||||||
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
url = validated_data.get("url", None)
|
url = validated_data.get("url", None)
|
||||||
|
|
||||||
# Extract the hostname from the URL
|
# Extract the hostname from the URL
|
||||||
hostname = urlparse(url).hostname
|
hostname = urlparse(url).hostname
|
||||||
if not hostname:
|
if not hostname:
|
||||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Invalid URL: No hostname found."}
|
||||||
|
)
|
||||||
|
|
||||||
# Resolve the hostname to IP addresses
|
# Resolve the hostname to IP addresses
|
||||||
try:
|
try:
|
||||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Hostname could not be resolved."}
|
||||||
|
)
|
||||||
|
|
||||||
if not ip_addresses:
|
if not ip_addresses:
|
||||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "No IP addresses found for the hostname."}
|
||||||
|
)
|
||||||
|
|
||||||
for addr in ip_addresses:
|
for addr in ip_addresses:
|
||||||
ip = ipaddress.ip_address(addr[4][0])
|
ip = ipaddress.ip_address(addr[4][0])
|
||||||
if ip.is_private or ip.is_loopback:
|
if ip.is_private or ip.is_loopback:
|
||||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "URL resolves to a blocked IP address."}
|
||||||
|
)
|
||||||
|
|
||||||
# Additional validation for multiple request domains and their subdomains
|
# Additional validation for multiple request domains and their subdomains
|
||||||
request = self.context.get('request')
|
request = self.context.get("request")
|
||||||
disallowed_domains = ['plane.so',] # Add your disallowed domains here
|
disallowed_domains = [
|
||||||
|
"plane.so",
|
||||||
|
] # Add your disallowed domains here
|
||||||
if request:
|
if request:
|
||||||
request_host = request.get_host().split(':')[0] # Remove port if present
|
request_host = request.get_host().split(":")[
|
||||||
|
0
|
||||||
|
] # Remove port if present
|
||||||
disallowed_domains.append(request_host)
|
disallowed_domains.append(request_host)
|
||||||
|
|
||||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
# Check if hostname is a subdomain or exact match of any disallowed domain
|
||||||
if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
|
if any(
|
||||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
hostname == domain or hostname.endswith("." + domain)
|
||||||
|
for domain in disallowed_domains
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"url": "URL domain or its subdomain is not allowed."}
|
||||||
|
)
|
||||||
|
|
||||||
return Webhook.objects.create(**validated_data)
|
return Webhook.objects.create(**validated_data)
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
url = validated_data.get("url", None)
|
url = validated_data.get("url", None)
|
||||||
if url:
|
if url:
|
||||||
# Extract the hostname from the URL
|
# Extract the hostname from the URL
|
||||||
hostname = urlparse(url).hostname
|
hostname = urlparse(url).hostname
|
||||||
if not hostname:
|
if not hostname:
|
||||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Invalid URL: No hostname found."}
|
||||||
|
)
|
||||||
|
|
||||||
# Resolve the hostname to IP addresses
|
# Resolve the hostname to IP addresses
|
||||||
try:
|
try:
|
||||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "Hostname could not be resolved."}
|
||||||
|
)
|
||||||
|
|
||||||
if not ip_addresses:
|
if not ip_addresses:
|
||||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "No IP addresses found for the hostname."}
|
||||||
|
)
|
||||||
|
|
||||||
for addr in ip_addresses:
|
for addr in ip_addresses:
|
||||||
ip = ipaddress.ip_address(addr[4][0])
|
ip = ipaddress.ip_address(addr[4][0])
|
||||||
if ip.is_private or ip.is_loopback:
|
if ip.is_private or ip.is_loopback:
|
||||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
raise serializers.ValidationError(
|
||||||
|
{"url": "URL resolves to a blocked IP address."}
|
||||||
|
)
|
||||||
|
|
||||||
# Additional validation for multiple request domains and their subdomains
|
# Additional validation for multiple request domains and their subdomains
|
||||||
request = self.context.get('request')
|
request = self.context.get("request")
|
||||||
disallowed_domains = ['plane.so',] # Add your disallowed domains here
|
disallowed_domains = [
|
||||||
|
"plane.so",
|
||||||
|
] # Add your disallowed domains here
|
||||||
if request:
|
if request:
|
||||||
request_host = request.get_host().split(':')[0] # Remove port if present
|
request_host = request.get_host().split(":")[
|
||||||
|
0
|
||||||
|
] # Remove port if present
|
||||||
disallowed_domains.append(request_host)
|
disallowed_domains.append(request_host)
|
||||||
|
|
||||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
# Check if hostname is a subdomain or exact match of any disallowed domain
|
||||||
if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
|
if any(
|
||||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
hostname == domain or hostname.endswith("." + domain)
|
||||||
|
for domain in disallowed_domains
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"url": "URL domain or its subdomain is not allowed."}
|
||||||
|
)
|
||||||
|
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
@@ -95,12 +130,7 @@ class WebhookSerializer(DynamicBaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WebhookLogSerializer(DynamicBaseSerializer):
|
class WebhookLogSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WebhookLog
|
model = WebhookLog
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = ["workspace", "webhook"]
|
||||||
"workspace",
|
|
||||||
"webhook"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer, DynamicBaseSerializer
|
||||||
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
from .user import UserLiteSerializer, UserAdminLiteSerializer
|
||||||
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
@@ -13,10 +13,11 @@ from plane.db.models import (
|
|||||||
TeamMember,
|
TeamMember,
|
||||||
WorkspaceMemberInvite,
|
WorkspaceMemberInvite,
|
||||||
WorkspaceTheme,
|
WorkspaceTheme,
|
||||||
|
WorkspaceUserProperties,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceSerializer(BaseSerializer):
|
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||||
owner = UserLiteSerializer(read_only=True)
|
owner = UserLiteSerializer(read_only=True)
|
||||||
total_members = serializers.IntegerField(read_only=True)
|
total_members = serializers.IntegerField(read_only=True)
|
||||||
total_issues = serializers.IntegerField(read_only=True)
|
total_issues = serializers.IntegerField(read_only=True)
|
||||||
@@ -50,6 +51,7 @@ class WorkSpaceSerializer(BaseSerializer):
|
|||||||
"owner",
|
"owner",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLiteSerializer(BaseSerializer):
|
class WorkspaceLiteSerializer(BaseSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workspace
|
model = Workspace
|
||||||
@@ -61,8 +63,7 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
|||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
class WorkSpaceMemberSerializer(DynamicBaseSerializer):
|
||||||
class WorkSpaceMemberSerializer(BaseSerializer):
|
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -72,13 +73,12 @@ class WorkSpaceMemberSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberMeSerializer(BaseSerializer):
|
class WorkspaceMemberMeSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMember
|
model = WorkspaceMember
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceMemberAdminSerializer(BaseSerializer):
|
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
|
||||||
member = UserAdminLiteSerializer(read_only=True)
|
member = UserAdminLiteSerializer(read_only=True)
|
||||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
|
|
||||||
@@ -108,7 +108,9 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class TeamSerializer(BaseSerializer):
|
class TeamSerializer(BaseSerializer):
|
||||||
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
|
members_detail = UserLiteSerializer(
|
||||||
|
read_only=True, source="members", many=True
|
||||||
|
)
|
||||||
members = serializers.ListField(
|
members = serializers.ListField(
|
||||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
|
||||||
write_only=True,
|
write_only=True,
|
||||||
@@ -145,7 +147,9 @@ class TeamSerializer(BaseSerializer):
|
|||||||
members = validated_data.pop("members")
|
members = validated_data.pop("members")
|
||||||
TeamMember.objects.filter(team=instance).delete()
|
TeamMember.objects.filter(team=instance).delete()
|
||||||
team_members = [
|
team_members = [
|
||||||
TeamMember(member=member, team=instance, workspace=instance.workspace)
|
TeamMember(
|
||||||
|
member=member, team=instance, workspace=instance.workspace
|
||||||
|
)
|
||||||
for member in members
|
for member in members
|
||||||
]
|
]
|
||||||
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
TeamMember.objects.bulk_create(team_members, batch_size=10)
|
||||||
@@ -161,3 +165,13 @@ class WorkspaceThemeSerializer(BaseSerializer):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"actor",
|
"actor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPropertiesSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = WorkspaceUserProperties
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"user",
|
||||||
|
]
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ urlpatterns = [
|
|||||||
*workspace_urls,
|
*workspace_urls,
|
||||||
*api_urls,
|
*api_urls,
|
||||||
*webhook_urls,
|
*webhook_urls,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -31,8 +31,14 @@ urlpatterns = [
|
|||||||
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
path("sign-in/", SignInEndpoint.as_view(), name="sign-in"),
|
||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# magic sign in
|
# magic sign in
|
||||||
path("magic-generate/", MagicGenerateEndpoint.as_view(), name="magic-generate"),
|
path(
|
||||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
"magic-generate/",
|
||||||
|
MagicGenerateEndpoint.as_view(),
|
||||||
|
name="magic-generate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
||||||
|
),
|
||||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
# Password Manipulation
|
# Password Manipulation
|
||||||
path(
|
path(
|
||||||
@@ -52,6 +58,8 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path(
|
||||||
|
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
||||||
|
),
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
|
||||||
from plane.app.views import ConfigurationEndpoint
|
from plane.app.views import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
@@ -9,4 +9,9 @@ urlpatterns = [
|
|||||||
ConfigurationEndpoint.as_view(),
|
ConfigurationEndpoint.as_view(),
|
||||||
name="configuration",
|
name="configuration",
|
||||||
),
|
),
|
||||||
]
|
path(
|
||||||
|
"mobile-configs/",
|
||||||
|
MobileConfigurationEndpoint.as_view(),
|
||||||
|
name="configuration",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -7,10 +7,17 @@ from plane.app.views import (
|
|||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
|
CycleUserPropertiesEndpoint,
|
||||||
|
ActiveCycleEndpoint
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/active-cycles/",
|
||||||
|
ActiveCycleEndpoint.as_view(),
|
||||||
|
name="workspace-active-cycle",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
|
||||||
CycleViewSet.as_view(
|
CycleViewSet.as_view(
|
||||||
@@ -44,7 +51,7 @@ urlpatterns = [
|
|||||||
name="project-issue-cycle",
|
name="project-issue-cycle",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
|
||||||
CycleIssueViewSet.as_view(
|
CycleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
@@ -84,4 +91,9 @@ urlpatterns = [
|
|||||||
TransferCycleIssueEndpoint.as_view(),
|
TransferCycleIssueEndpoint.as_view(),
|
||||||
name="transfer-issues",
|
name="transfer-issues",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
|
||||||
|
CycleUserPropertiesEndpoint.as_view(),
|
||||||
|
name="cycle-user-filters",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ urlpatterns = [
|
|||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
|
||||||
InboxIssueViewSet.as_view(
|
InboxIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ urlpatterns = [
|
|||||||
## End Comment Reactions
|
## End Comment Reactions
|
||||||
## IssueProperty
|
## IssueProperty
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
|
||||||
IssueUserDisplayPropertyEndpoint.as_view(),
|
IssueUserDisplayPropertyEndpoint.as_view(),
|
||||||
name="project-issue-display-properties",
|
name="project-issue-display-properties",
|
||||||
),
|
),
|
||||||
@@ -275,16 +275,17 @@ urlpatterns = [
|
|||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
|
||||||
IssueRelationViewSet.as_view(
|
IssueRelationViewSet.as_view(
|
||||||
{
|
{
|
||||||
|
"get": "list",
|
||||||
"post": "create",
|
"post": "create",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="issue-relation",
|
name="issue-relation",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
|
||||||
IssueRelationViewSet.as_view(
|
IssueRelationViewSet.as_view(
|
||||||
{
|
{
|
||||||
"delete": "destroy",
|
"post": "remove_relation",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="issue-relation",
|
name="issue-relation",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from plane.app.views import (
|
|||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
BulkImportModulesEndpoint,
|
BulkImportModulesEndpoint,
|
||||||
|
ModuleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ urlpatterns = [
|
|||||||
name="project-module-issues",
|
name="project-module-issues",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
|
||||||
ModuleIssueViewSet.as_view(
|
ModuleIssueViewSet.as_view(
|
||||||
{
|
{
|
||||||
"get": "retrieve",
|
"get": "retrieve",
|
||||||
@@ -101,4 +102,9 @@ urlpatterns = [
|
|||||||
BulkImportModulesEndpoint.as_view(),
|
BulkImportModulesEndpoint.as_view(),
|
||||||
name="bulk-modules-create",
|
name="bulk-modules-create",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
|
||||||
|
ModuleUserPropertiesEndpoint.as_view(),
|
||||||
|
name="cycle-user-filters",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -175,4 +175,4 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-deploy-board",
|
name="project-deploy-board",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from plane.app.views import (
|
|||||||
IssueViewViewSet,
|
IssueViewViewSet,
|
||||||
GlobalViewViewSet,
|
GlobalViewViewSet,
|
||||||
GlobalViewIssuesViewSet,
|
GlobalViewIssuesViewSet,
|
||||||
IssueViewFavoriteViewSet,
|
IssueViewFavoriteViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from plane.app.views import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
|
WorkspaceProjectMemberEndpoint,
|
||||||
|
WorkspaceUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +94,11 @@ urlpatterns = [
|
|||||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||||
name="workspace-member",
|
name="workspace-member",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/project-members/",
|
||||||
|
WorkspaceProjectMemberEndpoint.as_view(),
|
||||||
|
name="workspace-member-roles",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||||
WorkSpaceMemberViewSet.as_view(
|
WorkSpaceMemberViewSet.as_view(
|
||||||
@@ -195,4 +202,9 @@ urlpatterns = [
|
|||||||
WorkspaceLabelsEndpoint.as_view(),
|
WorkspaceLabelsEndpoint.as_view(),
|
||||||
name="workspace-labels",
|
name="workspace-labels",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/user-properties/",
|
||||||
|
WorkspaceUserPropertiesEndpoint.as_view(),
|
||||||
|
name="workspace-user-filters",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ from plane.app.views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#TODO: Delete this file
|
# TODO: Delete this file
|
||||||
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
# This url file has been deprecated use apiserver/plane/urls folder to create new urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -204,10 +204,14 @@ urlpatterns = [
|
|||||||
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"),
|
||||||
# Magic Sign In/Up
|
# Magic Sign In/Up
|
||||||
path(
|
path(
|
||||||
"magic-generate/", MagicSignInGenerateEndpoint.as_view(), name="magic-generate"
|
"magic-generate/",
|
||||||
|
MagicSignInGenerateEndpoint.as_view(),
|
||||||
|
name="magic-generate",
|
||||||
),
|
),
|
||||||
path("magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"),
|
path(
|
||||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
"magic-sign-in/", MagicSignInEndpoint.as_view(), name="magic-sign-in"
|
||||||
|
),
|
||||||
|
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||||
# Email verification
|
# Email verification
|
||||||
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
path("email-verify/", VerifyEmailEndpoint.as_view(), name="email-verify"),
|
||||||
path(
|
path(
|
||||||
@@ -272,7 +276,9 @@ urlpatterns = [
|
|||||||
# user workspace invitations
|
# user workspace invitations
|
||||||
path(
|
path(
|
||||||
"users/me/invitations/workspaces/",
|
"users/me/invitations/workspaces/",
|
||||||
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
UserWorkspaceInvitationsEndpoint.as_view(
|
||||||
|
{"get": "list", "post": "create"}
|
||||||
|
),
|
||||||
name="user-workspace-invitations",
|
name="user-workspace-invitations",
|
||||||
),
|
),
|
||||||
# user workspace invitation
|
# user workspace invitation
|
||||||
@@ -311,7 +317,9 @@ urlpatterns = [
|
|||||||
# user project invitations
|
# user project invitations
|
||||||
path(
|
path(
|
||||||
"users/me/invitations/projects/",
|
"users/me/invitations/projects/",
|
||||||
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
UserProjectInvitationsViewset.as_view(
|
||||||
|
{"get": "list", "post": "create"}
|
||||||
|
),
|
||||||
name="user-project-invitaions",
|
name="user-project-invitaions",
|
||||||
),
|
),
|
||||||
## Workspaces ##
|
## Workspaces ##
|
||||||
@@ -1238,7 +1246,7 @@ urlpatterns = [
|
|||||||
"post": "unarchive",
|
"post": "unarchive",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
name="project-page-unarchive"
|
name="project-page-unarchive",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
|
||||||
@@ -1264,19 +1272,22 @@ urlpatterns = [
|
|||||||
{
|
{
|
||||||
"post": "unlock",
|
"post": "unlock",
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
|
||||||
PageLogEndpoint.as_view(), name="page-transactions"
|
PageLogEndpoint.as_view(),
|
||||||
|
name="page-transactions",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
|
||||||
PageLogEndpoint.as_view(), name="page-transactions"
|
PageLogEndpoint.as_view(),
|
||||||
|
name="page-transactions",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
|
||||||
SubPagesEndpoint.as_view(), name="sub-page"
|
SubPagesEndpoint.as_view(),
|
||||||
|
name="sub-page",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
|
||||||
@@ -1326,7 +1337,9 @@ urlpatterns = [
|
|||||||
## End Pages
|
## End Pages
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path(
|
||||||
|
"api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"
|
||||||
|
),
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
# Integrations
|
# Integrations
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ from .workspace import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
|
WorkspaceProjectMemberEndpoint,
|
||||||
|
WorkspaceUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import (
|
from .view import (
|
||||||
@@ -59,6 +61,8 @@ from .cycle import (
|
|||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
|
CycleUserPropertiesEndpoint,
|
||||||
|
ActiveCycleEndpoint,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||||
from .issue import (
|
from .issue import (
|
||||||
@@ -103,6 +107,7 @@ from .module import (
|
|||||||
ModuleIssueViewSet,
|
ModuleIssueViewSet,
|
||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
|
ModuleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .api import ApiTokenEndpoint
|
from .api import ApiTokenEndpoint
|
||||||
@@ -136,7 +141,11 @@ from .page import (
|
|||||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
|
|
||||||
|
|
||||||
from .external import GPTIntegrationEndpoint, ReleaseNotesEndpoint, UnsplashEndpoint
|
from .external import (
|
||||||
|
GPTIntegrationEndpoint,
|
||||||
|
ReleaseNotesEndpoint,
|
||||||
|
UnsplashEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate import (
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
@@ -161,7 +170,7 @@ from .notification import (
|
|||||||
|
|
||||||
from .exporter import ExportIssuesEndpoint
|
from .exporter import ExportIssuesEndpoint
|
||||||
|
|
||||||
from .config import ConfigurationEndpoint
|
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||||
|
|
||||||
from .webhook import (
|
from .webhook import (
|
||||||
WebhookEndpoint,
|
WebhookEndpoint,
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If segment is present it cannot be same as x-axis
|
# If segment is present it cannot be same as x-axis
|
||||||
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
if segment and (
|
||||||
|
segment not in valid_xaxis_segment or x_axis == segment
|
||||||
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
@@ -110,7 +112,9 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||||
assignee_details = (
|
assignee_details = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug, **filters, assignees__avatar__isnull=False
|
workspace__slug=slug,
|
||||||
|
**filters,
|
||||||
|
assignees__avatar__isnull=False,
|
||||||
)
|
)
|
||||||
.order_by("assignees__id")
|
.order_by("assignees__id")
|
||||||
.distinct("assignees__id")
|
.distinct("assignees__id")
|
||||||
@@ -124,7 +128,9 @@ class AnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cycle_details = {}
|
cycle_details = {}
|
||||||
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
|
if x_axis in ["issue_cycle__cycle_id"] or segment in [
|
||||||
|
"issue_cycle__cycle_id"
|
||||||
|
]:
|
||||||
cycle_details = (
|
cycle_details = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@@ -186,7 +192,9 @@ class AnalyticViewViewset(BaseViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -196,7 +204,9 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, analytic_id):
|
def get(self, request, slug, analytic_id):
|
||||||
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
|
analytic_view = AnalyticView.objects.get(
|
||||||
|
pk=analytic_id, workspace__slug=slug
|
||||||
|
)
|
||||||
|
|
||||||
filter = analytic_view.query
|
filter = analytic_view.query
|
||||||
queryset = Issue.issue_objects.filter(**filter)
|
queryset = Issue.issue_objects.filter(**filter)
|
||||||
@@ -266,7 +276,9 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If segment is present it cannot be same as x-axis
|
# If segment is present it cannot be same as x-axis
|
||||||
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
|
if segment and (
|
||||||
|
segment not in valid_xaxis_segment or x_axis == segment
|
||||||
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Both segment and x axis cannot be same and segment should be valid"
|
"error": "Both segment and x axis cannot be same and segment should be valid"
|
||||||
@@ -293,7 +305,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
filters = issue_filters(request.GET, "GET")
|
filters = issue_filters(request.GET, "GET")
|
||||||
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
|
base_issues = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, **filters
|
||||||
|
)
|
||||||
|
|
||||||
total_issues = base_issues.count()
|
total_issues = base_issues.count()
|
||||||
|
|
||||||
@@ -306,7 +320,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
open_issues_groups = ["backlog", "unstarted", "started"]
|
open_issues_groups = ["backlog", "unstarted", "started"]
|
||||||
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
|
open_issues_queryset = state_groups.filter(
|
||||||
|
state__group__in=open_issues_groups
|
||||||
|
)
|
||||||
|
|
||||||
open_issues = open_issues_queryset.count()
|
open_issues = open_issues_queryset.count()
|
||||||
open_issues_classified = (
|
open_issues_classified = (
|
||||||
@@ -361,10 +377,12 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
|||||||
.order_by("-count")
|
.order_by("-count")
|
||||||
)
|
)
|
||||||
|
|
||||||
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("estimate_point"))[
|
open_estimate_sum = open_issues_queryset.aggregate(
|
||||||
|
sum=Sum("estimate_point")
|
||||||
|
)["sum"]
|
||||||
|
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))[
|
||||||
"sum"
|
"sum"
|
||||||
]
|
]
|
||||||
total_estimate_sum = base_issues.aggregate(sum=Sum("estimate_point"))["sum"]
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ class ApiTokenEndpoint(BaseAPIView):
|
|||||||
user=request.user,
|
user=request.user,
|
||||||
pk=pk,
|
pk=pk,
|
||||||
)
|
)
|
||||||
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
|
serializer = APITokenSerializer(
|
||||||
|
api_token, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ from plane.app.serializers import FileAssetSerializer
|
|||||||
|
|
||||||
|
|
||||||
class FileAssetEndpoint(BaseAPIView):
|
class FileAssetEndpoint(BaseAPIView):
|
||||||
parser_classes = (MultiPartParser, FormParser, JSONParser,)
|
parser_classes = (
|
||||||
|
MultiPartParser,
|
||||||
|
FormParser,
|
||||||
|
JSONParser,
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A viewset for viewing and editing task instances.
|
A viewset for viewing and editing task instances.
|
||||||
@@ -20,10 +24,18 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
files = FileAsset.objects.filter(asset=asset_key)
|
files = FileAsset.objects.filter(asset=asset_key)
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
serializer = FileAssetSerializer(
|
||||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
files, context={"request": request}, many=True
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"data": serializer.data, "status": True},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"error": "Asset key does not exist", "status": False},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
@@ -33,7 +45,7 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
serializer.save(workspace_id=workspace.id)
|
serializer.save(workspace_id=workspace.id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def delete(self, request, workspace_id, asset_key):
|
def delete(self, request, workspace_id, asset_key):
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||||
@@ -43,7 +55,6 @@ class FileAssetEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class FileAssetViewSet(BaseViewSet):
|
class FileAssetViewSet(BaseViewSet):
|
||||||
|
|
||||||
def restore(self, request, workspace_id, asset_key):
|
def restore(self, request, workspace_id, asset_key):
|
||||||
asset_key = str(workspace_id) + "/" + asset_key
|
asset_key = str(workspace_id) + "/" + asset_key
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key)
|
file_asset = FileAsset.objects.get(asset=asset_key)
|
||||||
@@ -56,12 +67,22 @@ class UserAssetsEndpoint(BaseAPIView):
|
|||||||
parser_classes = (MultiPartParser, FormParser)
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
def get(self, request, asset_key):
|
def get(self, request, asset_key):
|
||||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
files = FileAsset.objects.filter(
|
||||||
|
asset=asset_key, created_by=request.user
|
||||||
|
)
|
||||||
if files.exists():
|
if files.exists():
|
||||||
serializer = FileAssetSerializer(files, context={"request": request})
|
serializer = FileAssetSerializer(
|
||||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
files, context={"request": request}
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{"data": serializer.data, "status": True},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"error": "Asset key does not exist", "status": False},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
serializer = FileAssetSerializer(data=request.data)
|
||||||
@@ -70,9 +91,10 @@ class UserAssetsEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def delete(self, request, asset_key):
|
def delete(self, request, asset_key):
|
||||||
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
|
file_asset = FileAsset.objects.get(
|
||||||
|
asset=asset_key, created_by=request.user
|
||||||
|
)
|
||||||
file_asset.is_deleted = True
|
file_asset.is_deleted = True
|
||||||
file_asset.save()
|
file_asset.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ class ForgotPasswordEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Please check the email"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Please check the email"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -167,7 +168,9 @@ class ResetPasswordEndpoint(BaseAPIView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
except DjangoUnicodeDecodeError as indentifier:
|
except DjangoUnicodeDecodeError as indentifier:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -191,7 +194,8 @@ class ChangePasswordEndpoint(BaseAPIView):
|
|||||||
user.is_password_autoset = False
|
user.is_password_autoset = False
|
||||||
user.save()
|
user.save()
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Password updated successfully"}, status=status.HTTP_200_OK
|
{"message": "Password updated successfully"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@@ -213,7 +217,8 @@ class SetUserPasswordEndpoint(BaseAPIView):
|
|||||||
# Check password validation
|
# Check password validation
|
||||||
if not password and len(str(password)) < 8:
|
if not password and len(str(password)) < 8:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Password is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Password is not valid"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set the user password
|
# Set the user password
|
||||||
@@ -281,7 +286,9 @@ class MagicGenerateEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if data["current_attempt"] > 2:
|
if data["current_attempt"] > 2:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
{
|
||||||
|
"error": "Max attempts exhausted. Please try again later."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,7 +346,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Email is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# validate the email
|
# validate the email
|
||||||
@@ -347,7 +355,8 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
validate_email(email)
|
validate_email(email)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Email is not valid"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Email is not valid"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the user exists
|
# Check if the user exists
|
||||||
@@ -399,13 +408,18 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
if not current_attempt:
|
if not current_attempt:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
{
|
||||||
|
"error": "Max attempts exhausted. Please try again later."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
# Trigger the email
|
# Trigger the email
|
||||||
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
magic_link.delay(email, "magic_" + str(email), token, current_site)
|
||||||
return Response(
|
return Response(
|
||||||
{"is_password_autoset": user.is_password_autoset, "is_existing": False},
|
{
|
||||||
|
"is_password_autoset": user.is_password_autoset,
|
||||||
|
"is_existing": False,
|
||||||
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -433,7 +447,9 @@ class EmailCheckEndpoint(BaseAPIView):
|
|||||||
key, token, current_attempt = generate_magic_token(email=email)
|
key, token, current_attempt = generate_magic_token(email=email)
|
||||||
if not current_attempt:
|
if not current_attempt:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Max attempts exhausted. Please try again later."},
|
{
|
||||||
|
"error": "Max attempts exhausted. Please try again later."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# get configuration values
|
# get configuration values
|
||||||
# Get configuration values
|
# Get configuration values
|
||||||
ENABLE_SIGNUP, = get_configuration_value(
|
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "ENABLE_SIGNUP",
|
"key": "ENABLE_SIGNUP",
|
||||||
@@ -173,7 +173,7 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# Create the user
|
# Create the user
|
||||||
else:
|
else:
|
||||||
ENABLE_SIGNUP, = get_configuration_value(
|
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "ENABLE_SIGNUP",
|
"key": "ENABLE_SIGNUP",
|
||||||
@@ -364,8 +364,10 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
# Check if user has any accepted invites for workspace and add them to workspace
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
workspace_member_invites = (
|
||||||
email=user.email, accepted=True
|
WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
@@ -431,7 +433,9 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Your login code was incorrect. Please try again."},
|
{
|
||||||
|
"error": "Your login code was incorrect. Please try again."
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ class WebhookMixin:
|
|||||||
bulk = False
|
bulk = False
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
response = super().finalize_response(request, response, *args, **kwargs)
|
response = super().finalize_response(
|
||||||
|
request, response, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
# Check for the case should webhook be sent
|
||||||
if (
|
if (
|
||||||
@@ -88,7 +90,9 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
return self.model.objects.all()
|
return self.model.objects.all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
raise APIException(
|
||||||
|
"Please check the view", status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
def handle_exception(self, exc):
|
def handle_exception(self, exc):
|
||||||
"""
|
"""
|
||||||
@@ -99,6 +103,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
response = super().handle_exception(exc)
|
response = super().handle_exception(exc)
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(e) if settings.DEBUG else print("Server Error")
|
||||||
if isinstance(e, IntegrityError):
|
if isinstance(e, IntegrityError):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The payload is not valid"},
|
{"error": "The payload is not valid"},
|
||||||
@@ -123,11 +128,12 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
{"error": f"The required key does not exist."},
|
{"error": f"The required key does not exist."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(e) if settings.DEBUG else print("Server Error")
|
|
||||||
capture_exception(e)
|
|
||||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@@ -158,6 +164,24 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
|||||||
if resolve(self.request.path_info).url_name == "project":
|
if resolve(self.request.path_info).url_name == "project":
|
||||||
return self.kwargs.get("pk", None)
|
return self.kwargs.get("pk", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in self.request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
return fields if fields else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expand(self):
|
||||||
|
expand = [
|
||||||
|
expand
|
||||||
|
for expand in self.request.GET.get("expand", "").split(",")
|
||||||
|
if expand
|
||||||
|
]
|
||||||
|
return expand if expand else None
|
||||||
|
|
||||||
|
|
||||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@@ -204,15 +228,20 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
{"error": f"The required object does not exist."},
|
{"error": f"The required object does not exist."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(e, KeyError):
|
if isinstance(e, KeyError):
|
||||||
return Response({"error": f"The required key does not exist."}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": f"The required key does not exist."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
print(e)
|
print(e)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@@ -237,3 +266,21 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
|||||||
@property
|
@property
|
||||||
def project_id(self):
|
def project_id(self):
|
||||||
return self.kwargs.get("project_id", None)
|
return self.kwargs.get("project_id", None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fields(self):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in self.request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
return fields if fields else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expand(self):
|
||||||
|
expand = [
|
||||||
|
expand
|
||||||
|
for expand in self.request.GET.get("expand", "").split(",")
|
||||||
|
if expand
|
||||||
|
]
|
||||||
|
return expand if expand else None
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
# Get all the configuration
|
# Get all the configuration
|
||||||
(
|
(
|
||||||
GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID,
|
||||||
@@ -90,8 +89,16 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
# Authentication
|
# Authentication
|
||||||
data["google_client_id"] = GOOGLE_CLIENT_ID if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != "\"\"" else None
|
data["google_client_id"] = (
|
||||||
data["github_client_id"] = GITHUB_CLIENT_ID if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != "\"\"" else None
|
GOOGLE_CLIENT_ID
|
||||||
|
if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
data["github_client_id"] = (
|
||||||
|
GITHUB_CLIENT_ID
|
||||||
|
if GITHUB_CLIENT_ID and GITHUB_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
data["github_app_name"] = GITHUB_APP_NAME
|
data["github_app_name"] = GITHUB_APP_NAME
|
||||||
data["magic_login"] = (
|
data["magic_login"] = (
|
||||||
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||||
@@ -112,9 +119,129 @@ class ConfigurationEndpoint(BaseAPIView):
|
|||||||
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||||
|
|
||||||
# File size settings
|
# File size settings
|
||||||
data["file_size_limit"] = float(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
data["file_size_limit"] = float(
|
||||||
|
os.environ.get("FILE_SIZE_LIMIT", 5242880)
|
||||||
|
)
|
||||||
|
|
||||||
# is self managed
|
# is smtp configured
|
||||||
data["is_self_managed"] = bool(int(os.environ.get("IS_SELF_MANAGED", "1")))
|
data["is_smtp_configured"] = bool(EMAIL_HOST_USER) and bool(
|
||||||
|
EMAIL_HOST_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class MobileConfigurationEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
(
|
||||||
|
GOOGLE_CLIENT_ID,
|
||||||
|
GOOGLE_SERVER_CLIENT_ID,
|
||||||
|
GOOGLE_IOS_CLIENT_ID,
|
||||||
|
EMAIL_HOST_USER,
|
||||||
|
EMAIL_HOST_PASSWORD,
|
||||||
|
ENABLE_MAGIC_LINK_LOGIN,
|
||||||
|
ENABLE_EMAIL_PASSWORD,
|
||||||
|
POSTHOG_API_KEY,
|
||||||
|
POSTHOG_HOST,
|
||||||
|
UNSPLASH_ACCESS_KEY,
|
||||||
|
OPENAI_API_KEY,
|
||||||
|
) = get_configuration_value(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GOOGLE_CLIENT_ID", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_SERVER_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GOOGLE_SERVER_CLIENT_ID", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "GOOGLE_IOS_CLIENT_ID",
|
||||||
|
"default": os.environ.get("GOOGLE_IOS_CLIENT_ID", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST_USER",
|
||||||
|
"default": os.environ.get("EMAIL_HOST_USER", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "EMAIL_HOST_PASSWORD",
|
||||||
|
"default": os.environ.get("EMAIL_HOST_PASSWORD", None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||||
|
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ENABLE_EMAIL_PASSWORD",
|
||||||
|
"default": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "POSTHOG_API_KEY",
|
||||||
|
"default": os.environ.get("POSTHOG_API_KEY", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "POSTHOG_HOST",
|
||||||
|
"default": os.environ.get("POSTHOG_HOST", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
|
"default": os.environ.get("UNSPLASH_ACCESS_KEY", "1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "OPENAI_API_KEY",
|
||||||
|
"default": os.environ.get("OPENAI_API_KEY", "1"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data = {}
|
||||||
|
# Authentication
|
||||||
|
data["google_client_id"] = (
|
||||||
|
GOOGLE_CLIENT_ID
|
||||||
|
if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
data["google_server_client_id"] = (
|
||||||
|
GOOGLE_SERVER_CLIENT_ID
|
||||||
|
if GOOGLE_SERVER_CLIENT_ID and GOOGLE_SERVER_CLIENT_ID != '""'
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
data["google_ios_client_id"] = (
|
||||||
|
(GOOGLE_IOS_CLIENT_ID)[::-1]
|
||||||
|
if GOOGLE_IOS_CLIENT_ID is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
# Posthog
|
||||||
|
data["posthog_api_key"] = POSTHOG_API_KEY
|
||||||
|
data["posthog_host"] = POSTHOG_HOST
|
||||||
|
|
||||||
|
data["magic_login"] = (
|
||||||
|
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||||
|
) and ENABLE_MAGIC_LINK_LOGIN == "1"
|
||||||
|
|
||||||
|
data["email_password_login"] = ENABLE_EMAIL_PASSWORD == "1"
|
||||||
|
|
||||||
|
# Posthog
|
||||||
|
data["posthog_api_key"] = POSTHOG_API_KEY
|
||||||
|
data["posthog_host"] = POSTHOG_HOST
|
||||||
|
|
||||||
|
# Unsplash
|
||||||
|
data["has_unsplash_configured"] = bool(UNSPLASH_ACCESS_KEY)
|
||||||
|
|
||||||
|
# Open AI settings
|
||||||
|
data["has_openai_configured"] = bool(OPENAI_API_KEY)
|
||||||
|
|
||||||
|
# File size settings
|
||||||
|
data["file_size_limit"] = float(
|
||||||
|
os.environ.get("FILE_SIZE_LIMIT", 5242880)
|
||||||
|
)
|
||||||
|
|
||||||
|
# is smtp configured
|
||||||
|
data["is_smtp_configured"] = not (
|
||||||
|
bool(EMAIL_HOST_USER) and bool(EMAIL_HOST_PASSWORD)
|
||||||
|
)
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from django.db.models import (
|
|||||||
Case,
|
Case,
|
||||||
When,
|
When,
|
||||||
Value,
|
Value,
|
||||||
CharField
|
CharField,
|
||||||
)
|
)
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -31,10 +31,16 @@ from plane.app.serializers import (
|
|||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
CycleFavoriteSerializer,
|
CycleFavoriteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
IssueStateSerializer,
|
IssueStateSerializer,
|
||||||
CycleWriteSerializer,
|
CycleWriteSerializer,
|
||||||
|
CycleUserPropertiesSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
WorkspaceUserPermission
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
User,
|
User,
|
||||||
Cycle,
|
Cycle,
|
||||||
@@ -44,9 +50,10 @@ from plane.db.models import (
|
|||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
Label,
|
Label,
|
||||||
|
CycleUserProperties,
|
||||||
|
IssueSubscriber,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
@@ -61,7 +68,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(
|
serializer.save(
|
||||||
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
owned_by=self.request.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -140,7 +148,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
.annotate(
|
||||||
|
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"issue_cycle__issue__estimate_point",
|
||||||
@@ -164,35 +174,36 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(
|
.annotate(
|
||||||
status=Case(
|
status=Case(
|
||||||
When(
|
When(
|
||||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
Q(start_date__lte=timezone.now())
|
||||||
then=Value("CURRENT")
|
& Q(end_date__gte=timezone.now()),
|
||||||
|
then=Value("CURRENT"),
|
||||||
),
|
),
|
||||||
When(
|
When(
|
||||||
start_date__gt=timezone.now(),
|
start_date__gt=timezone.now(), then=Value("UPCOMING")
|
||||||
then=Value("UPCOMING")
|
|
||||||
),
|
|
||||||
When(
|
|
||||||
end_date__lt=timezone.now(),
|
|
||||||
then=Value("COMPLETED")
|
|
||||||
),
|
),
|
||||||
|
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||||
When(
|
When(
|
||||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||||
then=Value("DRAFT")
|
then=Value("DRAFT"),
|
||||||
),
|
),
|
||||||
default=Value("DRAFT"),
|
default=Value("DRAFT"),
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_cycle__issue__assignees",
|
"issue_cycle__issue__assignees",
|
||||||
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
queryset=User.objects.only(
|
||||||
|
"avatar", "first_name", "id"
|
||||||
|
).distinct(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"issue_cycle__issue__labels",
|
"issue_cycle__issue__labels",
|
||||||
queryset=Label.objects.only("name", "color", "id").distinct(),
|
queryset=Label.objects.only(
|
||||||
|
"name", "color", "id"
|
||||||
|
).distinct(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-is_favorite", "name")
|
.order_by("-is_favorite", "name")
|
||||||
@@ -202,6 +213,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
cycle_view = request.GET.get("cycle_view", "all")
|
cycle_view = request.GET.get("cycle_view", "all")
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||||
|
|
||||||
@@ -298,7 +314,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"completion_chart": {},
|
"completion_chart": {},
|
||||||
}
|
}
|
||||||
if data[0]["start_date"] and data[0]["end_date"]:
|
if data[0]["start_date"] and data[0]["end_date"]:
|
||||||
data[0]["distribution"]["completion_chart"] = burndown_plot(
|
data[0]["distribution"][
|
||||||
|
"completion_chart"
|
||||||
|
] = burndown_plot(
|
||||||
queryset=queryset.first(),
|
queryset=queryset.first(),
|
||||||
slug=slug,
|
slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -307,44 +325,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
# Upcoming Cycles
|
cycles = CycleSerializer(queryset, many=True).data
|
||||||
if cycle_view == "upcoming":
|
return Response(cycles, status=status.HTTP_200_OK)
|
||||||
queryset = queryset.filter(start_date__gt=timezone.now())
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# Completed Cycles
|
|
||||||
if cycle_view == "completed":
|
|
||||||
queryset = queryset.filter(end_date__lt=timezone.now())
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draft Cycles
|
|
||||||
if cycle_view == "draft":
|
|
||||||
queryset = queryset.filter(
|
|
||||||
end_date=None,
|
|
||||||
start_date=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# Incomplete Cycles
|
|
||||||
if cycle_view == "incomplete":
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
# If no matching view is found return all cycles
|
|
||||||
return Response(
|
|
||||||
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
if (
|
if (
|
||||||
@@ -360,8 +342,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
cycle = (
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
serializer = CycleSerializer(cycle)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -371,15 +363,22 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
|
||||||
request_data = request.data
|
request_data = request.data
|
||||||
|
|
||||||
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
|
if (
|
||||||
|
cycle.end_date is not None
|
||||||
|
and cycle.end_date < timezone.now().date()
|
||||||
|
):
|
||||||
if "sort_order" in request_data:
|
if "sort_order" in request_data:
|
||||||
# Can only change sort order
|
# Can only change sort order
|
||||||
request_data = {
|
request_data = {
|
||||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
"sort_order": request_data.get(
|
||||||
|
"sort_order", cycle.sort_order
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -389,7 +388,9 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
|
serializer = CycleWriteSerializer(
|
||||||
|
cycle, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -410,7 +411,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"assignee_id",
|
||||||
|
"avatar",
|
||||||
|
"display_name",
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
total_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
@@ -489,7 +496,10 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
if queryset.start_date and queryset.end_date:
|
if queryset.start_date and queryset.end_date:
|
||||||
data["distribution"]["completion_chart"] = burndown_plot(
|
data["distribution"]["completion_chart"] = burndown_plot(
|
||||||
queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk
|
queryset=queryset,
|
||||||
|
slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@@ -499,11 +509,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
cycle_issues = list(
|
cycle_issues = list(
|
||||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
CycleIssue.objects.filter(
|
||||||
"issue", flat=True
|
cycle_id=self.kwargs.get("pk")
|
||||||
)
|
).values_list("issue", flat=True)
|
||||||
|
)
|
||||||
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
|
||||||
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
@@ -546,7 +558,9 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue_id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -565,18 +579,23 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id, cycle_id):
|
def list(self, request, slug, project_id, cycle_id):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(bridge_id=F("issue_cycle__id"))
|
|
||||||
.filter(project_id=project_id)
|
.filter(project_id=project_id)
|
||||||
.filter(workspace__slug=slug)
|
.filter(workspace__slug=slug)
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
@@ -587,6 +606,8 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
.order_by(order_by)
|
.order_by(order_by)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@@ -594,32 +615,43 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
serializer = IssueSerializer(
|
||||||
issues = IssueStateSerializer(
|
|
||||||
issues, many=True, fields=fields if fields else None
|
issues, many=True, fields=fields if fields else None
|
||||||
).data
|
)
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, cycle_id):
|
def create(self, request, slug, project_id, cycle_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
|
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
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():
|
if (
|
||||||
|
cycle.end_date is not None
|
||||||
|
and cycle.end_date < timezone.now().date()
|
||||||
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
"error": "The Cycle has already been completed so no new issues can be added"
|
||||||
@@ -693,16 +725,22 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Return all Cycle Issues
|
# Return all Cycle Issues
|
||||||
|
issues = self.get_queryset().values_list("issue_id", flat=True)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
CycleIssueSerializer(self.get_queryset(), many=True).data,
|
IssueSerializer(
|
||||||
|
Issue.objects.filter(pk__in=issues), many=True
|
||||||
|
).data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, cycle_id, pk):
|
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
cycle_issue = CycleIssue.objects.get(
|
cycle_issue = CycleIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
)
|
)
|
||||||
issue_id = cycle_issue.issue_id
|
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="cycle.activity.deleted",
|
type="cycle.activity.deleted",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(
|
||||||
@@ -712,7 +750,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(cycle_issue.issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
@@ -834,3 +872,273 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug, project_id, cycle_id):
|
||||||
|
cycle_properties = CycleUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
cycle_properties.filters = request.data.get(
|
||||||
|
"filters", cycle_properties.filters
|
||||||
|
)
|
||||||
|
cycle_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", cycle_properties.display_filters
|
||||||
|
)
|
||||||
|
cycle_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", cycle_properties.display_properties
|
||||||
|
)
|
||||||
|
cycle_properties.save()
|
||||||
|
|
||||||
|
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, cycle_id):
|
||||||
|
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
serializer = CycleUserPropertiesSerializer(cycle_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveCycleEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
]
|
||||||
|
def get(self, request, slug):
|
||||||
|
subquery = CycleFavorite.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
cycle_id=OuterRef("pk"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
)
|
||||||
|
active_cycles = (
|
||||||
|
Cycle.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
start_date__lte=timezone.now(),
|
||||||
|
end_date__gte=timezone.now(),
|
||||||
|
)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("owned_by")
|
||||||
|
.annotate(is_favorite=Exists(subquery))
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_cycle",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
cancelled_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="cancelled",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
unstarted_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="unstarted",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
backlog_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="backlog",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||||
|
.annotate(
|
||||||
|
completed_estimates=Sum(
|
||||||
|
"issue_cycle__issue__estimate_point",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_estimates=Sum(
|
||||||
|
"issue_cycle__issue__estimate_point",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
status=Case(
|
||||||
|
When(
|
||||||
|
Q(start_date__lte=timezone.now())
|
||||||
|
& Q(end_date__gte=timezone.now()),
|
||||||
|
then=Value("CURRENT"),
|
||||||
|
),
|
||||||
|
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
|
||||||
|
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||||
|
When(
|
||||||
|
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||||
|
then=Value("DRAFT"),
|
||||||
|
),
|
||||||
|
default=Value("DRAFT"),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_cycle__issue__assignees",
|
||||||
|
queryset=User.objects.only("avatar", "first_name", "id").distinct(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_cycle__issue__labels",
|
||||||
|
queryset=Label.objects.only("name", "color", "id").distinct(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("-created_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
cycles = CycleSerializer(active_cycles, many=True).data
|
||||||
|
|
||||||
|
for cycle in cycles:
|
||||||
|
assignee_distribution = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_cycle__cycle_id=cycle["id"],
|
||||||
|
project_id=cycle["project"],
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
|
.values("display_name", "assignee_id", "avatar")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"assignee_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("display_name")
|
||||||
|
)
|
||||||
|
|
||||||
|
label_distribution = (
|
||||||
|
Issue.objects.filter(
|
||||||
|
issue_cycle__cycle_id=cycle["id"],
|
||||||
|
project_id=cycle["project"],
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.annotate(label_name=F("labels__name"))
|
||||||
|
.annotate(color=F("labels__color"))
|
||||||
|
.annotate(label_id=F("labels__id"))
|
||||||
|
.values("label_name", "color", "label_id")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"label_id",
|
||||||
|
filter=Q(
|
||||||
|
completed_at__isnull=True,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("label_name")
|
||||||
|
)
|
||||||
|
cycle["distribution"] = {
|
||||||
|
"assignees": assignee_distribution,
|
||||||
|
"labels": label_distribution,
|
||||||
|
"completion_chart": {},
|
||||||
|
}
|
||||||
|
if cycle["start_date"] and cycle["end_date"]:
|
||||||
|
cycle["distribution"][
|
||||||
|
"completion_chart"
|
||||||
|
] = burndown_plot(
|
||||||
|
queryset=active_cycles.get(pk=cycle["id"]),
|
||||||
|
slug=slug,
|
||||||
|
project_id=cycle["project"],
|
||||||
|
cycle_id=cycle["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(cycles, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||||
if project.estimate_id is not None:
|
if project.estimate_id is not None:
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
estimate_id=project.estimate_id,
|
estimate_id=project.estimate_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
serializer = EstimatePointSerializer(estimate_points, many=True)
|
serializer = EstimatePointSerializer(estimate_points, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response([], status=status.HTTP_200_OK)
|
return Response([], status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class BulkEstimatePointEndpoint(BaseViewSet):
|
class BulkEstimatePointEndpoint(BaseViewSet):
|
||||||
@@ -39,9 +39,13 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
serializer_class = EstimateSerializer
|
serializer_class = EstimateSerializer
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
estimates = Estimate.objects.filter(
|
estimates = (
|
||||||
workspace__slug=slug, project_id=project_id
|
Estimate.objects.filter(
|
||||||
).prefetch_related("points").select_related("workspace", "project")
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
.prefetch_related("points")
|
||||||
|
.select_related("workspace", "project")
|
||||||
|
)
|
||||||
serializer = EstimateReadSerializer(estimates, many=True)
|
serializer = EstimateReadSerializer(estimates, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@@ -53,14 +57,18 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
estimate_points = request.data.get("estimate_points", [])
|
estimate_points = request.data.get("estimate_points", [])
|
||||||
|
|
||||||
serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True)
|
serializer = EstimatePointSerializer(
|
||||||
|
data=request.data.get("estimate_points"), many=True
|
||||||
|
)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_serializer = EstimateSerializer(data=request.data.get("estimate"))
|
estimate_serializer = EstimateSerializer(
|
||||||
|
data=request.data.get("estimate")
|
||||||
|
)
|
||||||
if not estimate_serializer.is_valid():
|
if not estimate_serializer.is_valid():
|
||||||
return Response(
|
return Response(
|
||||||
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
estimate_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
@@ -135,7 +143,8 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
|
|
||||||
estimate_points = EstimatePoint.objects.filter(
|
estimate_points = EstimatePoint.objects.filter(
|
||||||
pk__in=[
|
pk__in=[
|
||||||
estimate_point.get("id") for estimate_point in estimate_points_data
|
estimate_point.get("id")
|
||||||
|
for estimate_point in estimate_points_data
|
||||||
],
|
],
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -157,10 +166,14 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
|||||||
updated_estimate_points.append(estimate_point)
|
updated_estimate_points.append(estimate_point)
|
||||||
|
|
||||||
EstimatePoint.objects.bulk_update(
|
EstimatePoint.objects.bulk_update(
|
||||||
updated_estimate_points, ["value"], batch_size=10,
|
updated_estimate_points,
|
||||||
|
["value"],
|
||||||
|
batch_size=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_point_serializer = EstimatePointSerializer(estimate_points, many=True)
|
estimate_point_serializer = EstimatePointSerializer(
|
||||||
|
estimate_points, many=True
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"estimate": estimate_serializer.data,
|
"estimate": estimate_serializer.data,
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ class ExportIssuesEndpoint(BaseAPIView):
|
|||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
# Get the workspace
|
# Get the workspace
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
provider = request.data.get("provider", False)
|
provider = request.data.get("provider", False)
|
||||||
multiple = request.data.get("multiple", False)
|
multiple = request.data.get("multiple", False)
|
||||||
project_ids = request.data.get("project", [])
|
project_ids = request.data.get("project", [])
|
||||||
|
|
||||||
if provider in ["csv", "xlsx", "json"]:
|
if provider in ["csv", "xlsx", "json"]:
|
||||||
if not project_ids:
|
if not project_ids:
|
||||||
project_ids = Project.objects.filter(
|
project_ids = Project.objects.filter(
|
||||||
@@ -63,9 +63,11 @@ class ExportIssuesEndpoint(BaseAPIView):
|
|||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
exporter_history = ExporterHistory.objects.filter(
|
exporter_history = ExporterHistory.objects.filter(
|
||||||
workspace__slug=slug
|
workspace__slug=slug
|
||||||
).select_related("workspace","initiated_by")
|
).select_related("workspace", "initiated_by")
|
||||||
|
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=exporter_history,
|
queryset=exporter_history,
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ from django.conf import settings
|
|||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import Workspace, Project
|
from plane.db.models import Workspace, Project
|
||||||
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
|
from plane.app.serializers import (
|
||||||
|
ProjectLiteSerializer,
|
||||||
|
WorkspaceLiteSerializer,
|
||||||
|
)
|
||||||
from plane.utils.integrations.github import get_release_notes
|
from plane.utils.integrations.github import get_release_notes
|
||||||
from plane.license.utils.instance_value import get_configuration_value
|
from plane.license.utils.instance_value import get_configuration_value
|
||||||
|
|
||||||
@@ -51,7 +54,8 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if not task:
|
if not task:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Task is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
final_text = task + "\n" + prompt
|
final_text = task + "\n" + prompt
|
||||||
@@ -89,7 +93,7 @@ class ReleaseNotesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
class UnsplashEndpoint(BaseAPIView):
|
class UnsplashEndpoint(BaseAPIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
UNSPLASH_ACCESS_KEY, = get_configuration_value(
|
(UNSPLASH_ACCESS_KEY,) = get_configuration_value(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"key": "UNSPLASH_ACCESS_KEY",
|
"key": "UNSPLASH_ACCESS_KEY",
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ from plane.app.serializers import (
|
|||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.integrations.github import get_github_repo_details
|
from plane.utils.integrations.github import get_github_repo_details
|
||||||
from plane.utils.importers.jira import jira_project_issue_summary, is_allowed_hostname
|
from plane.utils.importers.jira import (
|
||||||
|
jira_project_issue_summary,
|
||||||
|
is_allowed_hostname,
|
||||||
|
)
|
||||||
from plane.bgtasks.importer_task import service_importer
|
from plane.bgtasks.importer_task import service_importer
|
||||||
from plane.utils.html_processor import strip_tags
|
from plane.utils.html_processor import strip_tags
|
||||||
from plane.app.permissions import WorkSpaceAdminPermission
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
@@ -93,7 +96,8 @@ class ServiceIssueImportSummaryEndpoint(BaseAPIView):
|
|||||||
for key, error_message in params.items():
|
for key, error_message in params.items():
|
||||||
if not request.GET.get(key, False):
|
if not request.GET.get(key, False):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
|
{"error": error_message},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
project_key = request.GET.get("project_key", "")
|
project_key = request.GET.get("project_key", "")
|
||||||
@@ -236,7 +240,9 @@ class ImportServiceEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def delete(self, request, slug, service, pk):
|
def delete(self, request, slug, service, pk):
|
||||||
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
|
importer = Importer.objects.get(
|
||||||
|
pk=pk, service=service, workspace__slug=slug
|
||||||
|
)
|
||||||
|
|
||||||
if importer.imported_data is not None:
|
if importer.imported_data is not None:
|
||||||
# Delete all imported Issues
|
# Delete all imported Issues
|
||||||
@@ -254,8 +260,12 @@ class ImportServiceEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def patch(self, request, slug, service, pk):
|
def patch(self, request, slug, service, pk):
|
||||||
importer = Importer.objects.get(pk=pk, service=service, workspace__slug=slug)
|
importer = Importer.objects.get(
|
||||||
serializer = ImporterSerializer(importer, data=request.data, partial=True)
|
pk=pk, service=service, workspace__slug=slug
|
||||||
|
)
|
||||||
|
serializer = ImporterSerializer(
|
||||||
|
importer, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -291,9 +301,9 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Get the maximum sequence_id
|
# Get the maximum sequence_id
|
||||||
last_id = IssueSequence.objects.filter(project_id=project_id).aggregate(
|
last_id = IssueSequence.objects.filter(
|
||||||
largest=Max("sequence")
|
project_id=project_id
|
||||||
)["largest"]
|
).aggregate(largest=Max("sequence"))["largest"]
|
||||||
|
|
||||||
last_id = 1 if last_id is None else last_id + 1
|
last_id = 1 if last_id is None else last_id + 1
|
||||||
|
|
||||||
@@ -326,7 +336,9 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
if issue_data.get("state", False)
|
if issue_data.get("state", False)
|
||||||
else default_state.id,
|
else default_state.id,
|
||||||
name=issue_data.get("name", "Issue Created through Bulk"),
|
name=issue_data.get("name", "Issue Created through Bulk"),
|
||||||
description_html=issue_data.get("description_html", "<p></p>"),
|
description_html=issue_data.get(
|
||||||
|
"description_html", "<p></p>"
|
||||||
|
),
|
||||||
description_stripped=(
|
description_stripped=(
|
||||||
None
|
None
|
||||||
if (
|
if (
|
||||||
@@ -438,15 +450,21 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
for comment in comments_list
|
for comment in comments_list
|
||||||
]
|
]
|
||||||
|
|
||||||
_ = IssueComment.objects.bulk_create(bulk_issue_comments, batch_size=100)
|
_ = IssueComment.objects.bulk_create(
|
||||||
|
bulk_issue_comments, batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
# Attach Links
|
# Attach Links
|
||||||
_ = IssueLink.objects.bulk_create(
|
_ = IssueLink.objects.bulk_create(
|
||||||
[
|
[
|
||||||
IssueLink(
|
IssueLink(
|
||||||
issue=issue,
|
issue=issue,
|
||||||
url=issue_data.get("link", {}).get("url", "https://github.com"),
|
url=issue_data.get("link", {}).get(
|
||||||
title=issue_data.get("link", {}).get("title", "Original Issue"),
|
"url", "https://github.com"
|
||||||
|
),
|
||||||
|
title=issue_data.get("link", {}).get(
|
||||||
|
"title", "Original Issue"
|
||||||
|
),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
@@ -483,14 +501,18 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
|||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
modules = Module.objects.filter(id__in=[module.id for module in modules])
|
modules = Module.objects.filter(
|
||||||
|
id__in=[module.id for module in modules]
|
||||||
|
)
|
||||||
|
|
||||||
if len(modules) == len(modules_data):
|
if len(modules) == len(modules_data):
|
||||||
_ = ModuleLink.objects.bulk_create(
|
_ = ModuleLink.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ModuleLink(
|
ModuleLink(
|
||||||
module=module,
|
module=module,
|
||||||
url=module_data.get("link", {}).get("url", "https://plane.so"),
|
url=module_data.get("link", {}).get(
|
||||||
|
"url", "https://plane.so"
|
||||||
|
),
|
||||||
title=module_data.get("link", {}).get(
|
title=module_data.get("link", {}).get(
|
||||||
"title", "Original Issue"
|
"title", "Original Issue"
|
||||||
),
|
),
|
||||||
@@ -529,6 +551,8 @@ class BulkImportModulesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Modules created but issues could not be imported"},
|
{
|
||||||
|
"message": "Modules created but issues could not be imported"
|
||||||
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ class InboxViewSet(BaseViewSet):
|
|||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
inbox = Inbox.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
inbox = Inbox.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
# Handle default inbox delete
|
# Handle default inbox delete
|
||||||
if inbox.is_default:
|
if inbox.is_default:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -90,7 +92,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(
|
.filter(
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
inbox_id=self.kwargs.get("inbox_id"),
|
inbox_id=self.kwargs.get("inbox_id"),
|
||||||
@@ -107,12 +110,13 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(bridge_id=F("issue_inbox__id"))
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels")
|
.prefetch_related("assignees", "labels")
|
||||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -124,7 +128,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -147,7 +153,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# Check for valid priority
|
||||||
@@ -159,7 +166,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
"none",
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create or get state
|
# Create or get state
|
||||||
@@ -204,9 +212,12 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
serializer = IssueStateInboxSerializer(issue)
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
inbox_id=inbox_id,
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
@@ -229,7 +240,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if bool(issue_data):
|
if bool(issue_data):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=inbox_issue.issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
# Only allow guests and viewers to edit name and description
|
# Only allow guests and viewers to edit name and description
|
||||||
if project_member.role <= 10:
|
if project_member.role <= 10:
|
||||||
@@ -239,7 +252,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
"description_html": issue_data.get(
|
"description_html": issue_data.get(
|
||||||
"description_html", issue.description_html
|
"description_html", issue.description_html
|
||||||
),
|
),
|
||||||
"description": issue_data.get("description", issue.description),
|
"description": issue_data.get(
|
||||||
|
"description", issue.description
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(
|
||||||
@@ -285,7 +300,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
group="cancelled",
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
@@ -303,32 +320,37 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if issue.state.name == "Triage":
|
if issue.state.name == "Triage":
|
||||||
# Move to default state
|
# Move to default state
|
||||||
state = State.objects.filter(
|
state = State.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, default=True
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
default=True,
|
||||||
).first()
|
).first()
|
||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
InboxIssueSerializer(inbox_issue).data,
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
|
||||||
)
|
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
serializer = IssueStateInboxSerializer(issue)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
inbox_id=inbox_id,
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
@@ -350,9 +372,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
# Delete the issue also
|
# Delete the issue also
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
|
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Python improts
|
# Python improts
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
@@ -19,7 +20,10 @@ from plane.db.models import (
|
|||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
APIToken,
|
APIToken,
|
||||||
)
|
)
|
||||||
from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
|
from plane.app.serializers import (
|
||||||
|
IntegrationSerializer,
|
||||||
|
WorkspaceIntegrationSerializer,
|
||||||
|
)
|
||||||
from plane.utils.integrations.github import (
|
from plane.utils.integrations.github import (
|
||||||
get_github_metadata,
|
get_github_metadata,
|
||||||
delete_github_installation,
|
delete_github_installation,
|
||||||
@@ -27,6 +31,7 @@ from plane.utils.integrations.github import (
|
|||||||
from plane.app.permissions import WorkSpaceAdminPermission
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
from plane.utils.integrations.slack import slack_oauth
|
from plane.utils.integrations.slack import slack_oauth
|
||||||
|
|
||||||
|
|
||||||
class IntegrationViewSet(BaseViewSet):
|
class IntegrationViewSet(BaseViewSet):
|
||||||
serializer_class = IntegrationSerializer
|
serializer_class = IntegrationSerializer
|
||||||
model = Integration
|
model = Integration
|
||||||
@@ -101,7 +106,10 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
|||||||
code = request.data.get("code", False)
|
code = request.data.get("code", False)
|
||||||
|
|
||||||
if not code:
|
if not code:
|
||||||
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Code is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
slack_response = slack_oauth(code=code)
|
slack_response = slack_oauth(code=code)
|
||||||
|
|
||||||
@@ -110,7 +118,9 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
|
|||||||
team_id = metadata.get("team", {}).get("id", False)
|
team_id = metadata.get("team", {}).get("id", False)
|
||||||
if not metadata or not access_token or not team_id:
|
if not metadata or not access_token or not team_id:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Slack could not be installed. Please try again later"},
|
{
|
||||||
|
"error": "Slack could not be installed. Please try again later"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
config = {"team_id": team_id, "access_token": access_token}
|
config = {"team_id": team_id, "access_token": access_token}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ from plane.app.serializers import (
|
|||||||
GithubCommentSyncSerializer,
|
GithubCommentSyncSerializer,
|
||||||
)
|
)
|
||||||
from plane.utils.integrations.github import get_github_repos
|
from plane.utils.integrations.github import get_github_repos
|
||||||
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GithubRepositoriesEndpoint(BaseAPIView):
|
class GithubRepositoriesEndpoint(BaseAPIView):
|
||||||
@@ -185,11 +188,10 @@ class BulkCreateGithubIssueSyncEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class GithubCommentSyncViewSet(BaseViewSet):
|
class GithubCommentSyncViewSet(BaseViewSet):
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer_class = GithubCommentSyncSerializer
|
serializer_class = GithubCommentSyncSerializer
|
||||||
model = GithubCommentSync
|
model = GithubCommentSync
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.views import BaseViewSet, BaseAPIView
|
from plane.app.views import BaseViewSet, BaseAPIView
|
||||||
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
|
from plane.db.models import (
|
||||||
|
SlackProjectSync,
|
||||||
|
WorkspaceIntegration,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
from plane.app.serializers import SlackProjectSyncSerializer
|
from plane.app.serializers import SlackProjectSyncSerializer
|
||||||
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
)
|
||||||
from plane.utils.integrations.slack import slack_oauth
|
from plane.utils.integrations.slack import slack_oauth
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +45,8 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if not code:
|
if not code:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Code is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
slack_response = slack_oauth(code=code)
|
slack_response = slack_oauth(code=code)
|
||||||
@@ -54,7 +62,9 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
access_token=slack_response.get("access_token"),
|
access_token=slack_response.get("access_token"),
|
||||||
scopes=slack_response.get("scope"),
|
scopes=slack_response.get("scope"),
|
||||||
bot_user_id=slack_response.get("bot_user_id"),
|
bot_user_id=slack_response.get("bot_user_id"),
|
||||||
webhook_url=slack_response.get("incoming_webhook", {}).get("url"),
|
webhook_url=slack_response.get("incoming_webhook", {}).get(
|
||||||
|
"url"
|
||||||
|
),
|
||||||
data=slack_response,
|
data=slack_response,
|
||||||
team_id=slack_response.get("team", {}).get("id"),
|
team_id=slack_response.get("team", {}).get("id"),
|
||||||
team_name=slack_response.get("team", {}).get("name"),
|
team_name=slack_response.get("team", {}).get("name"),
|
||||||
@@ -62,7 +72,9 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
_ = ProjectMember.objects.get_or_create(
|
_ = ProjectMember.objects.get_or_create(
|
||||||
member=workspace_integration.actor, role=20, project_id=project_id
|
member=workspace_integration.actor,
|
||||||
|
role=20,
|
||||||
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
serializer = SlackProjectSyncSerializer(slack_project_sync)
|
serializer = SlackProjectSyncSerializer(slack_project_sync)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -74,6 +86,8 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Slack could not be installed. Please try again later"},
|
{
|
||||||
|
"error": "Slack could not be installed. Please try again later"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|||||||
+343
-137
@@ -34,11 +34,11 @@ from rest_framework.parsers import MultiPartParser, FormParser
|
|||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView, WebhookMixin
|
from . import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueCreateSerializer,
|
|
||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
IssuePropertySerializer,
|
IssuePropertySerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
|
IssueCreateSerializer,
|
||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
IssueFlatSerializer,
|
IssueFlatSerializer,
|
||||||
IssueLinkSerializer,
|
IssueLinkSerializer,
|
||||||
@@ -52,6 +52,7 @@ from plane.app.serializers import (
|
|||||||
IssueRelationSerializer,
|
IssueRelationSerializer,
|
||||||
RelatedIssueSerializer,
|
RelatedIssueSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
|
IssueRelationLiteSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@@ -81,7 +82,7 @@ from plane.db.models import (
|
|||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
class IssueViewSet(WebhookMixin, BaseViewSet):
|
class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
@@ -109,13 +110,9 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.filter(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
project_id=self.kwargs.get("project_id")
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
)
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
@@ -129,22 +126,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
queryset=IssueReaction.objects.select_related("actor"),
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).distinct()
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
|
||||||
def list(self, request, slug, project_id):
|
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
|
||||||
|
|
||||||
issue_queryset = (
|
|
||||||
self.get_queryset()
|
|
||||||
.filter(**filters)
|
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
@@ -154,17 +135,47 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
)
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -212,14 +223,17 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
issues = IssueSerializer(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
issue_queryset, many=True, fields=self.fields, expand=self.expand
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
).data
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
@@ -239,32 +253,42 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
# Track the issue
|
# Track the issue
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
|
issue = (
|
||||||
|
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||||
|
)
|
||||||
|
serializer = IssueSerializer(issue)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.issue_objects.annotate(
|
issue = self.get_queryset().filter(pk=pk).first()
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
return Response(
|
||||||
.order_by()
|
IssueSerializer(
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
issue, fields=self.fields, expand=self.expand
|
||||||
.values("count")
|
).data,
|
||||||
).get(workspace__slug=slug, project_id=project_id, pk=pk)
|
status=status.HTTP_200_OK,
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk=None):
|
def partial_update(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
|
serializer = IssueCreateSerializer(
|
||||||
|
issue, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -276,11 +300,16 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
current_instance=current_instance,
|
current_instance=current_instance,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
issue = self.get_queryset().filter(pk=pk).first()
|
||||||
|
return Response(
|
||||||
|
IssueSerializer(issue).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk=None):
|
def destroy(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
@@ -303,7 +332,13 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@@ -317,7 +352,9 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -336,7 +373,9 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -353,7 +392,9 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -401,7 +442,9 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
@@ -470,7 +513,9 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
|
issue_activities = IssueActivitySerializer(
|
||||||
|
issue_activities, many=True
|
||||||
|
).data
|
||||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||||
|
|
||||||
result_list = sorted(
|
result_list = sorted(
|
||||||
@@ -528,7 +573,9 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="comment.activity.created",
|
type="comment.activity.created",
|
||||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
serializer.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@@ -540,7 +587,10 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, issue_id, pk):
|
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
@@ -566,7 +616,10 @@ class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_comment = IssueComment.objects.get(
|
issue_comment = IssueComment.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueCommentSerializer(issue_comment).data,
|
IssueCommentSerializer(issue_comment).data,
|
||||||
@@ -590,16 +643,21 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
|||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def patch(self, request, slug, project_id):
|
||||||
issue_property, created = IssueProperty.objects.get_or_create(
|
issue_property = IssueProperty.objects.get(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not created:
|
issue_property.filters = request.data.get(
|
||||||
issue_property.properties = request.data.get("properties", {})
|
"filters", issue_property.filters
|
||||||
issue_property.save()
|
)
|
||||||
issue_property.properties = request.data.get("properties", {})
|
issue_property.display_filters = request.data.get(
|
||||||
|
"display_filters", issue_property.display_filters
|
||||||
|
)
|
||||||
|
issue_property.display_properties = request.data.get(
|
||||||
|
"display_properties", issue_property.display_properties
|
||||||
|
)
|
||||||
issue_property.save()
|
issue_property.save()
|
||||||
serializer = IssuePropertySerializer(issue_property)
|
serializer = IssuePropertySerializer(issue_property)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@@ -624,11 +682,17 @@ class LabelViewSet(BaseViewSet):
|
|||||||
serializer = LabelSerializer(data=request.data)
|
serializer = LabelSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(project_id=project_id)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Label with the same name already exists in the project"},
|
{
|
||||||
|
"error": "Label with the same name already exists in the project"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -683,7 +747,9 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def get(self, request, slug, project_id, issue_id):
|
def get(self, request, slug, project_id, issue_id):
|
||||||
sub_issues = (
|
sub_issues = (
|
||||||
Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug)
|
Issue.issue_objects.filter(
|
||||||
|
parent_id=issue_id, workspace__slug=slug
|
||||||
|
)
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("state")
|
.select_related("state")
|
||||||
@@ -691,7 +757,9 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
.prefetch_related("assignees")
|
.prefetch_related("assignees")
|
||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -703,7 +771,9 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -714,21 +784,15 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
queryset=IssueReaction.objects.select_related("actor"),
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(state_group=F("state__group"))
|
||||||
)
|
)
|
||||||
|
|
||||||
state_distribution = (
|
# create's a dict with state group name with their respective issue id's
|
||||||
State.objects.filter(workspace__slug=slug, state_issue__parent_id=issue_id)
|
result = defaultdict(list)
|
||||||
.annotate(state_group=F("group"))
|
for sub_issue in sub_issues:
|
||||||
.values("state_group")
|
result[sub_issue.state_group].append(str(sub_issue.id))
|
||||||
.annotate(state_count=Count("state_group"))
|
|
||||||
.order_by("state_group")
|
|
||||||
)
|
|
||||||
|
|
||||||
result = {
|
serializer = IssueSerializer(
|
||||||
item["state_group"]: item["state_count"] for item in state_distribution
|
|
||||||
}
|
|
||||||
|
|
||||||
serializer = IssueLiteSerializer(
|
|
||||||
sub_issues,
|
sub_issues,
|
||||||
many=True,
|
many=True,
|
||||||
)
|
)
|
||||||
@@ -758,7 +822,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
|
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
|
||||||
|
|
||||||
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
|
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group"))
|
||||||
|
|
||||||
# Track the issue
|
# Track the issue
|
||||||
_ = [
|
_ = [
|
||||||
@@ -773,11 +837,24 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
for sub_issue_id in sub_issue_ids
|
for sub_issue_id in sub_issue_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# create's a dict with state group name with their respective issue id's
|
||||||
|
result = defaultdict(list)
|
||||||
|
for sub_issue in updated_sub_issues:
|
||||||
|
result[sub_issue.state_group].append(str(sub_issue.id))
|
||||||
|
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
updated_sub_issues,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
IssueFlatSerializer(updated_sub_issues, many=True).data,
|
{
|
||||||
|
"sub_issues": serializer.data,
|
||||||
|
"state_distribution": result,
|
||||||
|
},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IssueLinkViewSet(BaseViewSet):
|
class IssueLinkViewSet(BaseViewSet):
|
||||||
@@ -809,7 +886,9 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="link.activity.created",
|
type="link.activity.created",
|
||||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
serializer.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(self.request.user.id),
|
actor_id=str(self.request.user.id),
|
||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
@@ -821,14 +900,19 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, issue_id, pk):
|
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
)
|
)
|
||||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
serializer = IssueLinkSerializer(
|
||||||
|
issue_link, data=request.data, partial=True
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -845,7 +929,10 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_link = IssueLink.objects.get(
|
issue_link = IssueLink.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
)
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueLinkSerializer(issue_link).data,
|
IssueLinkSerializer(issue_link).data,
|
||||||
@@ -971,13 +1058,23 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@@ -993,7 +1090,9 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -1003,7 +1102,9 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -1051,7 +1152,9 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
@@ -1062,9 +1165,10 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
else issue_queryset.filter(parent__isnull=True)
|
else issue_queryset.filter(parent__isnull=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
issues = IssueSerializer(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
).data
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
@@ -1138,24 +1242,11 @@ class IssueSubscriberViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug, project_id, issue_id):
|
def list(self, request, slug, project_id, issue_id):
|
||||||
members = (
|
members = ProjectMember.objects.filter(
|
||||||
ProjectMember.objects.filter(
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
project_id=project_id,
|
||||||
project_id=project_id,
|
is_active=True,
|
||||||
is_active=True,
|
).select_related("member")
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
is_subscribed=Exists(
|
|
||||||
IssueSubscriber.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
subscriber=OuterRef("member"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.select_related("member")
|
|
||||||
)
|
|
||||||
serializer = ProjectMemberLiteSerializer(members, many=True)
|
serializer = ProjectMemberLiteSerializer(members, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@@ -1210,7 +1301,9 @@ class IssueSubscriberViewSet(BaseViewSet):
|
|||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project=project_id,
|
project=project_id,
|
||||||
).exists()
|
).exists()
|
||||||
return Response({"subscribed": issue_subscriber}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"subscribed": issue_subscriber}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IssueReactionViewSet(BaseViewSet):
|
class IssueReactionViewSet(BaseViewSet):
|
||||||
@@ -1365,23 +1458,95 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id, issue_id):
|
||||||
|
issue_relations = (
|
||||||
|
IssueRelation.objects.filter(
|
||||||
|
Q(issue_id=issue_id) | Q(related_issue=issue_id)
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("issue")
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
blocking_issues = issue_relations.filter(
|
||||||
|
relation_type="blocked_by", related_issue_id=issue_id
|
||||||
|
)
|
||||||
|
blocked_by_issues = issue_relations.filter(
|
||||||
|
relation_type="blocked_by", issue_id=issue_id
|
||||||
|
)
|
||||||
|
duplicate_issues = issue_relations.filter(
|
||||||
|
issue_id=issue_id, relation_type="duplicate"
|
||||||
|
)
|
||||||
|
duplicate_issues_related = issue_relations.filter(
|
||||||
|
related_issue_id=issue_id, relation_type="duplicate"
|
||||||
|
)
|
||||||
|
relates_to_issues = issue_relations.filter(
|
||||||
|
issue_id=issue_id, relation_type="relates_to"
|
||||||
|
)
|
||||||
|
relates_to_issues_related = issue_relations.filter(
|
||||||
|
related_issue_id=issue_id, relation_type="relates_to"
|
||||||
|
)
|
||||||
|
|
||||||
|
blocked_by_issues_serialized = IssueRelationSerializer(
|
||||||
|
blocked_by_issues, many=True
|
||||||
|
).data
|
||||||
|
duplicate_issues_serialized = IssueRelationSerializer(
|
||||||
|
duplicate_issues, many=True
|
||||||
|
).data
|
||||||
|
relates_to_issues_serialized = IssueRelationSerializer(
|
||||||
|
relates_to_issues, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
# revere relation for blocked by issues
|
||||||
|
blocking_issues_serialized = RelatedIssueSerializer(
|
||||||
|
blocking_issues, many=True
|
||||||
|
).data
|
||||||
|
# reverse relation for duplicate issues
|
||||||
|
duplicate_issues_related_serialized = RelatedIssueSerializer(
|
||||||
|
duplicate_issues_related, many=True
|
||||||
|
).data
|
||||||
|
# reverse relation for related issues
|
||||||
|
relates_to_issues_related_serialized = RelatedIssueSerializer(
|
||||||
|
relates_to_issues_related, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
"blocking": blocking_issues_serialized,
|
||||||
|
"blocked_by": blocked_by_issues_serialized,
|
||||||
|
"duplicate": duplicate_issues_serialized
|
||||||
|
+ duplicate_issues_related_serialized,
|
||||||
|
"relates_to": relates_to_issues_serialized
|
||||||
|
+ relates_to_issues_related_serialized,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, issue_id):
|
def create(self, request, slug, project_id, issue_id):
|
||||||
related_list = request.data.get("related_list", [])
|
relation_type = request.data.get("relation_type", None)
|
||||||
relation = request.data.get("relation", None)
|
issues = request.data.get("issues", [])
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
issue_relation = IssueRelation.objects.bulk_create(
|
issue_relation = IssueRelation.objects.bulk_create(
|
||||||
[
|
[
|
||||||
IssueRelation(
|
IssueRelation(
|
||||||
issue_id=related_issue["issue"],
|
issue_id=issue
|
||||||
related_issue_id=related_issue["related_issue"],
|
if relation_type == "blocking"
|
||||||
relation_type=related_issue["relation_type"],
|
else issue_id,
|
||||||
|
related_issue_id=issue_id
|
||||||
|
if relation_type == "blocking"
|
||||||
|
else issue,
|
||||||
|
relation_type="blocked_by"
|
||||||
|
if relation_type == "blocking"
|
||||||
|
else relation_type,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
updated_by=request.user,
|
updated_by=request.user,
|
||||||
)
|
)
|
||||||
for related_issue in related_list
|
for issue in issues
|
||||||
],
|
],
|
||||||
batch_size=10,
|
batch_size=10,
|
||||||
ignore_conflicts=True,
|
ignore_conflicts=True,
|
||||||
@@ -1397,7 +1562,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if relation == "blocking":
|
if relation_type == "blocking":
|
||||||
return Response(
|
return Response(
|
||||||
RelatedIssueSerializer(issue_relation, many=True).data,
|
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
@@ -1408,10 +1573,24 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
def remove_relation(self, request, slug, project_id, issue_id):
|
||||||
issue_relation = IssueRelation.objects.get(
|
relation_type = request.data.get("relation_type", None)
|
||||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
related_issue = request.data.get("related_issue", None)
|
||||||
)
|
|
||||||
|
if relation_type == "blocking":
|
||||||
|
issue_relation = IssueRelation.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=related_issue,
|
||||||
|
related_issue_id=issue_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_relation = IssueRelation.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
related_issue_id=related_issue,
|
||||||
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueRelationSerializer(issue_relation).data,
|
IssueRelationSerializer(issue_relation).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
@@ -1419,7 +1598,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
issue_relation.delete()
|
issue_relation.delete()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue_relation.activity.deleted",
|
type="issue_relation.activity.deleted",
|
||||||
requested_data=json.dumps({"related_list": None}),
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
@@ -1439,7 +1618,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.objects.annotate(
|
Issue.objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -1464,11 +1645,21 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@@ -1484,7 +1675,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -1494,7 +1687,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -1542,14 +1737,17 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
issues = IssueSerializer(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
).data
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
@@ -1569,7 +1767,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
# Track the issue
|
# Track the issue
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue_draft.activity.created",
|
type="issue_draft.activity.created",
|
||||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
requested_data=json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
@@ -1580,14 +1780,18 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
serializer = IssueSerializer(issue, data=request.data, partial=True)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if request.data.get("is_draft") is not None and not request.data.get(
|
if request.data.get(
|
||||||
"is_draft"
|
"is_draft"
|
||||||
):
|
) is not None and not request.data.get("is_draft"):
|
||||||
serializer.save(created_at=timezone.now(), updated_at=timezone.now())
|
serializer.save(
|
||||||
|
created_at=timezone.now(), updated_at=timezone.now()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
serializer.save()
|
serializer.save()
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
@@ -1612,7 +1816,9 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk=None):
|
def destroy(self, request, slug, project_id, pk=None):
|
||||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
current_instance = json.dumps(
|
current_instance = json.dumps(
|
||||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
)
|
)
|
||||||
@@ -1626,4 +1832,4 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
current_instance=current_instance,
|
current_instance=current_instance,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ from plane.app.serializers import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
IssueStateSerializer,
|
IssueSerializer,
|
||||||
|
ModuleUserPropertiesSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Module,
|
Module,
|
||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
@@ -32,6 +36,8 @@ from plane.db.models import (
|
|||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
@@ -54,7 +60,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
||||||
subquery = ModuleFavorite.objects.filter(
|
subquery = ModuleFavorite.objects.filter(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
module_id=OuterRef("pk"),
|
module_id=OuterRef("pk"),
|
||||||
@@ -74,7 +79,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"link_module",
|
"link_module",
|
||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
queryset=ModuleLink.objects.select_related(
|
||||||
|
"module", "created_by"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@@ -136,7 +143,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-is_favorite","-created_at")
|
.order_by("-is_favorite", "-created_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
@@ -153,6 +160,18 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
modules = ModuleSerializer(
|
||||||
|
queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
return Response(modules, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk):
|
def retrieve(self, request, slug, project_id, pk):
|
||||||
queryset = self.get_queryset().get(pk=pk)
|
queryset = self.get_queryset().get(pk=pk)
|
||||||
|
|
||||||
@@ -167,7 +186,13 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"assignee_id",
|
||||||
|
"avatar",
|
||||||
|
"display_name",
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_issues=Count(
|
total_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
@@ -251,7 +276,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
if queryset.start_date and queryset.target_date:
|
if queryset.start_date and queryset.target_date:
|
||||||
data["distribution"]["completion_chart"] = burndown_plot(
|
data["distribution"]["completion_chart"] = burndown_plot(
|
||||||
queryset=queryset, slug=slug, project_id=project_id, module_id=pk
|
queryset=queryset,
|
||||||
|
slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@@ -260,9 +288,13 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
module = Module.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
module_issues = list(
|
module_issues = list(
|
||||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
ModuleIssue.objects.filter(module_id=pk).values_list(
|
||||||
|
"issue", flat=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
@@ -289,7 +321,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
webhook_event = "module_issue"
|
webhook_event = "module_issue"
|
||||||
bulk = True
|
bulk = True
|
||||||
|
|
||||||
|
|
||||||
filterset_fields = [
|
filterset_fields = [
|
||||||
"issue__labels__id",
|
"issue__labels__id",
|
||||||
"issue__assignees__id",
|
"issue__assignees__id",
|
||||||
@@ -304,7 +335,9 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -324,18 +357,23 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id, module_id):
|
def list(self, request, slug, project_id, module_id):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
Issue.issue_objects.filter(issue_module__module_id=module_id)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(bridge_id=F("issue_module__id"))
|
|
||||||
.filter(project_id=project_id)
|
.filter(project_id=project_id)
|
||||||
.filter(workspace__slug=slug)
|
.filter(workspace__slug=slug)
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
@@ -346,6 +384,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
.order_by(order_by)
|
.order_by(order_by)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@@ -353,21 +393,32 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
|
serializer = IssueSerializer(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
issues, many=True, fields=fields if fields else None
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, module_id):
|
def create(self, request, slug, project_id, module_id):
|
||||||
issues = request.data.get("issues", [])
|
issues = request.data.get("issues", [])
|
||||||
if not len(issues):
|
if not len(issues):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||||
@@ -439,25 +490,32 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
issues = self.get_queryset().values_list("issue_id", flat=True)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
ModuleIssueSerializer(self.get_queryset(), many=True).data,
|
IssueSerializer(
|
||||||
|
Issue.objects.filter(pk__in=issues), many=True
|
||||||
|
).data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, module_id, pk):
|
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||||
module_issue = ModuleIssue.objects.get(
|
module_issue = ModuleIssue.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=module_id,
|
||||||
|
issue_id=issue_id,
|
||||||
)
|
)
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="module.activity.deleted",
|
type="module.activity.deleted",
|
||||||
requested_data=json.dumps(
|
requested_data=json.dumps(
|
||||||
{
|
{
|
||||||
"module_id": str(module_id),
|
"module_id": str(module_id),
|
||||||
"issues": [str(module_issue.issue_id)],
|
"issues": [str(issue_id)],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(module_issue.issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
@@ -521,4 +579,42 @@ class ModuleFavoriteViewSet(BaseViewSet):
|
|||||||
module_id=module_id,
|
module_id=module_id,
|
||||||
)
|
)
|
||||||
module_favorite.delete()
|
module_favorite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug, project_id, module_id):
|
||||||
|
module_properties = ModuleUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
module_id=module_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
module_properties.filters = request.data.get(
|
||||||
|
"filters", module_properties.filters
|
||||||
|
)
|
||||||
|
module_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", module_properties.display_filters
|
||||||
|
)
|
||||||
|
module_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", module_properties.display_properties
|
||||||
|
)
|
||||||
|
module_properties.save()
|
||||||
|
|
||||||
|
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, module_id):
|
||||||
|
module_properties, _ = ModuleUserProperties.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=module_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
serializer = ModuleUserPropertiesSerializer(module_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
|
|
||||||
# Filters based on query parameters
|
# Filters based on query parameters
|
||||||
snoozed_filters = {
|
snoozed_filters = {
|
||||||
"true": Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False),
|
"true": Q(snoozed_till__lt=timezone.now())
|
||||||
"false": Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
| Q(snoozed_till__isnull=False),
|
||||||
|
"false": Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications = notifications.filter(snoozed_filters[snoozed])
|
notifications = notifications.filter(snoozed_filters[snoozed])
|
||||||
@@ -72,14 +74,18 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
issue_ids = IssueSubscriber.objects.filter(
|
issue_ids = IssueSubscriber.objects.filter(
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Assigned Issues
|
# Assigned Issues
|
||||||
if type == "assigned":
|
if type == "assigned":
|
||||||
issue_ids = IssueAssignee.objects.filter(
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
@@ -94,10 +100,14 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
issue_ids = Issue.objects.filter(
|
issue_ids = Issue.objects.filter(
|
||||||
workspace__slug=slug, created_by=request.user
|
workspace__slug=slug, created_by=request.user
|
||||||
).values_list("pk", flat=True)
|
).values_list("pk", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(notifications),
|
queryset=(notifications),
|
||||||
@@ -227,11 +237,13 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
# Filter for snoozed notifications
|
# Filter for snoozed notifications
|
||||||
if snoozed:
|
if snoozed:
|
||||||
notifications = notifications.filter(
|
notifications = notifications.filter(
|
||||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
Q(snoozed_till__lt=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=False)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
notifications = notifications.filter(
|
notifications = notifications.filter(
|
||||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
Q(snoozed_till__gte=timezone.now())
|
||||||
|
| Q(snoozed_till__isnull=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter for archived or unarchive
|
# Filter for archived or unarchive
|
||||||
@@ -245,14 +257,18 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
issue_ids = IssueSubscriber.objects.filter(
|
issue_ids = IssueSubscriber.objects.filter(
|
||||||
workspace__slug=slug, subscriber_id=request.user.id
|
workspace__slug=slug, subscriber_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Assigned Issues
|
# Assigned Issues
|
||||||
if type == "assigned":
|
if type == "assigned":
|
||||||
issue_ids = IssueAssignee.objects.filter(
|
issue_ids = IssueAssignee.objects.filter(
|
||||||
workspace__slug=slug, assignee_id=request.user.id
|
workspace__slug=slug, assignee_id=request.user.id
|
||||||
).values_list("issue_id", flat=True)
|
).values_list("issue_id", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
@@ -267,7 +283,9 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
issue_ids = Issue.objects.filter(
|
issue_ids = Issue.objects.filter(
|
||||||
workspace__slug=slug, created_by=request.user
|
workspace__slug=slug, created_by=request.user
|
||||||
).values_list("pk", flat=True)
|
).values_list("pk", flat=True)
|
||||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
notifications = notifications.filter(
|
||||||
|
entity_identifier__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
updated_notifications = []
|
updated_notifications = []
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ class PageViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
try:
|
try:
|
||||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
if page.is_locked:
|
if page.is_locked:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -127,7 +129,9 @@ class PageViewSet(BaseViewSet):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except Page.DoesNotExist:
|
except Page.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -157,17 +161,21 @@ class PageViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||||
return Response(
|
pages = PageSerializer(queryset, many=True).data
|
||||||
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
|
return Response(pages, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
|
|
||||||
def archive(self, request, slug, project_id, page_id):
|
def archive(self, request, slug, project_id, page_id):
|
||||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=page_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
# only the owner and admin can archive the page
|
# only the owner and admin can archive the page
|
||||||
if (
|
if (
|
||||||
ProjectMember.objects.filter(
|
ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user, is_active=True, role__gt=20
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
role__gt=20,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
or request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
@@ -181,12 +189,17 @@ class PageViewSet(BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def unarchive(self, request, slug, project_id, page_id):
|
def unarchive(self, request, slug, project_id, page_id):
|
||||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=page_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
# only the owner and admin can un archive the page
|
# only the owner and admin can un archive the page
|
||||||
if (
|
if (
|
||||||
ProjectMember.objects.filter(
|
ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user, is_active=True, role__gt=20
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
role__gt=20,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
or request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
@@ -210,17 +223,21 @@ class PageViewSet(BaseViewSet):
|
|||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
).filter(archived_at__isnull=False)
|
).filter(archived_at__isnull=False)
|
||||||
|
|
||||||
return Response(
|
pages = PageSerializer(pages, many=True).data
|
||||||
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
return Response(pages, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
page = Page.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
# only the owner and admin can delete the page
|
# only the owner and admin can delete the page
|
||||||
if (
|
if (
|
||||||
ProjectMember.objects.filter(
|
ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user, is_active=True, role__gt=20
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
role__gt=20,
|
||||||
).exists()
|
).exists()
|
||||||
or request.user.id != page.owned_by_id
|
or request.user.id != page.owned_by_id
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from plane.app.serializers import (
|
|||||||
ProjectFavoriteSerializer,
|
ProjectFavoriteSerializer,
|
||||||
ProjectDeployBoardSerializer,
|
ProjectDeployBoardSerializer,
|
||||||
ProjectMemberAdminSerializer,
|
ProjectMemberAdminSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
@@ -85,9 +86,15 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
|
.filter(
|
||||||
|
Q(project_projectmember__member=self.request.user)
|
||||||
|
| Q(network=2)
|
||||||
|
)
|
||||||
.select_related(
|
.select_related(
|
||||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
"workspace",
|
||||||
|
"workspace__owner",
|
||||||
|
"default_assignee",
|
||||||
|
"project_lead",
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_favorite=Exists(
|
is_favorite=Exists(
|
||||||
@@ -159,7 +166,11 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
sort_order_query = ProjectMember.objects.filter(
|
sort_order_query = ProjectMember.objects.filter(
|
||||||
member=request.user,
|
member=request.user,
|
||||||
@@ -172,7 +183,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.annotate(sort_order=Subquery(sort_order_query))
|
.annotate(sort_order=Subquery(sort_order_query))
|
||||||
.order_by("sort_order", "name")
|
.order_by("sort_order", "name")
|
||||||
)
|
)
|
||||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
queryset=(projects),
|
queryset=(projects),
|
||||||
@@ -180,12 +193,10 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
projects, many=True
|
projects, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
projects = ProjectListSerializer(
|
||||||
return Response(
|
projects, many=True, fields=fields if fields else None
|
||||||
ProjectListSerializer(
|
).data
|
||||||
projects, many=True, fields=fields if fields else None
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
).data
|
|
||||||
)
|
|
||||||
|
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
try:
|
try:
|
||||||
@@ -199,7 +210,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
# Add the user as Administrator to the project
|
# Add the user as Administrator to the project
|
||||||
project_member = ProjectMember.objects.create(
|
project_member = ProjectMember.objects.create(
|
||||||
project_id=serializer.data["id"], member=request.user, role=20
|
project_id=serializer.data["id"],
|
||||||
|
member=request.user,
|
||||||
|
role=20,
|
||||||
)
|
)
|
||||||
# Also create the issue property for the user
|
# Also create the issue property for the user
|
||||||
_ = IssueProperty.objects.create(
|
_ = IssueProperty.objects.create(
|
||||||
@@ -272,9 +285,15 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -287,7 +306,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
except Workspace.DoesNotExist as e:
|
except Workspace.DoesNotExist as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Workspace does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -312,7 +332,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
if serializer.data["inbox_view"]:
|
if serializer.data["inbox_view"]:
|
||||||
Inbox.objects.get_or_create(
|
Inbox.objects.get_or_create(
|
||||||
name=f"{project.name} Inbox", project=project, is_default=True
|
name=f"{project.name} Inbox",
|
||||||
|
project=project,
|
||||||
|
is_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the triage state in Backlog group
|
# Create the triage state in Backlog group
|
||||||
@@ -324,10 +346,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
color="#ff7700",
|
color="#ff7700",
|
||||||
)
|
)
|
||||||
|
|
||||||
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
@@ -337,7 +365,8 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Project does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -372,11 +401,14 @@ class ProjectInvitationsViewset(BaseViewSet):
|
|||||||
# Check if email is provided
|
# Check if email is provided
|
||||||
if not emails:
|
if not emails:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Emails are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
requesting_user = ProjectMember.objects.get(
|
requesting_user = ProjectMember.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, member_id=request.user.id
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member_id=request.user.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if any invited user has an higher role
|
# Check if any invited user has an higher role
|
||||||
@@ -550,7 +582,9 @@ class ProjectJoinEndpoint(BaseAPIView):
|
|||||||
_ = WorkspaceMember.objects.create(
|
_ = WorkspaceMember.objects.create(
|
||||||
workspace_id=project_invite.workspace_id,
|
workspace_id=project_invite.workspace_id,
|
||||||
member=user,
|
member=user,
|
||||||
role=15 if project_invite.role >= 15 else project_invite.role,
|
role=15
|
||||||
|
if project_invite.role >= 15
|
||||||
|
else project_invite.role,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Else make him active
|
# Else make him active
|
||||||
@@ -660,7 +694,8 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
sort_order = [
|
sort_order = [
|
||||||
project_member.get("sort_order")
|
project_member.get("sort_order")
|
||||||
for project_member in project_members
|
for project_member in project_members
|
||||||
if str(project_member.get("member_id")) == str(member.get("member_id"))
|
if str(project_member.get("member_id"))
|
||||||
|
== str(member.get("member_id"))
|
||||||
]
|
]
|
||||||
bulk_project_members.append(
|
bulk_project_members.append(
|
||||||
ProjectMember(
|
ProjectMember(
|
||||||
@@ -668,7 +703,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
role=member.get("role", 10),
|
role=member.get("role", 10),
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace_id=project.workspace_id,
|
workspace_id=project.workspace_id,
|
||||||
sort_order=sort_order[0] - 10000 if len(sort_order) else 65535,
|
sort_order=sort_order[0] - 10000
|
||||||
|
if len(sort_order)
|
||||||
|
else 65535,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
bulk_issue_props.append(
|
bulk_issue_props.append(
|
||||||
@@ -713,13 +750,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
project_member = ProjectMember.objects.get(
|
# Get the list of project members for the project
|
||||||
member=request.user,
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
project_members = ProjectMember.objects.filter(
|
project_members = ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
@@ -727,10 +758,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
).select_related("project", "member", "workspace")
|
).select_related("project", "member", "workspace")
|
||||||
|
|
||||||
if project_member.role > 10:
|
serializer = ProjectMemberRoleSerializer(
|
||||||
serializer = ProjectMemberAdminSerializer(project_members, many=True)
|
project_members, fields=("id", "member", "role"), many=True
|
||||||
else:
|
)
|
||||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
@@ -758,7 +788,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
> requested_project_member.role
|
> requested_project_member.role
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot update a role that is higher than your own role"},
|
{
|
||||||
|
"error": "You cannot update a role that is higher than your own role"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -797,7 +829,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
# User cannot deactivate higher role
|
# User cannot deactivate higher role
|
||||||
if requesting_project_member.role < project_member.role:
|
if requesting_project_member.role < project_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than you"},
|
{
|
||||||
|
"error": "You cannot remove a user having role higher than you"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -848,7 +882,8 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if len(team_members) == 0:
|
if len(team_members) == 0:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "No such team exists"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "No such team exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
@@ -895,7 +930,8 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if name == "":
|
if name == "":
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
exists = ProjectIdentifier.objects.filter(
|
exists = ProjectIdentifier.objects.filter(
|
||||||
@@ -912,16 +948,23 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if name == "":
|
if name == "":
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"},
|
||||||
)
|
|
||||||
|
|
||||||
if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
|
|
||||||
return Response(
|
|
||||||
{"error": "Cannot delete an identifier of an existing project"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
|
if Project.objects.filter(
|
||||||
|
identifier=name, workspace__slug=slug
|
||||||
|
).exists():
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Cannot delete an identifier of an existing project"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectIdentifier.objects.filter(
|
||||||
|
name=name, workspace__slug=slug
|
||||||
|
).delete()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=status.HTTP_204_NO_CONTENT,
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
@@ -939,7 +982,9 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if project_member is None:
|
if project_member is None:
|
||||||
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
|
return Response(
|
||||||
|
{"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
view_props = project_member.view_props
|
view_props = project_member.view_props
|
||||||
default_props = project_member.default_props
|
default_props = project_member.default_props
|
||||||
@@ -947,8 +992,12 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
sort_order = project_member.sort_order
|
sort_order = project_member.sort_order
|
||||||
|
|
||||||
project_member.view_props = request.data.get("view_props", view_props)
|
project_member.view_props = request.data.get("view_props", view_props)
|
||||||
project_member.default_props = request.data.get("default_props", default_props)
|
project_member.default_props = request.data.get(
|
||||||
project_member.preferences = request.data.get("preferences", preferences)
|
"default_props", default_props
|
||||||
|
)
|
||||||
|
project_member.preferences = request.data.get(
|
||||||
|
"preferences", preferences
|
||||||
|
)
|
||||||
project_member.sort_order = request.data.get("sort_order", sort_order)
|
project_member.sort_order = request.data.get("sort_order", sort_order)
|
||||||
|
|
||||||
project_member.save()
|
project_member.save()
|
||||||
@@ -1010,18 +1059,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
files = []
|
files = []
|
||||||
s3_client_params = {
|
s3 = boto3.client(
|
||||||
"service_name": "s3",
|
"s3",
|
||||||
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
}
|
)
|
||||||
|
|
||||||
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
|
|
||||||
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
|
|
||||||
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
|
|
||||||
|
|
||||||
s3 = boto3.client(**s3_client_params)
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
"Prefix": "static/project-cover/",
|
"Prefix": "static/project-cover/",
|
||||||
@@ -1034,19 +1076,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
|||||||
if not content["Key"].endswith(
|
if not content["Key"].endswith(
|
||||||
"/"
|
"/"
|
||||||
): # This line ensures we're only getting files, not "sub-folders"
|
): # This line ensures we're only getting files, not "sub-folders"
|
||||||
if (
|
files.append(
|
||||||
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
|
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||||
and settings.AWS_S3_CUSTOM_DOMAIN
|
)
|
||||||
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
|
|
||||||
and settings.AWS_S3_URL_PROTOCOL
|
|
||||||
):
|
|
||||||
files.append(
|
|
||||||
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
files.append(
|
|
||||||
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(files, status=status.HTTP_200_OK)
|
return Response(files, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@@ -1113,6 +1145,7 @@ class UserProjectRolesEndpoint(BaseAPIView):
|
|||||||
).values("project_id", "role")
|
).values("project_id", "role")
|
||||||
|
|
||||||
project_members = {
|
project_members = {
|
||||||
str(member["project_id"]): member["role"] for member in project_members
|
str(member["project_id"]): member["role"]
|
||||||
|
for member in project_members
|
||||||
}
|
}
|
||||||
return Response(project_members, status=status.HTTP_200_OK)
|
return Response(project_members, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
|
from plane.db.models import (
|
||||||
|
Workspace,
|
||||||
|
Project,
|
||||||
|
Issue,
|
||||||
|
Cycle,
|
||||||
|
Module,
|
||||||
|
Page,
|
||||||
|
IssueView,
|
||||||
|
)
|
||||||
from plane.utils.issue_search import search_issues
|
from plane.utils.issue_search import search_issues
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +33,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return (
|
return (
|
||||||
Workspace.objects.filter(q, workspace_member__member=self.request.user)
|
Workspace.objects.filter(
|
||||||
|
q, workspace_member__member=self.request.user
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.values("name", "id", "slug")
|
.values("name", "id", "slug")
|
||||||
)
|
)
|
||||||
@@ -38,7 +48,8 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
return (
|
return (
|
||||||
Project.objects.filter(
|
Project.objects.filter(
|
||||||
q,
|
q,
|
||||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
Q(project_projectmember__member=self.request.user)
|
||||||
|
| Q(network=2),
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -169,7 +180,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get(
|
||||||
|
"workspace_search", "false"
|
||||||
|
)
|
||||||
project_id = request.query_params.get("project_id", False)
|
project_id = request.query_params.get("project_id", False)
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
@@ -209,7 +222,9 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
class IssueSearchEndpoint(BaseAPIView):
|
class IssueSearchEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
query = request.query_params.get("search", False)
|
query = request.query_params.get("search", False)
|
||||||
workspace_search = request.query_params.get("workspace_search", "false")
|
workspace_search = request.query_params.get(
|
||||||
|
"workspace_search", "false"
|
||||||
|
)
|
||||||
parent = request.query_params.get("parent", "false")
|
parent = request.query_params.get("parent", "false")
|
||||||
issue_relation = request.query_params.get("issue_relation", "false")
|
issue_relation = request.query_params.get("issue_relation", "false")
|
||||||
cycle = request.query_params.get("cycle", "false")
|
cycle = request.query_params.get("cycle", "false")
|
||||||
@@ -234,9 +249,9 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||||||
issues = issues.filter(
|
issues = issues.filter(
|
||||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
~Q(pk=issue_id), ~Q(pk=issue.parent_id), parent__isnull=True
|
||||||
).exclude(
|
).exclude(
|
||||||
pk__in=Issue.issue_objects.filter(parent__isnull=False).values_list(
|
pk__in=Issue.issue_objects.filter(
|
||||||
"parent_id", flat=True
|
parent__isnull=False
|
||||||
)
|
).values_list("parent_id", flat=True)
|
||||||
)
|
)
|
||||||
if issue_relation == "true" and issue_id:
|
if issue_relation == "true" and issue_id:
|
||||||
issue = Issue.issue_objects.get(pk=issue_id)
|
issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
|
|||||||
@@ -77,16 +77,21 @@ class StateViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
if state.default:
|
||||||
return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Default state cannot be deleted"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
||||||
|
|
||||||
if issue_exist:
|
if issue_exist:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The state is not empty, only empty states can be deleted"},
|
{
|
||||||
|
"error": "The state is not empty, only empty states can be deleted"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
state.delete()
|
state.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
is_admin = InstanceAdmin.objects.filter(
|
is_admin = InstanceAdmin.objects.filter(
|
||||||
instance=instance, user=request.user
|
instance=instance, user=request.user
|
||||||
).exists()
|
).exists()
|
||||||
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
def deactivate(self, request):
|
def deactivate(self, request):
|
||||||
# Check all workspace user is active
|
# Check all workspace user is active
|
||||||
@@ -51,7 +53,12 @@ class UserEndpoint(BaseViewSet):
|
|||||||
|
|
||||||
# Instance admin check
|
# Instance admin check
|
||||||
if InstanceAdmin.objects.filter(user=user).exists():
|
if InstanceAdmin.objects.filter(user=user).exists():
|
||||||
return Response({"error": "You cannot deactivate your account since you are an instance admin"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot deactivate your account since you are an instance admin"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
projects_to_deactivate = []
|
projects_to_deactivate = []
|
||||||
workspaces_to_deactivate = []
|
workspaces_to_deactivate = []
|
||||||
@@ -61,7 +68,10 @@ class UserEndpoint(BaseViewSet):
|
|||||||
).annotate(
|
).annotate(
|
||||||
other_admin_exists=Count(
|
other_admin_exists=Count(
|
||||||
Case(
|
Case(
|
||||||
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
|
When(
|
||||||
|
Q(role=20, is_active=True) & ~Q(member=request.user),
|
||||||
|
then=1,
|
||||||
|
),
|
||||||
default=0,
|
default=0,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
@@ -86,7 +96,10 @@ class UserEndpoint(BaseViewSet):
|
|||||||
).annotate(
|
).annotate(
|
||||||
other_admin_exists=Count(
|
other_admin_exists=Count(
|
||||||
Case(
|
Case(
|
||||||
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
|
When(
|
||||||
|
Q(role=20, is_active=True) & ~Q(member=request.user),
|
||||||
|
then=1,
|
||||||
|
),
|
||||||
default=0,
|
default=0,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
@@ -95,7 +108,9 @@ class UserEndpoint(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for workspace in workspaces:
|
for workspace in workspaces:
|
||||||
if workspace.other_admin_exists > 0 or (workspace.total_members == 1):
|
if workspace.other_admin_exists > 0 or (
|
||||||
|
workspace.total_members == 1
|
||||||
|
):
|
||||||
workspace.is_active = False
|
workspace.is_active = False
|
||||||
workspaces_to_deactivate.append(workspace)
|
workspaces_to_deactivate.append(workspace)
|
||||||
else:
|
else:
|
||||||
@@ -134,7 +149,9 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
|||||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||||
@@ -142,14 +159,16 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
|||||||
user = User.objects.get(pk=request.user.id, is_active=True)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{"message": "Updated successfully"}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
|
queryset = IssueActivity.objects.filter(
|
||||||
"actor", "workspace", "issue", "project"
|
actor=request.user
|
||||||
)
|
).select_related("actor", "workspace", "issue", "project")
|
||||||
|
|
||||||
return self.paginate(
|
return self.paginate(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -158,4 +177,3 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
|||||||
issue_activities, many=True
|
issue_activities, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,15 @@ from . import BaseViewSet, BaseAPIView
|
|||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
GlobalViewSerializer,
|
GlobalViewSerializer,
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueLiteSerializer,
|
IssueSerializer,
|
||||||
IssueViewFavoriteSerializer,
|
IssueViewFavoriteSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
from plane.app.permissions import (
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
ProjectEntityPermission,
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Workspace,
|
Workspace,
|
||||||
GlobalView,
|
GlobalView,
|
||||||
@@ -37,14 +42,15 @@ from plane.db.models import (
|
|||||||
IssueReaction,
|
IssueReaction,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
)
|
)
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
|
|
||||||
|
|
||||||
class GlobalViewViewSet(BaseViewSet):
|
class GlobalViewViewSet(BaseViewSet):
|
||||||
serializer_class = GlobalViewSerializer
|
serializer_class = IssueViewSerializer
|
||||||
model = GlobalView
|
model = IssueView
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
]
|
]
|
||||||
@@ -58,6 +64,7 @@ class GlobalViewViewSet(BaseViewSet):
|
|||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project__isnull=True)
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.order_by(self.request.GET.get("order_by", "-created_at"))
|
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||||
.distinct()
|
.distinct()
|
||||||
@@ -72,7 +79,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.issue_objects.annotate(
|
Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -95,11 +104,21 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
@@ -116,17 +135,36 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
subscriber=self.request.user, issue_id=OuterRef("id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -174,17 +212,17 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
|
serializer = IssueSerializer(
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
return Response(
|
|
||||||
issue_dict,
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewViewSet(BaseViewSet):
|
class IssueViewViewSet(BaseViewSet):
|
||||||
@@ -217,6 +255,18 @@ class IssueViewViewSet(BaseViewSet):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
views = IssueViewSerializer(
|
||||||
|
queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
return Response(views, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewFavoriteViewSet(BaseViewSet):
|
class IssueViewFavoriteViewSet(BaseViewSet):
|
||||||
serializer_class = IssueViewFavoriteSerializer
|
serializer_class = IssueViewFavoriteSerializer
|
||||||
@@ -246,4 +296,4 @@ class IssueViewFavoriteViewSet(BaseViewSet):
|
|||||||
view_id=view_id,
|
view_id=view_id,
|
||||||
)
|
)
|
||||||
view_favourite.delete()
|
view_favourite.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ class WebhookEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save(workspace_id=workspace.id)
|
serializer.save(workspace_id=workspace.id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ from plane.app.serializers import (
|
|||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
WorkspaceMemberAdminSerializer,
|
WorkspaceMemberAdminSerializer,
|
||||||
WorkspaceMemberMeSerializer,
|
WorkspaceMemberMeSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
|
WorkspaceUserPropertiesSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.views.base import BaseAPIView
|
from plane.app.views.base import BaseAPIView
|
||||||
from . import BaseViewSet
|
from . import BaseViewSet
|
||||||
@@ -64,6 +66,7 @@ from plane.db.models import (
|
|||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
|
WorkspaceUserProperties,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
WorkSpaceBasePermission,
|
WorkSpaceBasePermission,
|
||||||
@@ -71,11 +74,13 @@ from plane.app.permissions import (
|
|||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
WorkspaceViewerPermission,
|
WorkspaceViewerPermission,
|
||||||
WorkspaceUserPermission,
|
WorkspaceUserPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceViewSet(BaseViewSet):
|
class WorkSpaceViewSet(BaseViewSet):
|
||||||
model = Workspace
|
model = Workspace
|
||||||
serializer_class = WorkSpaceSerializer
|
serializer_class = WorkSpaceSerializer
|
||||||
@@ -111,7 +116,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
self.filter_queryset(super().get_queryset().select_related("owner"))
|
self.filter_queryset(
|
||||||
|
super().get_queryset().select_related("owner")
|
||||||
|
)
|
||||||
.order_by("name")
|
.order_by("name")
|
||||||
.filter(
|
.filter(
|
||||||
workspace_member__member=self.request.user,
|
workspace_member__member=self.request.user,
|
||||||
@@ -137,7 +144,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if len(name) > 80 or len(slug) > 48:
|
if len(name) > 80 or len(slug) > 48:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "The maximum length for name is 80 and for slug is 48"},
|
{
|
||||||
|
"error": "The maximum length for name is 80 and for slug is 48"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,7 +159,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
role=20,
|
role=20,
|
||||||
company_role=request.data.get("company_role", ""),
|
company_role=request.data.get("company_role", ""),
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
[serializer.errors[error][0] for error in serializer.errors],
|
[serializer.errors[error][0] for error in serializer.errors],
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -173,6 +184,11 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
member_count = (
|
member_count = (
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace=OuterRef("id"),
|
workspace=OuterRef("id"),
|
||||||
@@ -204,13 +220,17 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
.annotate(total_members=member_count)
|
.annotate(total_members=member_count)
|
||||||
.annotate(total_issues=issue_count)
|
.annotate(total_issues=issue_count)
|
||||||
.filter(
|
.filter(
|
||||||
workspace_member__member=request.user, workspace_member__is_active=True
|
workspace_member__member=request.user,
|
||||||
|
workspace_member__is_active=True,
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
workspaces = WorkSpaceSerializer(
|
||||||
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
|
self.filter_queryset(workspace),
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
fields=fields if fields else None,
|
||||||
|
many=True,
|
||||||
|
).data
|
||||||
|
return Response(workspaces, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||||
@@ -250,7 +270,8 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
|||||||
# Check if email is provided
|
# Check if email is provided
|
||||||
if not emails:
|
if not emails:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Emails are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# check for role level of the requesting user
|
# check for role level of the requesting user
|
||||||
@@ -407,7 +428,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# Delete the invitation
|
# Delete the invitation
|
||||||
workspace_invite.delete()
|
workspace_invite.delete()
|
||||||
|
|
||||||
# Send event
|
# Send event
|
||||||
workspace_invite_event.delay(
|
workspace_invite_event.delay(
|
||||||
user=user.id if user is not None else None,
|
user=user.id if user is not None else None,
|
||||||
@@ -537,10 +558,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
workspace_members = self.get_queryset()
|
workspace_members = self.get_queryset()
|
||||||
|
|
||||||
if workspace_member.role > 10:
|
if workspace_member.role > 10:
|
||||||
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
serializer = WorkspaceMemberAdminSerializer(
|
||||||
|
workspace_members,
|
||||||
|
fields=("id", "member", "role"),
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
serializer = WorkSpaceMemberSerializer(
|
serializer = WorkSpaceMemberSerializer(
|
||||||
workspace_members,
|
workspace_members,
|
||||||
|
fields=("id", "member", "role"),
|
||||||
many=True,
|
many=True,
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@@ -572,7 +598,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
> requested_workspace_member.role
|
> requested_workspace_member.role
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot update a role that is higher than your own role"},
|
{
|
||||||
|
"error": "You cannot update a role that is higher than your own role"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -611,7 +639,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if requesting_workspace_member.role < workspace_member.role:
|
if requesting_workspace_member.role < workspace_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than you"},
|
{
|
||||||
|
"error": "You cannot remove a user having role higher than you"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -705,6 +735,49 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceProjectMemberEndpoint(BaseAPIView):
|
||||||
|
serializer_class = ProjectMemberRoleSerializer
|
||||||
|
model = ProjectMember
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
# Fetch all project IDs where the user is involved
|
||||||
|
project_ids = (
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.values_list("project_id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all the project members in which the user is involved
|
||||||
|
project_members = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
project_id__in=project_ids,
|
||||||
|
is_active=True,
|
||||||
|
).select_related("project", "member", "workspace")
|
||||||
|
project_members = ProjectMemberRoleSerializer(
|
||||||
|
project_members, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
project_members_dict = dict()
|
||||||
|
|
||||||
|
# Construct a dictionary with project_id as key and project_members as value
|
||||||
|
for project_member in project_members:
|
||||||
|
project_id = project_member.pop("project")
|
||||||
|
if str(project_id) not in project_members_dict:
|
||||||
|
project_members_dict[str(project_id)] = []
|
||||||
|
project_members_dict[str(project_id)].append(project_member)
|
||||||
|
|
||||||
|
return Response(project_members_dict, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class TeamMemberViewSet(BaseViewSet):
|
class TeamMemberViewSet(BaseViewSet):
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
model = Team
|
model = Team
|
||||||
@@ -739,7 +812,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if len(members) != len(request.data.get("members", [])):
|
if len(members) != len(request.data.get("members", [])):
|
||||||
users = list(set(request.data.get("members", [])).difference(members))
|
users = list(
|
||||||
|
set(request.data.get("members", [])).difference(members)
|
||||||
|
)
|
||||||
users = User.objects.filter(pk__in=users)
|
users = User.objects.filter(pk__in=users)
|
||||||
|
|
||||||
serializer = UserLiteSerializer(users, many=True)
|
serializer = UserLiteSerializer(users, many=True)
|
||||||
@@ -753,7 +828,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
serializer = TeamSerializer(data=request.data, context={"workspace": workspace})
|
serializer = TeamSerializer(
|
||||||
|
data=request.data, context={"workspace": workspace}
|
||||||
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@@ -782,7 +859,9 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
|||||||
workspace_id=last_workspace_id, member=request.user
|
workspace_id=last_workspace_id, member=request.user
|
||||||
).select_related("workspace", "project", "member", "workspace__owner")
|
).select_related("workspace", "project", "member", "workspace__owner")
|
||||||
|
|
||||||
project_member_serializer = ProjectMemberSerializer(project_member, many=True)
|
project_member_serializer = ProjectMemberSerializer(
|
||||||
|
project_member, many=True
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -966,7 +1045,11 @@ class WorkspaceThemeViewSet(BaseViewSet):
|
|||||||
serializer_class = WorkspaceThemeSerializer
|
serializer_class = WorkspaceThemeSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
)
|
||||||
|
|
||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
@@ -1229,12 +1312,22 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get(self, request, slug, user_id):
|
def get(self, request, slug, user_id):
|
||||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
# Custom ordering for priority and state
|
# Custom ordering for priority and state
|
||||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
order_by_param = request.GET.get("order_by", "-created_at")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
@@ -1247,7 +1340,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(
|
.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -1268,7 +1363,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
attachment_count=IssueAttachment.objects.filter(
|
||||||
|
issue=OuterRef("id")
|
||||||
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@@ -1278,7 +1375,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
# Priority Ordering
|
# Priority Ordering
|
||||||
if order_by_param == "priority" or order_by_param == "-priority":
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
priority_order = (
|
priority_order = (
|
||||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
)
|
)
|
||||||
issue_queryset = issue_queryset.annotate(
|
issue_queryset = issue_queryset.annotate(
|
||||||
priority_order=Case(
|
priority_order=Case(
|
||||||
@@ -1326,7 +1425,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
else order_by_param
|
else order_by_param
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
@@ -1334,8 +1435,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
issues = IssueLiteSerializer(
|
issues = IssueLiteSerializer(
|
||||||
issue_queryset, many=True, fields=fields if fields else None
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
).data
|
).data
|
||||||
issue_dict = {str(issue["id"]): issue for issue in issues}
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
return Response(issue_dict, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLabelsEndpoint(BaseAPIView):
|
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||||
@@ -1347,5 +1447,43 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
|||||||
labels = Label.objects.filter(
|
labels = Label.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
|
).values(
|
||||||
|
"parent", "name", "color", "id", "project_id", "workspace__slug"
|
||||||
|
)
|
||||||
return Response(labels, status=status.HTTP_200_OK)
|
return Response(labels, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug):
|
||||||
|
workspace_properties = WorkspaceUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_properties.filters = request.data.get(
|
||||||
|
"filters", workspace_properties.filters
|
||||||
|
)
|
||||||
|
workspace_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", workspace_properties.display_filters
|
||||||
|
)
|
||||||
|
workspace_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", workspace_properties.display_properties
|
||||||
|
)
|
||||||
|
workspace_properties.save()
|
||||||
|
|
||||||
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
(
|
||||||
|
workspace_properties,
|
||||||
|
_,
|
||||||
|
) = WorkspaceUserProperties.objects.get_or_create(
|
||||||
|
user=request.user, workspace__slug=slug
|
||||||
|
)
|
||||||
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ def get_assignee_details(slug, filters):
|
|||||||
def get_label_details(slug, filters):
|
def get_label_details(slug, filters):
|
||||||
"""Fetch label details if required"""
|
"""Fetch label details if required"""
|
||||||
return (
|
return (
|
||||||
Issue.objects.filter(workspace__slug=slug, **filters, labels__id__isnull=False)
|
Issue.objects.filter(
|
||||||
|
workspace__slug=slug, **filters, labels__id__isnull=False
|
||||||
|
)
|
||||||
.distinct("labels__id")
|
.distinct("labels__id")
|
||||||
.order_by("labels__id")
|
.order_by("labels__id")
|
||||||
.values("labels__id", "labels__color", "labels__name")
|
.values("labels__id", "labels__color", "labels__name")
|
||||||
@@ -174,7 +176,9 @@ def generate_segmented_rows(
|
|||||||
):
|
):
|
||||||
segment_zero = list(
|
segment_zero = list(
|
||||||
set(
|
set(
|
||||||
item.get("segment") for sublist in distribution.values() for item in sublist
|
item.get("segment")
|
||||||
|
for sublist in distribution.values()
|
||||||
|
for item in sublist
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,7 +197,9 @@ def generate_segmented_rows(
|
|||||||
]
|
]
|
||||||
|
|
||||||
for segment in segment_zero:
|
for segment in segment_zero:
|
||||||
value = next((x.get(key) for x in data if x.get("segment") == segment), "0")
|
value = next(
|
||||||
|
(x.get(key) for x in data if x.get("segment") == segment), "0"
|
||||||
|
)
|
||||||
generated_row.append(value)
|
generated_row.append(value)
|
||||||
|
|
||||||
if x_axis == ASSIGNEE_ID:
|
if x_axis == ASSIGNEE_ID:
|
||||||
@@ -212,7 +218,11 @@ def generate_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == LABEL_ID:
|
if x_axis == LABEL_ID:
|
||||||
label = next(
|
label = next(
|
||||||
(lab for lab in label_details if str(lab[LABEL_ID]) == str(item)),
|
(
|
||||||
|
lab
|
||||||
|
for lab in label_details
|
||||||
|
if str(lab[LABEL_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -221,7 +231,11 @@ def generate_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == STATE_ID:
|
if x_axis == STATE_ID:
|
||||||
state = next(
|
state = next(
|
||||||
(sta for sta in state_details if str(sta[STATE_ID]) == str(item)),
|
(
|
||||||
|
sta
|
||||||
|
for sta in state_details
|
||||||
|
if str(sta[STATE_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,7 +244,11 @@ def generate_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == CYCLE_ID:
|
if x_axis == CYCLE_ID:
|
||||||
cycle = next(
|
cycle = next(
|
||||||
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)),
|
(
|
||||||
|
cyc
|
||||||
|
for cyc in cycle_details
|
||||||
|
if str(cyc[CYCLE_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -239,7 +257,11 @@ def generate_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == MODULE_ID:
|
if x_axis == MODULE_ID:
|
||||||
module = next(
|
module = next(
|
||||||
(mod for mod in module_details if str(mod[MODULE_ID]) == str(item)),
|
(
|
||||||
|
mod
|
||||||
|
for mod in module_details
|
||||||
|
if str(mod[MODULE_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -266,7 +288,11 @@ def generate_segmented_rows(
|
|||||||
if segmented == LABEL_ID:
|
if segmented == LABEL_ID:
|
||||||
for index, segm in enumerate(row_zero[2:]):
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
label = next(
|
label = next(
|
||||||
(lab for lab in label_details if str(lab[LABEL_ID]) == str(segm)),
|
(
|
||||||
|
lab
|
||||||
|
for lab in label_details
|
||||||
|
if str(lab[LABEL_ID]) == str(segm)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if label:
|
if label:
|
||||||
@@ -275,7 +301,11 @@ def generate_segmented_rows(
|
|||||||
if segmented == STATE_ID:
|
if segmented == STATE_ID:
|
||||||
for index, segm in enumerate(row_zero[2:]):
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
state = next(
|
state = next(
|
||||||
(sta for sta in state_details if str(sta[STATE_ID]) == str(segm)),
|
(
|
||||||
|
sta
|
||||||
|
for sta in state_details
|
||||||
|
if str(sta[STATE_ID]) == str(segm)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if state:
|
if state:
|
||||||
@@ -284,7 +314,11 @@ def generate_segmented_rows(
|
|||||||
if segmented == MODULE_ID:
|
if segmented == MODULE_ID:
|
||||||
for index, segm in enumerate(row_zero[2:]):
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
module = next(
|
module = next(
|
||||||
(mod for mod in label_details if str(mod[MODULE_ID]) == str(segm)),
|
(
|
||||||
|
mod
|
||||||
|
for mod in label_details
|
||||||
|
if str(mod[MODULE_ID]) == str(segm)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if module:
|
if module:
|
||||||
@@ -293,7 +327,11 @@ def generate_segmented_rows(
|
|||||||
if segmented == CYCLE_ID:
|
if segmented == CYCLE_ID:
|
||||||
for index, segm in enumerate(row_zero[2:]):
|
for index, segm in enumerate(row_zero[2:]):
|
||||||
cycle = next(
|
cycle = next(
|
||||||
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(segm)),
|
(
|
||||||
|
cyc
|
||||||
|
for cyc in cycle_details
|
||||||
|
if str(cyc[CYCLE_ID]) == str(segm)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if cycle:
|
if cycle:
|
||||||
@@ -315,7 +353,10 @@ def generate_non_segmented_rows(
|
|||||||
):
|
):
|
||||||
rows = []
|
rows = []
|
||||||
for item, data in distribution.items():
|
for item, data in distribution.items():
|
||||||
row = [item, data[0].get("count" if y_axis == "issue_count" else "estimate")]
|
row = [
|
||||||
|
item,
|
||||||
|
data[0].get("count" if y_axis == "issue_count" else "estimate"),
|
||||||
|
]
|
||||||
|
|
||||||
if x_axis == ASSIGNEE_ID:
|
if x_axis == ASSIGNEE_ID:
|
||||||
assignee = next(
|
assignee = next(
|
||||||
@@ -333,7 +374,11 @@ def generate_non_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == LABEL_ID:
|
if x_axis == LABEL_ID:
|
||||||
label = next(
|
label = next(
|
||||||
(lab for lab in label_details if str(lab[LABEL_ID]) == str(item)),
|
(
|
||||||
|
lab
|
||||||
|
for lab in label_details
|
||||||
|
if str(lab[LABEL_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -342,7 +387,11 @@ def generate_non_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == STATE_ID:
|
if x_axis == STATE_ID:
|
||||||
state = next(
|
state = next(
|
||||||
(sta for sta in state_details if str(sta[STATE_ID]) == str(item)),
|
(
|
||||||
|
sta
|
||||||
|
for sta in state_details
|
||||||
|
if str(sta[STATE_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -351,7 +400,11 @@ def generate_non_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == CYCLE_ID:
|
if x_axis == CYCLE_ID:
|
||||||
cycle = next(
|
cycle = next(
|
||||||
(cyc for cyc in cycle_details if str(cyc[CYCLE_ID]) == str(item)),
|
(
|
||||||
|
cyc
|
||||||
|
for cyc in cycle_details
|
||||||
|
if str(cyc[CYCLE_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -360,7 +413,11 @@ def generate_non_segmented_rows(
|
|||||||
|
|
||||||
if x_axis == MODULE_ID:
|
if x_axis == MODULE_ID:
|
||||||
module = next(
|
module = next(
|
||||||
(mod for mod in module_details if str(mod[MODULE_ID]) == str(item)),
|
(
|
||||||
|
mod
|
||||||
|
for mod in module_details
|
||||||
|
if str(mod[MODULE_ID]) == str(item)
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -369,7 +426,10 @@ def generate_non_segmented_rows(
|
|||||||
|
|
||||||
rows.append(tuple(row))
|
rows.append(tuple(row))
|
||||||
|
|
||||||
row_zero = [row_mapping.get(x_axis, "X-Axis"), row_mapping.get(y_axis, "Y-Axis")]
|
row_zero = [
|
||||||
|
row_mapping.get(x_axis, "X-Axis"),
|
||||||
|
row_mapping.get(y_axis, "Y-Axis"),
|
||||||
|
]
|
||||||
return [tuple(row_zero)] + rows
|
return [tuple(row_zero)] + rows
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
|
|
||||||
class BgtasksConfig(AppConfig):
|
class BgtasksConfig(AppConfig):
|
||||||
name = 'plane.bgtasks'
|
name = "plane.bgtasks"
|
||||||
|
|||||||
@@ -40,22 +40,24 @@ def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
|||||||
email,
|
email,
|
||||||
event=event_name,
|
event=event_name,
|
||||||
properties={
|
properties={
|
||||||
"event_id": uuid.uuid4().hex,
|
"event_id": uuid.uuid4().hex,
|
||||||
"user": {"email": email, "id": str(user)},
|
"user": {"email": email, "id": str(user)},
|
||||||
"device_ctx": {
|
"device_ctx": {
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"user_agent": user_agent,
|
"user_agent": user_agent,
|
||||||
},
|
},
|
||||||
"medium": medium,
|
"medium": medium,
|
||||||
"first_time": first_time
|
"first_time": first_time,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_from):
|
def workspace_invite_event(
|
||||||
|
user, email, user_agent, ip, event_name, accepted_from
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
POSTHOG_API_KEY, POSTHOG_HOST = posthogConfiguration()
|
||||||
|
|
||||||
@@ -65,14 +67,14 @@ def workspace_invite_event(user, email, user_agent, ip, event_name, accepted_fro
|
|||||||
email,
|
email,
|
||||||
event=event_name,
|
event=event_name,
|
||||||
properties={
|
properties={
|
||||||
"event_id": uuid.uuid4().hex,
|
"event_id": uuid.uuid4().hex,
|
||||||
"user": {"email": email, "id": str(user)},
|
"user": {"email": email, "id": str(user)},
|
||||||
"device_ctx": {
|
"device_ctx": {
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"user_agent": user_agent,
|
"user_agent": user_agent,
|
||||||
},
|
},
|
||||||
"accepted_from": accepted_from
|
"accepted_from": accepted_from,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user